diff --git a/.gitignore b/.gitignore index 74ec2fe5090eb4c265dad2b6353db799abf7abfe..ce046a954ada404e318a191340d9d4cde391e891 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ build/ # Logs *.log - +# Protobuf +*.pb.go diff --git a/cmd/conch-agent/Makefile b/cmd/conch-agent/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..ee091ca2e9fa29ad3223b7a5d2fe0dafdf4bf6a5 --- /dev/null +++ b/cmd/conch-agent/Makefile @@ -0,0 +1,17 @@ +BINARY_NAME := conch-agent +PROTO_DIR := ./pb +BUILD_DIR := ./build + +.PHONY: build clean + +build: + @go mod tidy + @go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.11 + @go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5.1 + cd $(PROTO_DIR) && protoc --go_out=. --go-grpc_out=require_unimplemented_servers=false:. *.proto + @cd ../ && mkdir -p $(BUILD_DIR) + @go build -o $(BUILD_DIR)/$(BINARY_NAME) . + +clean: + @rm -rf $(BUILD_DIR) + @echo "clean!" diff --git a/cmd/conch-agent/go.mod b/cmd/conch-agent/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..348aa32f7fdafb7ed9a020554f5d6db2c79aa9a7 --- /dev/null +++ b/cmd/conch-agent/go.mod @@ -0,0 +1,13 @@ +module conch-agent + +go 1.23.0 + +require google.golang.org/grpc v1.75.0 + +require ( + golang.org/x/net v0.41.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.26.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect + google.golang.org/protobuf v1.36.8 // indirect +) diff --git a/cmd/conch-agent/go.sum b/cmd/conch-agent/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..c21c8b5700633a8f624eb0b5e58f3a7d59f742b0 --- /dev/null +++ b/cmd/conch-agent/go.sum @@ -0,0 +1,36 @@ +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= diff --git a/cmd/conch-agent/gprc.go b/cmd/conch-agent/gprc.go new file mode 100644 index 0000000000000000000000000000000000000000..dcca1f57ad05369dcddc84c7dccd3689a8a16a8c --- /dev/null +++ b/cmd/conch-agent/gprc.go @@ -0,0 +1,254 @@ +// main.go - Conchd gRPC service implementation +// Implements core gRPC methods: HealthCheck, ExecuteCommand, StartProcess, PostFiles, GetFile +package main + +import ( + "context" + "io" + "log" + "os" + "os/exec" + "path/filepath" + + "conch-agent/pb" +) + +const DirPerm = 0755 +const FilePerm = 0644 + +type ConchdServer struct { + Version string +} + +func (s *ConchdServer) HealthCheck(ctx context.Context, in *pb.Empty) (*pb.CheckReply, error) { + log.Println("Received health check request") + return &pb.CheckReply{Message: "OK"}, nil +} + +func (s *ConchdServer) ExecuteCommand(ctx context.Context, req *pb.CommandRequest) (*pb.CommandResponse, error) { + log.Printf("Received command execution request: %s %v", req.Command, req.Args) + + cmd := exec.Command(req.Command, req.Args...) + output, err := cmd.CombinedOutput() + if err != nil { + log.Printf("Command execution failed: %v", err) + return &pb.CommandResponse{ + Stdout: err.Error(), + }, err + } + + return &pb.CommandResponse{ + Stdout: string(output), + }, nil +} + +// buildErrorResponse creates a unified StartProcessResponse with error information +// This function eliminates duplicate error response construction logic +func buildErrorResponse(errMsg string) *pb.StartProcessResponse { + log.Printf("ERROR: %s", errMsg) + return &pb.StartProcessResponse{ + Stdout: "", + Stderr: "", + ExitCode: -1, + Error: errMsg, + } +} + +// Starts a process with custom working dir, environment, and script content +func (s *ConchdServer) StartProcess(ctx context.Context, req *pb.StartProcessRequest) (*pb.StartProcessResponse, error) { + log.Printf( + "Received start process request: cmd=%s, args=%v, cwd=%s, has_content=%t", + req.Cmd, req.Args, req.Cwd, req.Content != "", + ) + + // Determine working directory (temp dir if not specified) + workDir := req.Cwd + var isTempDir bool + if workDir == "" { + tempDir, err := os.MkdirTemp("", "conchd-job-") + if err != nil { + errMsg := "failed to create temporary working directory: " + err.Error() + return buildErrorResponse(errMsg), nil + } + workDir = tempDir + isTempDir = true + defer func() { + // Clean up temp dir only if process exits successfully + if isTempDir { + os.RemoveAll(workDir) + } + }() + } else { + // Ensure specified working directory exists + if err := os.MkdirAll(workDir, DirPerm); err != nil { + errMsg := "failed to create specified working directory: " + err.Error() + return buildErrorResponse(errMsg), nil + } + } + + // Write script content to file if provided + var scriptPath string + if req.Content != "" { + // Determine script filename based on command type + scriptExt := map[string]string{ + "python": "main.py", + "python3": "main.py", + "python2": "main.py", + "node": "main.js", + "nodejs": "main.js", + "bash": "main.sh", + "sh": "main.sh", + "zsh": "main.sh", + "fish": "main.sh", + "lua": "main.lua", + "ruby": "main.rb", + "rb": "main.rb", + } + scriptName, ok := scriptExt[req.Cmd] + if !ok { + scriptName = "main.py" + } + scriptPath = filepath.Join(workDir, scriptName) + + // Write content to script file + if err := os.WriteFile(scriptPath, []byte(req.Content), FilePerm); err != nil { + errMsg := "failed to write script file: " + err.Error() + return buildErrorResponse(errMsg), nil + } + } + + // Build command arguments (use script path if no args provided) + args := req.Args + if len(args) == 0 && scriptPath != "" { + args = []string{scriptPath} + } + + // Create command with context (supports cancellation) + cmd := exec.CommandContext(ctx, req.Cmd, args...) + cmd.Dir = workDir + + // Set environment variables + env := os.Environ() + for k, v := range req.Env { + env = append(env, k+"="+v) + } + cmd.Env = env + + // Create pipes for stdout/stderr capture + stdoutPipe, err := cmd.StdoutPipe() + if err != nil { + errMsg := "failed to create stdout pipe: " + err.Error() + return buildErrorResponse(errMsg), nil + } + + stderrPipe, err := cmd.StderrPipe() + if err != nil { + errMsg := "failed to create stderr pipe: " + err.Error() + return buildErrorResponse(errMsg), nil + } + + // Start the process + if err := cmd.Start(); err != nil { + errMsg := "failed to start process: " + err.Error() + return buildErrorResponse(errMsg), nil + } + + // Read output from pipes (non-blocking) + outBytes, _ := io.ReadAll(stdoutPipe) + errBytes, _ := io.ReadAll(stderrPipe) + + // Wait for process completion and get exit code + if err := cmd.Wait(); err != nil { + log.Printf("Process exited with error: %v", err) + } + + exitCode := 0 + if cmd.ProcessState != nil { + exitCode = cmd.ProcessState.ExitCode() + } + + return &pb.StartProcessResponse{ + Stdout: string(outBytes), + Stderr: string(errBytes), + ExitCode: int32(exitCode), + Error: "", + }, nil +} + +// Uploads multiple files to specified paths on the server +func (s *ConchdServer) PostFiles(ctx context.Context, req *pb.PostFilesRequest) (*pb.PostFilesResponse, error) { + if len(req.Files) == 0 { + errMsg := "no files provided for upload" + log.Printf("WARN: %s", errMsg) + return &pb.PostFilesResponse{ + UploadedCount: 0, + Error: errMsg, + }, nil + } + + var uploadedCount int32 + for _, file := range req.Files { + if file.Filepath == "" { + errMsg := "empty filepath for upload" + log.Printf("ERROR: %s", errMsg) + return &pb.PostFilesResponse{ + UploadedCount: uploadedCount, + Error: errMsg, + }, nil + } + + // Create parent directories if needed + targetDir := filepath.Dir(file.Filepath) + if err := os.MkdirAll(targetDir, DirPerm); err != nil { + errMsg := "failed to create parent directory for " + file.Filepath + ": " + err.Error() + log.Printf("ERROR: %s", errMsg) + return &pb.PostFilesResponse{ + UploadedCount: uploadedCount, + Error: errMsg, + }, nil + } + + // Write file content to target path + if err := os.WriteFile(file.Filepath, file.Content, FilePerm); err != nil { + errMsg := "failed to write file " + file.Filepath + ": " + err.Error() + log.Printf("ERROR: %s", errMsg) + return &pb.PostFilesResponse{ + UploadedCount: uploadedCount, + Error: errMsg, + }, nil + } + + log.Printf("Successfully uploaded file: %s (size: %d bytes)", file.Filepath, len(file.Content)) + uploadedCount++ + } + + return &pb.PostFilesResponse{ + UploadedCount: uploadedCount, + Error: "", + }, nil +} + +// Reads and returns content of a specified file on the server +func (s *ConchdServer) GetFile(ctx context.Context, req *pb.GetFileRequest) (*pb.GetFileResponse, error) { + if req.Filepath == "" { + errMsg := "filepath is required for file retrieval" + log.Printf("WARN: %s", errMsg) + return &pb.GetFileResponse{Error: errMsg}, nil + } + + content, err := os.ReadFile(req.Filepath) + if err != nil { + var errMsg string + if os.IsNotExist(err) { + errMsg = "file not found: " + req.Filepath + log.Printf("WARN: %s", errMsg) + } else { + errMsg = "failed to read file " + req.Filepath + ": " + err.Error() + log.Printf("WARN: %s", errMsg) + } + return &pb.GetFileResponse{Error: errMsg}, nil + } + + log.Printf("Successfully read file: %s (size: %d bytes)", req.Filepath, len(content)) + return &pb.GetFileResponse{Content: content}, nil +} \ No newline at end of file diff --git a/cmd/conch-agent/main.go b/cmd/conch-agent/main.go new file mode 100644 index 0000000000000000000000000000000000000000..b3e3d220fe3d194c214498b063e743af2b7b367c --- /dev/null +++ b/cmd/conch-agent/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "log" + "net" + "os" + + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" + + "conch-agent/pb" +) + +const ServerPort = ":4064" + +const ServerVersion = "0.0.1" + +func main() { + fmt.Println("This is conch-agent") + fmt.Println("========================================") + + if len(os.Args) > 1 { + fmt.Printf("arguments: %v\n", os.Args[1:]) + } + + listener, err := net.Listen("tcp", ServerPort) + if err != nil { + log.Fatalf("failed to create listener: %v", err) + } + + grpcServer := grpc.NewServer() + pb.RegisterConchdServiceServer(grpcServer, &ConchdServer{Version: ServerVersion}) + + reflection.Register(grpcServer) + log.Printf("conchd gRPC server listening at %v (version: %s)", listener.Addr(), ServerVersion) + + if err := grpcServer.Serve(listener); err != nil { + log.Fatalf("failed to serve gRPC: %v", err) + } + +} diff --git a/cmd/conch-agent/pb/agent.proto b/cmd/conch-agent/pb/agent.proto new file mode 100644 index 0000000000000000000000000000000000000000..2117078416b8b733bb5bc08d4e5603ee48154b8a --- /dev/null +++ b/cmd/conch-agent/pb/agent.proto @@ -0,0 +1,67 @@ +syntax = "proto3"; + +option go_package = "./;pb"; + +package pb; + +message Empty {} + +// ConchdService provides process/command management and file transfer capabilities +service ConchdService { + rpc HealthCheck (Empty) returns (CheckReply); + rpc ExecuteCommand(CommandRequest) returns (CommandResponse); + rpc StartProcess(StartProcessRequest) returns (StartProcessResponse); + rpc PostFiles(PostFilesRequest) returns (PostFilesResponse); + rpc GetFile(GetFileRequest) returns (GetFileResponse); +} + +message CheckReply { + string message = 1; +} + +message CommandRequest { + string command = 1; + repeated string args = 2; +} + +message CommandResponse { + string stdout = 1; +} + +message StartProcessRequest { + string cmd = 1; + repeated string args = 2; + map env = 3; + string cwd = 4; + string content = 5; +} + +message StartProcessResponse { + string stdout = 1; + string stderr = 2; + int32 exit_code = 3; + string error = 4; +} + +message PostFilesRequest { + repeated File files = 1; +} + +message File { + string filepath = 1; + bytes content = 2; +} + +message PostFilesResponse { + int32 uploaded_count = 1; + string error = 2; +} + +message GetFileRequest { + string filepath = 1; +} + +message GetFileResponse { + bytes content = 1; + string error = 2; +} \ No newline at end of file diff --git a/cmd/conchd/main.go b/cmd/conchd/main.go deleted file mode 100644 index 71f960789a372a41f9b1d9fa01698c54aec8f8d8..0000000000000000000000000000000000000000 --- a/cmd/conchd/main.go +++ /dev/null @@ -1,20 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/openeuler/conch/pkg/common" -) - -func main() { - fmt.Println("conchd - Conch agentd") - fmt.Println("========================================") - - message := common.GetMessage("conchd") - fmt.Println(message) - - if len(os.Args) > 1 { - fmt.Printf("arguments: %v\n", os.Args[1:]) - } -}