diff --git a/.circleci/config.yml b/.circleci/config.yml index 7a91342..0f07bea 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,15 +1,21 @@ +version: 2.1 + +orbs: + go: circleci/go@1.11 + env: &env environment: GO111MODULE: auto - GRUNTWORK_INSTALLER_VERSION: v0.0.36 - MODULE_CI_VERSION: v0.54.0 + GRUNTWORK_INSTALLER_VERSION: v0.0.39 + MODULE_CI_VERSION: v0.57.0 TERRATEST_LOG_PARSER_VERSION: v0.37.0 GOLANG_VERSION: 1.21.1 + defaults: &defaults - machine: - enabled: true - image: ubuntu-2004:2022.10.1 <<: *env + docker: + - image: 087285199408.dkr.ecr.us-east-1.amazonaws.com/circle-ci-test-image-base:go1.21.9-tf1.5-tg58.8-pck1.8-ci56.0 + run_precommit: &run_precommit # Fail the build if the pre-commit hooks don't pass. Note: if you run $ pre-commit install locally within this repo, these hooks will # execute automatically every time before you commit, ensuring the build never fails at this step! @@ -17,6 +23,7 @@ run_precommit: &run_precommit command: | pre-commit install pre-commit run --all-files + install_gruntwork_utils: &install_gruntwork_utils name: install gruntwork utils command: | @@ -30,15 +37,13 @@ install_gruntwork_utils: &install_gruntwork_utils --terragrunt-version "NONE" \ --packer-version "NONE" \ --go-version ${GOLANG_VERSION} -version: 2.1 + #---------------------------------------------------------------------------------------------------------------------- # BUILD JOBS #---------------------------------------------------------------------------------------------------------------------- jobs: precommit: - <<: *env - docker: - - image: 087285199408.dkr.ecr.us-east-1.amazonaws.com/circle-ci-test-image-base:go1.21.9-tf1.5-tg39.1-pck1.8-ci54.0 + <<: *defaults steps: - checkout # Fail the build if the pre-commit hooks don't pass. Note: if you run pre-commit install locally, these hooks will @@ -73,13 +78,36 @@ jobs: - run: name: Terratest log parser command: | - gruntwork-install --binary-name 'terratest_log_parser' --repo 'https://github.com/gruntwork-io/terratest' --tag 'v0.30.0' terratest_log_parser --testlog logs/tests.log --outputdir logs when: always - store_artifacts: path: logs - store_test_results: path: logs + build: + resource_class: xlarge + <<: *defaults + steps: + - checkout + - run: + <<: *install_gruntwork_utils + - run: | + go mod tidy + pushd examples/client-server/client + build-go-binaries --app-name terragrunt-iac-engine-client --dest-path bin --ld-flags "-X github.com/gruntwork-io/go-commons/version.Version=$CIRCLE_TAG -extldflags '-static'" + popd + pushd examples/client-server/server + build-go-binaries --app-name terragrunt-iac-engine-server --dest-path bin --ld-flags "-X github.com/gruntwork-io/go-commons/version.Version=$CIRCLE_TAG -extldflags '-static'" + - persist_to_workspace: + root: . + paths: + - examples/client-server/client/bin + - examples/client-server/server/bin + - store_artifacts: + path: examples/client-server/client/bin + - store_artifacts: + path: examples/client-server/server/bin + #---------------------------------------------------------------------------------------------------------------------- # WORKFLOWS #---------------------------------------------------------------------------------------------------------------------- @@ -106,3 +134,15 @@ workflows: filters: tags: only: /^v.*/ + - build: + filters: + tags: + only: /^v.*/ + requires: + - precommit + context: + - AWS__PHXDEVOPS__circle-ci-test + - GCP__automated-tests + - GITHUB__PAT__gruntwork-ci + - APPLE__OSX__code-signing + - TERRAGRUNT_ENGINE__circle-ci diff --git a/.gitignore b/.gitignore index 2bc21d7..d2c1226 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,6 @@ vendor .DS_Store mocks/ .go-version +terragrunt-engine-client +terragrunt-engine-server diff --git a/Makefile b/Makefile index 6e8fe48..d675a20 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ tools: go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.4.0 go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.2 -protoc: $(shell find . -name '*.proto') +protoc: $(shell find ./proto -name '*.proto') protoc --go_out=. --go_opt=paths=source_relative proto/engine.proto protoc --go-grpc_out=. --go-grpc_opt=paths=source_relative proto/engine.proto diff --git a/README.md b/README.md index 6661cd5..d3c6e8b 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,10 @@ Make commands: ## Engines -Impelmentations of the Terragrunt Engine include: +Implementations of the Terragrunt Engine include: - [terragrunt-engine-opentofu](https://github.com/gruntwork-io/terragrunt-engine-opentofu) +- [terragrunt-engine-client-server](./examples/client-server) ## Example Engine Implementation diff --git a/engine/meta.go b/engine/meta.go new file mode 100644 index 0000000..493e0cd --- /dev/null +++ b/engine/meta.go @@ -0,0 +1,46 @@ +package engine + +import ( + "encoding/json" + "fmt" + "strconv" + + "github.com/gruntwork-io/terragrunt-engine-go/proto" + "google.golang.org/protobuf/types/known/structpb" +) + +// Meta extracts request.Meta to go map[string]interface{} struct. +func Meta(request *proto.RunRequest) (map[string]interface{}, error) { + protoMeta := request.Meta + meta := make(map[string]interface{}) + for key, anyValue := range protoMeta { + var value structpb.Value + if err := anyValue.UnmarshalTo(&value); err != nil { + return nil, fmt.Errorf("error unmarshaling any value to structpb.Value: %w", err) + } + jsonData, err := value.MarshalJSON() + if err != nil { + return nil, fmt.Errorf("error marshaling structpb.Value to JSON: %w", err) + } + var result interface{} + if err := json.Unmarshal(jsonData, &result); err != nil { + return nil, fmt.Errorf("error unmarshaling JSON to interface{}: %w", err) + } + meta[key] = result + } + + return meta, nil +} + +// MetaString returns the value of a meta key from the RunRequest. If the key does not exist, an empty string is returned. +func MetaString(request *proto.RunRequest, key string) (string, error) { + meta, err := Meta(request) + if err != nil { + return "", err + } + value, ok := meta[key] + if !ok { + return "", nil + } + return strconv.Unquote(fmt.Sprintf("%v", value)) +} diff --git a/examples/client-server/Dockerfile b/examples/client-server/Dockerfile new file mode 100644 index 0000000..a876011 --- /dev/null +++ b/examples/client-server/Dockerfile @@ -0,0 +1,28 @@ +FROM golang:1.21.13-bullseye as builder + +ARG ENGINE_VERSION=v0.0.3-rc2024081902 + +# clone base engine repo +WORKDIR src +RUN git clone https://github.com/gruntwork-io/terragrunt-engine-go.git -b ${ENGINE_VERSION} . +# add examples from working directory +RUN rm -rf ./examples/* +COPY . ./examples/client-server + +# build engine +WORKDIR examples/client-server +RUN make + +FROM gruntwork/terragrunt:0.2.0 + +ENV LISTEN_ADDRESS=0.0.0.0:50051 + +RUN mise use --global -y opentofu@1.7.0 +RUN ln -s /root/.local/share/mise/installs/opentofu/1.7.0/bin/tofu /bin/tofu + +COPY --from=builder /go/src/examples/client-server/terragrunt-engine-server /opt/terragrunt-engine-server + +RUN mkdir /app +WORKDIR /app +ENTRYPOINT ["/opt/terragrunt-engine-server"] + diff --git a/examples/client-server/Makefile b/examples/client-server/Makefile new file mode 100644 index 0000000..4ebef2f --- /dev/null +++ b/examples/client-server/Makefile @@ -0,0 +1,26 @@ +default: build + +tools: + go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.2 + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.4.0 + go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.2 + +protoc: $(shell find ./examples/client-server -name '*.proto') + protoc --go_out=. --go_opt=paths=source_relative proto/proto.proto + protoc --go-grpc_out=. --go-grpc_opt=paths=source_relative proto/proto.proto + +build: + set -xe ;\ + cd client ;\ + go build -o ../terragrunt-engine-client -ldflags "-extldflags '-static'" . ;\ + cd .. ;\ + cd server ;\ + go build -o ../terragrunt-engine-server -ldflags "-extldflags '-static'" . + +docker: + docker build -t terragrunt-engine-server . + +pre-commit: + pre-commit run --all-files + +.PHONY: default lint protoc test tools diff --git a/examples/client-server/README.md b/examples/client-server/README.md new file mode 100644 index 0000000..2f9da08 --- /dev/null +++ b/examples/client-server/README.md @@ -0,0 +1,39 @@ +# Example Engine Client - Server implementation + +Example implementation of the Terragrunt IaC engine client and server. +Use it only for testing purposes since it is allowing execution of arbitrary bash commands on the server. + +To build the client and server locally, run the `make` command: +```bash +make +``` +This will build the `terragrunt-engine-client` and `terragrunt-engine-server` binaries. + +## Example HCL Configuration + +Here is an example of how you can configure the IaC engine client in your Terragrunt configuration for AMD64 Linux: +* run `docker compose up` to start the server +* prepare the client configuration in `terragrunt.hcl` file +```hcl +# terragrunt.hcl +engine { + source = "./terragrunt-iac-engine-client" + meta = { + # server endpoint + endpoint = "127.0.0.1:50051" + # authentication token + token = get_env("TG_SERVER_TOKEN") + } +} +``` + +Terragrunt execution: +```bash +export TG_EXPERIMENTAL_ENGINE=1 +export TG_SERVER_TOKEN=secret-token + +terragrunt apply --auto-approve +``` + +End to end example: +[![asciicast](https://asciinema.org/a/672387.svg)](https://asciinema.org/a/672387) \ No newline at end of file diff --git a/examples/client-server/client/main.go b/examples/client-server/client/main.go new file mode 100644 index 0000000..0139dcf --- /dev/null +++ b/examples/client-server/client/main.go @@ -0,0 +1,177 @@ +package main + +import ( + "context" + "time" + + "github.com/gruntwork-io/terragrunt-engine-go/examples/client-server/util" + + "google.golang.org/grpc/credentials/insecure" + + "github.com/gruntwork-io/terragrunt-engine-go/engine" + "github.com/hashicorp/go-plugin" + log "github.com/sirupsen/logrus" + + pb "github.com/gruntwork-io/terragrunt-engine-go/examples/client-server/proto" + tgengine "github.com/gruntwork-io/terragrunt-engine-go/proto" + "google.golang.org/grpc" +) + +const ( + iacCommandEnvName = "IAC_COMMAND" + tfAutoApproveEnvName = "TF_IN_AUTOMATION" + connectAddressEnvName = "CONNECT_ADDRESS" + + defaultIacCommand = "tofu" + defaultConnectAddress = "localhost:50051" + + tokenMeta = "token" + endpointMeta = "endpoint" +) + +type Command struct { + Token string + Command string + WorkingDir string + EnvVars map[string]string +} + +type CommandOutput struct { + Output string + Error string + ExitCode int32 +} + +func Run(endpoint string, command *Command) (*CommandOutput, error) { + connectAddress := util.GetEnv(connectAddressEnvName, defaultConnectAddress) + if endpoint != "" { + connectAddress = endpoint + } + log.Infof("Connecting to %s", connectAddress) + conn, err := grpc.NewClient(connectAddress, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, err + } + defer func() { + err := conn.Close() + if err != nil { + log.Errorf("Error closing connection: %v", err) + } + }() + + client := pb.NewShellServiceClient(conn) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + resp, err := client.RunCommand(ctx, &pb.CommandRequest{ + Command: command.Command, + WorkingDir: command.WorkingDir, + EnvVars: command.EnvVars, + Token: command.Token, + }) + if err != nil { + return nil, err + } + + output := &CommandOutput{ + Output: resp.Output, + Error: resp.Error, + ExitCode: resp.ExitCode, + } + return output, nil +} + +type ClientServerEngine struct { + tgengine.UnimplementedEngineServer +} + +func (c *ClientServerEngine) Init(req *tgengine.InitRequest, stream tgengine.Engine_InitServer) error { + err := stream.Send(&tgengine.InitResponse{Stdout: "Client server engine initialized\n", Stderr: "", ResultCode: 0}) + if err != nil { + return err + } + return nil +} + +func (c *ClientServerEngine) Run(req *tgengine.RunRequest, stream tgengine.Engine_RunServer) error { + log.Debugf("Run client command: %v", req.Command) + log.Debugf("Run client args: %v", req.Args) + log.Debugf("Run client dir: %v", req.WorkingDir) + log.Debugf("Run client meta: %v", req.Meta) + iacCommand := util.GetEnv(iacCommandEnvName, defaultIacCommand) + + token, err := engine.MetaString(req, tokenMeta) + if err != nil { + return err + } + + endpoint, err := engine.MetaString(req, endpointMeta) + if err != nil { + return err + } + + // build run command + command := iacCommand + "" + for _, value := range req.Args { + command += " " + value + } + req.EnvVars[tfAutoApproveEnvName] = "true" + + output, err := Run(endpoint, &Command{ + Command: command, + WorkingDir: req.WorkingDir, + EnvVars: req.EnvVars, + Token: token, + }) + if err != nil { + return err + } + err = stream.Send(&tgengine.RunResponse{ + Stdout: output.Output, + Stderr: output.Error, + ResultCode: output.ExitCode, + }) + if err != nil { + return err + } + + return nil +} + +func (c *ClientServerEngine) Shutdown(req *tgengine.ShutdownRequest, stream tgengine.Engine_ShutdownServer) error { + err := stream.Send(&tgengine.ShutdownResponse{Stdout: "Client server engine shutdown\n", Stderr: "", ResultCode: 0}) + if err != nil { + return err + } + return nil +} + +// GRPCServer is used to register with the gRPC server +// +//nolint:unparam // result 0 (error) is always nil +func (c *ClientServerEngine) GRPCServer(_ *plugin.GRPCBroker, s *grpc.Server) error { + tgengine.RegisterEngineServer(s, c) + return nil +} + +// GRPCClient is used to create a client that connects to the +// +//nolint:unparam // result 0 (error) is always nil +func (c *ClientServerEngine) GRPCClient(_ context.Context, _ *plugin.GRPCBroker, client *grpc.ClientConn) (any, error) { + return tgengine.NewEngineClient(client), nil +} + +func main() { + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: plugin.HandshakeConfig{ + ProtocolVersion: 1, + MagicCookieKey: "engine", + MagicCookieValue: "terragrunt", + }, + Plugins: map[string]plugin.Plugin{ + "client-server-engine": &engine.TerragruntGRPCEngine{Impl: &ClientServerEngine{}}, + }, + GRPCServer: plugin.DefaultGRPCServer, + }) +} diff --git a/examples/client-server/docker-compose.yaml b/examples/client-server/docker-compose.yaml new file mode 100644 index 0000000..7ad36ea --- /dev/null +++ b/examples/client-server/docker-compose.yaml @@ -0,0 +1,11 @@ +services: + terragrunt-engine-server: + build: + context: . + dockerfile: Dockerfile + environment: + TOKEN: secret-token + volumes: + - ./:/app + ports: + - "127.0.0.1:50051:50051" diff --git a/examples/client-server/go.mod b/examples/client-server/go.mod new file mode 100644 index 0000000..a263bbf --- /dev/null +++ b/examples/client-server/go.mod @@ -0,0 +1,28 @@ +module github.com/gruntwork-io/terragrunt-engine-go/examples/client-server + +go 1.21 + +require ( + github.com/gruntwork-io/terragrunt-engine-go v0.0.2 + github.com/hashicorp/go-plugin v1.6.1 + github.com/sirupsen/logrus v1.9.3 + google.golang.org/grpc v1.65.0 + google.golang.org/protobuf v1.34.2 +) + +require ( + github.com/fatih/color v1.7.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/hashicorp/go-hclog v0.14.1 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect + github.com/mattn/go-colorable v0.1.4 // indirect + github.com/mattn/go-isatty v0.0.10 // indirect + github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 // indirect + github.com/oklog/run v1.0.0 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect +) + +replace github.com/gruntwork-io/terragrunt-engine-go => ../.. diff --git a/examples/client-server/go.sum b/examples/client-server/go.sum new file mode 100644 index 0000000..8b90ed3 --- /dev/null +++ b/examples/client-server/go.sum @@ -0,0 +1,54 @@ +github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= +github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hashicorp/go-hclog v0.14.1 h1:nQcJDQwIAGnmoUWp8ubocEX40cCml/17YkF6csQLReU= +github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOsVdwI= +github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= +github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 h1:7GoSOOW2jpsfkntVKaS2rAr1TJqfcxotyaUcuxoZSzg= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/client-server/proto/proto.pb.go b/examples/client-server/proto/proto.pb.go new file mode 100644 index 0000000..55bddf8 --- /dev/null +++ b/examples/client-server/proto/proto.pb.go @@ -0,0 +1,269 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.2 +// protoc v3.12.4 +// source: proto/proto.proto + +package proto + +import ( + reflect "reflect" + sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type CommandRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + Command string `protobuf:"bytes,2,opt,name=command,proto3" json:"command,omitempty"` + WorkingDir string `protobuf:"bytes,3,opt,name=working_dir,json=workingDir,proto3" json:"working_dir,omitempty"` + EnvVars map[string]string `protobuf:"bytes,4,rep,name=env_vars,json=envVars,proto3" json:"env_vars,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *CommandRequest) Reset() { + *x = CommandRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_proto_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CommandRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CommandRequest) ProtoMessage() {} + +func (x *CommandRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_proto_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CommandRequest.ProtoReflect.Descriptor instead. +func (*CommandRequest) Descriptor() ([]byte, []int) { + return file_proto_proto_proto_rawDescGZIP(), []int{0} +} + +func (x *CommandRequest) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *CommandRequest) GetCommand() string { + if x != nil { + return x.Command + } + return "" +} + +func (x *CommandRequest) GetWorkingDir() string { + if x != nil { + return x.WorkingDir + } + return "" +} + +func (x *CommandRequest) GetEnvVars() map[string]string { + if x != nil { + return x.EnvVars + } + return nil +} + +type CommandResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Output string `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"` + ExitCode int32 `protobuf:"varint,2,opt,name=exit_code,json=exitCode,proto3" json:"exit_code,omitempty"` + Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"` +} + +func (x *CommandResponse) Reset() { + *x = CommandResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_proto_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CommandResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CommandResponse) ProtoMessage() {} + +func (x *CommandResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_proto_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CommandResponse.ProtoReflect.Descriptor instead. +func (*CommandResponse) Descriptor() ([]byte, []int) { + return file_proto_proto_proto_rawDescGZIP(), []int{1} +} + +func (x *CommandResponse) GetOutput() string { + if x != nil { + return x.Output + } + return "" +} + +func (x *CommandResponse) GetExitCode() int32 { + if x != nil { + return x.ExitCode + } + return 0 +} + +func (x *CommandResponse) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +var File_proto_proto_proto protoreflect.FileDescriptor + +var file_proto_proto_proto_rawDesc = []byte{ + 0x0a, 0x11, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xdc, 0x01, 0x0a, 0x0e, 0x43, + 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, + 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x1f, 0x0a, + 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x69, 0x72, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x44, 0x69, 0x72, 0x12, 0x3d, + 0x0a, 0x08, 0x65, 0x6e, 0x76, 0x5f, 0x76, 0x61, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x22, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x72, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x65, 0x6e, 0x76, 0x56, 0x61, 0x72, 0x73, 0x1a, 0x3a, 0x0a, + 0x0c, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x5c, 0x0a, 0x0f, 0x43, 0x6f, 0x6d, + 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, + 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, + 0x74, 0x70, 0x75, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x65, 0x78, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x64, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x65, 0x78, 0x69, 0x74, 0x43, 0x6f, 0x64, + 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x32, 0x4b, 0x0a, 0x0c, 0x53, 0x68, 0x65, 0x6c, 0x6c, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3b, 0x0a, 0x0a, 0x52, 0x75, 0x6e, 0x43, 0x6f, + 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x6f, + 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_proto_proto_proto_rawDescOnce sync.Once + file_proto_proto_proto_rawDescData = file_proto_proto_proto_rawDesc +) + +func file_proto_proto_proto_rawDescGZIP() []byte { + file_proto_proto_proto_rawDescOnce.Do(func() { + file_proto_proto_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_proto_proto_rawDescData) + }) + return file_proto_proto_proto_rawDescData +} + +var file_proto_proto_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_proto_proto_proto_goTypes = []any{ + (*CommandRequest)(nil), // 0: proto.CommandRequest + (*CommandResponse)(nil), // 1: proto.CommandResponse + nil, // 2: proto.CommandRequest.EnvVarsEntry +} +var file_proto_proto_proto_depIdxs = []int32{ + 2, // 0: proto.CommandRequest.env_vars:type_name -> proto.CommandRequest.EnvVarsEntry + 0, // 1: proto.ShellService.RunCommand:input_type -> proto.CommandRequest + 1, // 2: proto.ShellService.RunCommand:output_type -> proto.CommandResponse + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_proto_proto_proto_init() } +func file_proto_proto_proto_init() { + if File_proto_proto_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proto_proto_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*CommandRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_proto_proto_msgTypes[1].Exporter = func(v any, i int) any { + switch v := v.(*CommandResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_proto_proto_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_proto_proto_goTypes, + DependencyIndexes: file_proto_proto_proto_depIdxs, + MessageInfos: file_proto_proto_proto_msgTypes, + }.Build() + File_proto_proto_proto = out.File + file_proto_proto_proto_rawDesc = nil + file_proto_proto_proto_goTypes = nil + file_proto_proto_proto_depIdxs = nil +} diff --git a/examples/client-server/proto/proto.proto b/examples/client-server/proto/proto.proto new file mode 100644 index 0000000..910e7eb --- /dev/null +++ b/examples/client-server/proto/proto.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package proto; +option go_package = "./proto"; + +service ShellService { + rpc RunCommand (CommandRequest) returns (CommandResponse); +} + +message CommandRequest { + string token = 1; + string command = 2; + string working_dir = 3; + map env_vars = 4; +} + +message CommandResponse { + string output = 1; + int32 exit_code = 2; + string error = 3; +} diff --git a/examples/client-server/proto/proto_grpc.pb.go b/examples/client-server/proto/proto_grpc.pb.go new file mode 100644 index 0000000..8e2db3d --- /dev/null +++ b/examples/client-server/proto/proto_grpc.pb.go @@ -0,0 +1,111 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.4.0 +// - protoc v3.12.4 +// source: proto/proto.proto + +package proto + +import ( + context "context" + + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.62.0 or later. +const _ = grpc.SupportPackageIsVersion8 + +const ( + ShellService_RunCommand_FullMethodName = "/proto.ShellService/RunCommand" +) + +// ShellServiceClient is the client API for ShellService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ShellServiceClient interface { + RunCommand(ctx context.Context, in *CommandRequest, opts ...grpc.CallOption) (*CommandResponse, error) +} + +type shellServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewShellServiceClient(cc grpc.ClientConnInterface) ShellServiceClient { + return &shellServiceClient{cc} +} + +func (c *shellServiceClient) RunCommand(ctx context.Context, in *CommandRequest, opts ...grpc.CallOption) (*CommandResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CommandResponse) + err := c.cc.Invoke(ctx, ShellService_RunCommand_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ShellServiceServer is the server API for ShellService service. +// All implementations must embed UnimplementedShellServiceServer +// for forward compatibility +type ShellServiceServer interface { + RunCommand(context.Context, *CommandRequest) (*CommandResponse, error) + mustEmbedUnimplementedShellServiceServer() +} + +// UnimplementedShellServiceServer must be embedded to have forward compatible implementations. +type UnimplementedShellServiceServer struct { +} + +func (UnimplementedShellServiceServer) RunCommand(context.Context, *CommandRequest) (*CommandResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RunCommand not implemented") +} +func (UnimplementedShellServiceServer) mustEmbedUnimplementedShellServiceServer() {} + +// UnsafeShellServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ShellServiceServer will +// result in compilation errors. +type UnsafeShellServiceServer interface { + mustEmbedUnimplementedShellServiceServer() +} + +func RegisterShellServiceServer(s grpc.ServiceRegistrar, srv ShellServiceServer) { + s.RegisterService(&ShellService_ServiceDesc, srv) +} + +func _ShellService_RunCommand_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CommandRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ShellServiceServer).RunCommand(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ShellService_RunCommand_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ShellServiceServer).RunCommand(ctx, req.(*CommandRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ShellService_ServiceDesc is the grpc.ServiceDesc for ShellService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ShellService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "proto.ShellService", + HandlerType: (*ShellServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "RunCommand", + Handler: _ShellService_RunCommand_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/proto.proto", +} diff --git a/examples/client-server/server/main.go b/examples/client-server/server/main.go new file mode 100644 index 0000000..86c2d67 --- /dev/null +++ b/examples/client-server/server/main.go @@ -0,0 +1,153 @@ +package main + +import ( + "context" + "flag" + "fmt" + "io" + "net" + "os" + "os/exec" + + "github.com/gruntwork-io/terragrunt-engine-go/examples/client-server/util" + + "google.golang.org/grpc" + + pb "github.com/gruntwork-io/terragrunt-engine-go/examples/client-server/proto" + log "github.com/sirupsen/logrus" +) + +const ( + listenAddressEnvName = "LISTEN_ADDRESS" + tokenEnvName = "TOKEN" + defaultListenAddress = ":50051" + + readBufferSize = 1024 +) + +// ShellServiceServer implements the ShellService defined in the proto file. +type ShellServiceServer struct { + pb.UnimplementedShellServiceServer + Token string +} + +// RunCommand validates the token and runs the command. +func (s *ShellServiceServer) RunCommand(_ context.Context, req *pb.CommandRequest) (*pb.CommandResponse, error) { + if req.Token != s.Token { + log.Warnf("Invalid token: %s, expected %s", req.Token, s.Token) + return nil, fmt.Errorf("invalid token") + } + + log.Debugf("Running command: %s in %s", req.Command, req.WorkingDir) + // run command in bash + cmd := exec.Command("bash", "-c", req.Command) + + // Set the working directory if provided + if req.WorkingDir != "" { + cmd.Dir = req.WorkingDir + } + + // Set the environment variables if provided + if len(req.EnvVars) > 0 { + env := os.Environ() + for key, value := range req.EnvVars { + env = append(env, key+"="+value) + } + cmd.Env = env + } + + // Create pipes for stdin, stdout, and stderr + stdin, err := cmd.StdinPipe() + if err != nil { + return nil, err + } + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + stderr, err := cmd.StderrPipe() + if err != nil { + return nil, err + } + + // Start the command + if err := cmd.Start(); err != nil { + return nil, err + } + + defer func() { + if err := stdin.Close(); err != nil { + log.Errorf("Error closing stdin: %v", err) + } + }() + + // Read stdout and stderr + outputChan := make(chan string) + errorChan := make(chan string) + + go readOutput(stdout, outputChan) + go readOutput(stderr, errorChan) + + // Wait for the command to finish + err = cmd.Wait() + + // Collect output and error + output := <-outputChan + errorOutput := <-errorChan + + exitCode := 0 + if err != nil { + if exitError, ok := err.(*exec.ExitError); ok { + exitCode = exitError.ExitCode() + } + } + + return &pb.CommandResponse{ + Output: output, + ExitCode: int32(exitCode), + Error: errorOutput, + }, nil +} + +func readOutput(r io.Reader, ch chan<- string) { + var output string + buf := make([]byte, readBufferSize) + for { + n, err := r.Read(buf) + if n > 0 { + output += string(buf[:n]) + } + if err != nil { + break + } + } + ch <- output +} + +// Serve starts the gRPC server +func Serve(token string) { + address := util.GetEnv(listenAddressEnvName, defaultListenAddress) + listener, err := net.Listen("tcp", address) + if err != nil { + log.Fatalf("Failed to listen: %v", err) + } + grpcServer := grpc.NewServer() + pb.RegisterShellServiceServer(grpcServer, &ShellServiceServer{Token: token}) + log.Infof("Server is running on port " + address) + if err := grpcServer.Serve(listener); err != nil { + log.Fatalf("Failed to serve: %v", err) + } +} + +func main() { + token := util.GetEnv(tokenEnvName, "") + if token == "" { + cliToken := flag.String("token", "", "Token for authenticating requests") + flag.Parse() + if *cliToken == "" { + log.Fatal("Token is required") + } + token = *cliToken + } + Serve(token) +} diff --git a/examples/client-server/util/env.go b/examples/client-server/util/env.go new file mode 100644 index 0000000..0cdb1a3 --- /dev/null +++ b/examples/client-server/util/env.go @@ -0,0 +1,10 @@ +package util + +import "os" + +func GetEnv(key, defaultValue string) string { + if value, exists := os.LookupEnv(key); exists { + return value + } + return defaultValue +} diff --git a/go.mod b/go.mod index 589b654..618dbbf 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,8 @@ go 1.21 require ( github.com/golang/protobuf v1.5.4 github.com/hashicorp/go-plugin v1.6.1 - github.com/stretchr/testify v1.2.2 + github.com/sirupsen/logrus v1.9.3 + github.com/stretchr/testify v1.7.0 google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.34.2 ) @@ -24,4 +25,5 @@ require ( golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) diff --git a/go.sum b/go.sum index e5bcf35..52ed98a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,6 @@ github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= @@ -27,27 +28,28 @@ github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 h1:Di6ANFilr+S60a4S61ZM00vLdw0IrQOSMS2/6mrnOU0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A= google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= -google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= -google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=