From c81e1be37ddc932d4b538667331d3b736b4aff5f Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Wed, 13 Oct 2021 10:57:24 -0300 Subject: [PATCH 001/130] Catch agent crashes (#130) --- go.mod | 3 +- go.sum | 2 ++ main.go | 28 +++++++++++++++---- .../docker_private_image_gcr_bad_creds.rb | 2 +- .../dockerhub_private_image_bad_creds.rb | 2 +- 5 files changed, 29 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 3eb919e3..24027068 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,8 @@ require ( github.com/gorilla/handlers v1.4.0 github.com/gorilla/mux v1.6.2 github.com/kr/pty v1.1.3 - github.com/renderedtext/go-watchman v0.0.0-20200730135545-ce6ef348090b + github.com/mitchellh/panicwrap v1.0.0 + github.com/renderedtext/go-watchman v0.0.0-20210705111746-70108070d074 github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0 github.com/spf13/pflag v1.0.3 github.com/stretchr/testify v1.6.1 diff --git a/go.sum b/go.sum index 8ce24eb8..f0195551 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/kr/pty v1.1.3 h1:/Um6a/ZmD5tF7peoOJ5oN5KMQ0DrGVQSXLNwyckutPk= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/mitchellh/panicwrap v1.0.0 h1:67zIyVakCIvcs69A0FGfZjBdPleaonSgGlXRSRlb6fE= +github.com/mitchellh/panicwrap v1.0.0/go.mod h1:pKvZHwWrZowLUzftuFq7coarnxbBXU4aQh3N0BJOeeA= 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/renderedtext/agent v0.10.3 h1:IEE0tpbVYsul4EwH0p69vJd5SCUiHRtOb+wzp5ggIXQ= diff --git a/main.go b/main.go index e255720a..fa65d9c9 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "os" "time" + "github.com/mitchellh/panicwrap" watchman "github.com/renderedtext/go-watchman" api "github.com/semaphoreci/agent/pkg/api" jobs "github.com/semaphoreci/agent/pkg/jobs" @@ -18,18 +19,30 @@ import ( var VERSION = "dev" func main() { + logFile := OpenLogfile() + log.SetOutput(logFile) + log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile) + + exitStatus, err := panicwrap.BasicWrap(panicHandler) + if err != nil { + panic(err) + } + + // If exitStatus >= 0, then we're the parent process and the panicwrap + // re-executed ourselves and completed. Just exit with the proper status. + if exitStatus >= 0 { + os.Exit(exitStatus) + } + + // Otherwise, exitStatus < 0 means we're the child. Continue executing as normal... // Initialize global randomness rand.Seed(time.Now().UnixNano()) action := os.Args[1] - logfile := OpenLogfile() - log.SetOutput(logfile) - log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile) - switch action { case "serve": - RunServer(logfile) + RunServer(logFile) case "run": RunSingleJob() case "version": @@ -98,3 +111,8 @@ func RunSingleJob() { job.Run() } + +func panicHandler(output string) { + log.Printf("Child agent process panicked:\n\n%s\n", output) + os.Exit(1) +} diff --git a/test/e2e/docker/docker_private_image_gcr_bad_creds.rb b/test/e2e/docker/docker_private_image_gcr_bad_creds.rb index a67d86b2..14c87aaa 100644 --- a/test/e2e/docker/docker_private_image_gcr_bad_creds.rb +++ b/test/e2e/docker/docker_private_image_gcr_bad_creds.rb @@ -54,7 +54,7 @@ {"directive":"Setting up image pull credentials","event":"cmd_started","timestamp":"*"} {"event":"cmd_output","output":"Setting up credentials for GCR\\n","timestamp":"*"} {"event":"cmd_output","output":"cat /tmp/gcr/keyfile.json | docker login -u _json_key --password-stdin https://$GCR_HOSTNAME\\n","timestamp":"*"} - {"event":"cmd_output","output":"Error response from daemon: Get https://gcr.io/v2/: unauthorized: GCR login failed. You may have invalid credentials. To login successfully, follow the steps in: https://cloud.google.com/container-registry/docs/advanced-authentication\\n","timestamp":"*"} + {"event":"cmd_output","output":"Error response from daemon: Get \\"https://gcr.io/v2/\\": unauthorized: GCR login failed. You may have invalid credentials. To login successfully, follow the steps in: https://cloud.google.com/container-registry/docs/advanced-authentication\\n","timestamp":"*"} {"event":"cmd_output","output":"\\n","timestamp":"*"} {"directive":"Setting up image pull credentials","event":"cmd_finished","exit_code":1,"finished_at":"*","started_at":"*","timestamp":"*"} {"event":"job_finished","result":"failed","timestamp":"*"} diff --git a/test/e2e/docker/dockerhub_private_image_bad_creds.rb b/test/e2e/docker/dockerhub_private_image_bad_creds.rb index bf12f812..092c7f71 100644 --- a/test/e2e/docker/dockerhub_private_image_bad_creds.rb +++ b/test/e2e/docker/dockerhub_private_image_bad_creds.rb @@ -53,7 +53,7 @@ {"event":"cmd_started", "timestamp":"*", "directive":"Setting up image pull credentials"} {"event":"cmd_output", "timestamp":"*", "output":"Setting up credentials for DockerHub\\n"} {"event":"cmd_output", "timestamp":"*", "output":"echo $DOCKERHUB_PASSWORD | docker login --username $DOCKERHUB_USERNAME --password-stdin\\n"} - {"event":"cmd_output", "timestamp":"*", "output":"Error response from daemon: Get https://registry-1.docker.io/v2/: unauthorized: incorrect username or password\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Error response from daemon: Get \\"https://registry-1.docker.io/v2/\\": unauthorized: incorrect username or password\\n"} {"event":"cmd_output", "timestamp":"*", "output":"\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"Setting up image pull credentials", "event":"cmd_finished","exit_code":1,"finished_at":"*","started_at":"*","timestamp":"*"} From 171f7011eb2603d2133074abe47b440a5963131a Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Wed, 13 Oct 2021 15:02:42 -0300 Subject: [PATCH 002/130] Fix goreleaser (#131) --- .goreleaser.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 0e55ebd6..da88cf27 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -35,7 +35,7 @@ changelog: - Merge branch brews: - - github: + - tap: owner: semaphoreci name: homebrew-tap folder: Formula From b1f7ab158e00a6e117c242fd58f6494338eeb9be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0ar=C4=8Devi=C4=87?= Date: Mon, 1 Nov 2021 14:16:20 +0100 Subject: [PATCH 003/130] Self-hosted agent implementation (#91) --- .goreleaser.yml | 5 + .semaphore/release.yml | 2 +- .semaphore/semaphore.yml | 90 ++- Dockerfile.empty_ubuntu | 7 + Dockerfile.self_hosted | 12 + Makefile | 48 +- README.md | 85 ++- config.example.yaml | 8 + go.mod | 20 +- go.sum | 610 +++++++++++++++++- install.sh | 121 ++++ lint.toml | 25 + main.go | 182 +++++- pkg/api/job_request.go | 7 + pkg/config/config.go | 46 ++ pkg/eventlogger/default.go | 44 ++ pkg/eventlogger/filebackend.go | 19 +- pkg/eventlogger/formatter.go | 16 + pkg/eventlogger/httpbackend.go | 135 ++++ pkg/executors/authorized_keys.go | 4 + pkg/executors/docker_compose_executor.go | 114 +++- pkg/executors/docker_compose_executor_test.go | 8 +- pkg/executors/docker_compose_file.go | 27 +- pkg/executors/docker_compose_file_test.go | 3 +- pkg/executors/executor.go | 17 +- pkg/executors/shell_executor.go | 28 +- pkg/executors/shell_executor_test.go | 3 +- pkg/httputils/httputils.go | 5 + pkg/jobs/job.go | 284 +++++--- pkg/listener/job_processor.go | 248 +++++++ pkg/listener/listener.go | 115 ++++ pkg/listener/selfhostedapi/api.go | 37 ++ pkg/listener/selfhostedapi/disconnect.go | 37 ++ pkg/listener/selfhostedapi/get_job.go | 45 ++ pkg/listener/selfhostedapi/logs.go | 31 + pkg/listener/selfhostedapi/register.go | 67 ++ pkg/listener/selfhostedapi/sync.go | 83 +++ pkg/osinfo/osinfo.go | 115 ++++ pkg/osinfo/osinfo_test.go | 17 + pkg/retry/retry.go | 24 + pkg/retry/retry_test.go | 29 + pkg/server/server.go | 64 +- pkg/shell/output_buffer.go | 5 +- pkg/shell/process.go | 49 +- pkg/shell/shell.go | 39 +- test/e2e.rb | 193 ++---- test/e2e/docker/broken_unicode.rb | 7 +- test/e2e/docker/check_dev_kvm.rb | 84 ++- test/e2e/docker/command_aliases.rb | 7 +- test/e2e/docker/container_custom_name.rb | 15 +- test/e2e/docker/container_env_vars.rb | 9 +- test/e2e/docker/container_options.rb | 7 +- test/e2e/docker/docker_in_docker.rb | 7 +- test/e2e/docker/docker_private_image_ecr.rb | 15 +- .../docker_private_image_ecr_bad_creds.rb | 15 +- test/e2e/docker/docker_private_image_gcr.rb | 11 +- .../docker_private_image_gcr_bad_creds.rb | 11 +- .../docker/docker_registry_private_image.rb | 15 +- ...docker_registry_private_image_bad_creds.rb | 15 +- test/e2e/docker/dockerhub_private_image.rb | 13 +- .../dockerhub_private_image_bad_creds.rb | 13 +- test/e2e/docker/env_vars.rb | 15 +- test/e2e/docker/epilogue_on_fail.rb | 7 +- test/e2e/docker/epilogue_on_pass.rb | 7 +- test/e2e/docker/failed_job.rb | 7 +- test/e2e/docker/file_injection.rb | 13 +- .../docker/file_injection_broken_file_mode.rb | 9 +- test/e2e/docker/hello_world.rb | 7 +- test/e2e/docker/host_setup_commands.rb | 7 +- test/e2e/docker/job_stopping.rb | 7 +- test/e2e/docker/job_stopping_on_epilogue.rb | 52 ++ test/e2e/docker/multiple_containers.rb | 13 +- test/e2e/docker/no_bash.rb | 7 +- test/e2e/docker/non_existing_image.rb | 7 +- test/e2e/docker/ssh_jump_points.rb | 7 +- test/e2e/docker/stty_restoration.rb | 7 +- test/e2e/docker/unicode.rb | 7 +- test/e2e/docker/unknown_command.rb | 7 +- .../self-hosted/broken_finished_callback.rb | 28 + test/e2e/self-hosted/broken_get_job.rb | 30 + .../self-hosted/broken_teardown_callback.rb | 28 + ...cker_compose_fail_on_missing_host_files.rb | 64 ++ .../docker_compose_host_env_vars.rb | 100 +++ .../self-hosted/docker_compose_host_files.rb | 87 +++ .../docker_compose_missing_host_files.rb | 93 +++ test/e2e/self-hosted/no_ssh_jump_points.rb | 48 ++ test/e2e/self-hosted/shell_host_env_vars.rb | 81 +++ test/e2e/self-hosted/shutdown.rb | 47 ++ .../e2e/self-hosted/shutdown_while_waiting.rb | 8 + test/e2e/shell/broken_unicode.rb | 7 +- test/e2e/shell/command_aliases.rb | 7 +- test/e2e/shell/env_vars.rb | 15 +- test/e2e/shell/epilogue_on_fail.rb | 7 +- test/e2e/shell/epilogue_on_pass.rb | 7 +- test/e2e/shell/failed_job.rb | 7 +- test/e2e/shell/file_injection.rb | 13 +- .../shell/file_injection_broken_file_mode.rb | 9 +- test/e2e/shell/hello_world.rb | 7 +- test/e2e/shell/job_stopping.rb | 7 +- test/e2e/shell/job_stopping_on_epilogue.rb | 52 ++ test/e2e/shell/killing_root_bash.rb | 7 +- test/e2e/shell/set_e.rb | 7 +- test/e2e/shell/set_pipefail.rb | 7 +- test/e2e/shell/ssh_jump_points.rb | 7 +- test/e2e/shell/stty_restoration.rb | 7 +- test/e2e/shell/unicode.rb | 7 +- test/e2e/shell/unknown_command.rb | 7 +- test/e2e_support/api_mode.rb | 181 ++++++ test/e2e_support/docker-compose-listen.yml | 35 + test/e2e_support/listener_mode.rb | 233 +++++++ test/hub_reference/.gitignore | 1 + test/hub_reference/Dockerfile | 5 + test/hub_reference/Gemfile | 4 + test/hub_reference/Gemfile.lock | 31 + test/hub_reference/app.rb | 172 +++++ 115 files changed, 4371 insertions(+), 695 deletions(-) create mode 100644 Dockerfile.empty_ubuntu create mode 100644 Dockerfile.self_hosted create mode 100644 config.example.yaml create mode 100755 install.sh create mode 100644 lint.toml create mode 100644 pkg/config/config.go create mode 100644 pkg/eventlogger/formatter.go create mode 100644 pkg/eventlogger/httpbackend.go create mode 100644 pkg/httputils/httputils.go create mode 100644 pkg/listener/job_processor.go create mode 100644 pkg/listener/listener.go create mode 100644 pkg/listener/selfhostedapi/api.go create mode 100644 pkg/listener/selfhostedapi/disconnect.go create mode 100644 pkg/listener/selfhostedapi/get_job.go create mode 100644 pkg/listener/selfhostedapi/logs.go create mode 100644 pkg/listener/selfhostedapi/register.go create mode 100644 pkg/listener/selfhostedapi/sync.go create mode 100644 pkg/osinfo/osinfo.go create mode 100644 pkg/osinfo/osinfo_test.go create mode 100644 pkg/retry/retry.go create mode 100644 pkg/retry/retry_test.go create mode 100644 test/e2e/docker/job_stopping_on_epilogue.rb create mode 100644 test/e2e/self-hosted/broken_finished_callback.rb create mode 100644 test/e2e/self-hosted/broken_get_job.rb create mode 100644 test/e2e/self-hosted/broken_teardown_callback.rb create mode 100644 test/e2e/self-hosted/docker_compose_fail_on_missing_host_files.rb create mode 100644 test/e2e/self-hosted/docker_compose_host_env_vars.rb create mode 100644 test/e2e/self-hosted/docker_compose_host_files.rb create mode 100644 test/e2e/self-hosted/docker_compose_missing_host_files.rb create mode 100644 test/e2e/self-hosted/no_ssh_jump_points.rb create mode 100644 test/e2e/self-hosted/shell_host_env_vars.rb create mode 100644 test/e2e/self-hosted/shutdown.rb create mode 100644 test/e2e/self-hosted/shutdown_while_waiting.rb create mode 100644 test/e2e/shell/job_stopping_on_epilogue.rb create mode 100644 test/e2e_support/api_mode.rb create mode 100644 test/e2e_support/docker-compose-listen.yml create mode 100644 test/e2e_support/listener_mode.rb create mode 100644 test/hub_reference/.gitignore create mode 100644 test/hub_reference/Dockerfile create mode 100644 test/hub_reference/Gemfile create mode 100644 test/hub_reference/Gemfile.lock create mode 100644 test/hub_reference/app.rb diff --git a/.goreleaser.yml b/.goreleaser.yml index da88cf27..11d203fd 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -13,10 +13,15 @@ builds: goarch: - 386 - amd64 + - arm + - arm64 archives: - id: agent name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' + files: + - README.md + - install.sh replacements: darwin: Darwin linux: Linux diff --git a/.semaphore/release.yml b/.semaphore/release.yml index 0154c61d..8339a26f 100644 --- a/.semaphore/release.yml +++ b/.semaphore/release.yml @@ -14,7 +14,7 @@ blocks: - name: sem-robot-ghtoken prologue: commands: - - sem-version go 1.13 + - sem-version go 1.16 - "export GOPATH=~/go" - "export PATH=/home/semaphore/go/bin:$PATH" - checkout diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index fff7af9c..07c14075 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -5,7 +5,32 @@ agent: type: e1-standard-2 os_image: ubuntu1804 +execution_time_limit: + minutes: 15 + +fail_fast: + stop: + when: true + blocks: + - name: "Lint" + dependencies: [] + task: + env_vars: + - name: GO111MODULE + value: "on" + + prologue: + commands: + - sem-version go 1.16 + - checkout + + jobs: + - name: Lint + commands: + - go get -u github.com/mgechev/revive + - make lint + - name: "Tests" dependencies: [] task: @@ -15,7 +40,7 @@ blocks: prologue: commands: - - sem-version go 1.13 + - sem-version go 1.16 - checkout - go version - go get @@ -35,27 +60,34 @@ blocks: prologue: commands: - - sem-version go 1.13 + - sem-version go 1.16 - checkout - go version - go get - go build + - mkdir /tmp/agent epilogue: commands: - - docker exec -ti agent cat /tmp/agent_log + - if [ "$TEST_MODE" = "api" ]; then docker exec -ti agent cat /tmp/agent_log; else docker logs e2e_support_agent_1; fi + - if [ "$TEST_MODE" = "api" ]; then echo "No hub"; else docker logs e2e_support_hub_1; fi jobs: - name: Shell commands: - "make e2e TEST=shell/$TEST" matrix: + - env_var: TEST_MODE + values: + - api + - listen - env_var: TEST values: - command_aliases - env_vars - failed_job - job_stopping + - job_stopping_on_epilogue - file_injection - file_injection_broken_file_mode - stty_restoration @@ -84,21 +116,27 @@ blocks: prologue: commands: - - sem-version go 1.13 + - sem-version go 1.16 - checkout - go version - go get - go build + - mkdir /tmp/agent epilogue: commands: - - docker exec -ti agent cat /tmp/agent_log + - if [ "$TEST_MODE" = "api" ]; then docker exec -ti agent cat /tmp/agent_log; else docker logs e2e_support_agent_1; fi + - if [ "$TEST_MODE" = "api" ]; then echo "No hub"; else docker logs e2e_support_hub_1; fi jobs: - name: Docker commands: - "make e2e TEST=docker/$TEST" matrix: + - env_var: TEST_MODE + values: + - api + - listen - env_var: TEST values: - hello_world @@ -106,6 +144,7 @@ blocks: - env_vars - failed_job - job_stopping + - job_stopping_on_epilogue - file_injection - file_injection_broken_file_mode - stty_restoration @@ -132,6 +171,47 @@ blocks: - host_setup_commands - multiple_containers + - name: "Self hosted E2E" + dependencies: [] + task: + env_vars: + - name: GO111MODULE + value: "on" + - name: TEST_MODE + value: "listen" + + prologue: + commands: + - sem-version go 1.16 + - checkout + - go version + - go get + - go build + - mkdir /tmp/agent + + epilogue: + commands: + - docker logs e2e_support_agent_1 + - docker logs e2e_support_hub_1 + + jobs: + - name: Self hosted + commands: + - "make e2e TEST=self-hosted/$TEST" + matrix: + - env_var: TEST + values: + - broken_finished_callback + - broken_teardown_callback + - broken_get_job + - docker_compose_host_env_vars + - docker_compose_host_files + - docker_compose_missing_host_files + - docker_compose_fail_on_missing_host_files + - shell_host_env_vars + - shutdown + - shutdown_while_waiting + promotions: - name: Release pipeline_file: "release.yml" diff --git a/Dockerfile.empty_ubuntu b/Dockerfile.empty_ubuntu new file mode 100644 index 00000000..2be025be --- /dev/null +++ b/Dockerfile.empty_ubuntu @@ -0,0 +1,7 @@ +FROM ubuntu:20.04 + +RUN apt-get update -qy +RUN apt-get install -y ca-certificates openssh-client +RUN update-ca-certificates + +WORKDIR /app diff --git a/Dockerfile.self_hosted b/Dockerfile.self_hosted new file mode 100644 index 00000000..987767f3 --- /dev/null +++ b/Dockerfile.self_hosted @@ -0,0 +1,12 @@ +FROM ubuntu:20.04 + +RUN apt-get update -qy +RUN apt-get install -y ca-certificates openssh-client +RUN update-ca-certificates + +ADD build/agent /app/agent +RUN chmod +x /app/agent + +WORKDIR /app + +CMD /app/agent start --endpoint $ENDPOINT --token $TOKEN diff --git a/Makefile b/Makefile index 8e2c8107..097393dc 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,4 @@ +.PHONY: e2e AGENT_PORT_IN_TESTS=30000 AGENT_SSH_PORT_IN_TESTS=2222 @@ -8,6 +9,9 @@ go.install: sudo mv go /usr/local cd - +lint: + revive -formatter friendly -config lint.toml ./... + run: go run *.go run $(JOB) .PHONY: run @@ -22,29 +26,37 @@ test: build: rm -rf build - go build -o build/agent main.go + env GOOS=linux GOARCH=386 go build -o build/agent main.go .PHONY: build -docker.build: build - -docker stop agent - -docker rm agent - docker build -t agent -f Dockerfile.test . -.PHONY: docker.build +e2e: build + ruby test/e2e/$(TEST).rb -docker.run: docker.build - -docker stop agent - docker run --privileged --device /dev/ptmx -v /tmp/agent-temp-directory/:/tmp/agent-temp-directory -v /var/run/docker.sock:/var/run/docker.sock -p $(AGENT_PORT_IN_TESTS):8000 -p $(AGENT_SSH_PORT_IN_TESTS):22 --name agent -tdi agent bash -c "service ssh restart && nohup ./agent serve --auth-token-secret 'TzRVcspTmxhM9fUkdi1T/0kVXNETCi8UdZ8dLM8va4E' & sleep infinity" - sleep 2 -.PHONY: docker.run +e2e.listen.mode.logs: + docker-compose -f test/e2e_support/docker-compose-listen.yml logs -f -docker.clean: - -docker stop $$(docker ps -q) - -docker rm $$(docker ps -qa) -.PHONY: docker.clean +# +# An ubuntu environment that has the ./build/agent CLI mounted. +# This environment is ideal for testing self-hosted agents without the fear +# that some runaway command will mess up your dev environment. +# +empty.ubuntu.machine: + docker run --rm -v $(PWD):/app -ti empty-ubuntu-self-hosted-agent /bin/bash -e2e: docker.clean docker.run - ruby test/e2e/$(TEST).rb -.PHONY: e2e +empty.ubuntu.machine.build: + docker build -f Dockerfile.empty_ubuntu -t empty-ubuntu-self-hosted-agent . + +# +# Docker Release +# +docker.build: + $(MAKE) build + docker build -f Dockerfile.self_hosted -t semaphoreci/agent:latest . + +docker.push: + docker tag semaphoreci/agent:latest semaphoreci/agent:$$(git rev-parse HEAD) + docker push semaphoreci/agent:$$(git rev-parse HEAD) + docker push semaphoreci/agent:latest release.major: git fetch --tags diff --git a/README.md b/README.md index 5566589b..e8664002 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,54 @@ -# Semaphore 2.0 Agents +# Semaphore 2.0 Agent Base agent responsibilities: -- [x] Run jobs -- [x] Provide output logs -- [x] Run a server -- [x] Inject env variables -- [x] Inject files -- [x] Run epilogue commands -- [x] Set up an SSH jump-point +- Run jobs +- Provide output logs +- Run a server +- Inject env variables +- Inject files +- Run epilogue commands +- Set up an SSH jump-point (only available in hosted environment) Docker Based CI/CD: -- [x] Run commands in docker container -- [x] Start multiple docker containers, connect them via DNS -- [x] Checkout source code, run tests -- [x] Inject files and environments variables -- [x] Pull private docker images -- [x] Store and restore files from cache -- [x] Build docker in docker -- [x] Upload docker images from docker -- [x] Set up an SSH jump-point +- Run commands in docker container +- Start multiple docker containers, connect them via DNS +- Checkout source code, run tests +- Inject files and environments variables +- Pull private docker images +- Store and restore files from cache (only available in hosted environment for now) +- Build docker in docker +- Upload docker images from docker +- Set up an SSH jump-point (only available in hosted version) ## Usage -``` bash -agent [command] [flag] -``` +This agent is intended to be used in two environments: hosted or self hosted. -Commands: +### Hosted environment -``` txt - version Print Agent version - serve Start server - run Runs a single job -``` +In the hosted environment, the agent runs inside Semaphore's infrastructure, starting an HTTP server that receives jobs in HTTP requests. The [`agent serve`](#agent-serve-flags) command is used to run the agent in that scenario. + +### Self hosted environment + +In the self hosted environment, you control where you run it and no HTTP server is started; that way, all communication happens from the agent to Semaphore and no HTTP endpoint is required to exist inside your infrastructure. + +The [`agent start`](#agent-start-flags) command is used to run the agent in that scenario. + +## Commands + +### `agent start [flags]` + +Starts the agent in a self hosted environment. The agent will register itself with Semaphore and periodically sync with Semaphore. No HTTP server is started and exposed. Read more about it [in the docs](https://docs.semaphoreci.com/ci-cd-environment/self-hosted-agents-overview). + +### `agent serve [flags]` + +Starts the agent as an HTTP server that receives requests in HTTP requests. Intended only to run jobs in Semaphore's own infrastructure. If you are looking for the command to run the agent in a self hosted environment, check [`agent start`](#agent-start-params). Flags: -``` txt +```txt --auth-token-secret Auth token for accessing the server (required) --port Set a custom port (default 8000) --host Set the bind address to a specific IP (default 0.0.0.0) @@ -47,6 +57,7 @@ Flags: --statsd-host The host where to send StatsD metrics. --statsd-port The port where to send StatsD metrics. --statsd-namespace The prefix to be added to every StatsD metric. +``` Start with defaults: @@ -54,10 +65,7 @@ Start with defaults: agent serve --auth-token-secret 'myJwtToken' ``` -## SSH jump-points - -When a job starts, the public SSH keys sent with the Job Request are injected -into the '~/.ssh/authorized_keys'. +When a job starts, the public SSH keys sent with the Job Request are injected into the `~/.ssh/authorized_keys`. After that, a jump point for accessing the job is set up. For shell based executors this is a simple `bash --login`. For docker compose based executors, @@ -66,12 +74,10 @@ executes `docker-compose exec bash --login`. To SSH into an agent, use: -``` bash +```bash ssh -t -p bash /tmp/ssh_jump_point ``` -### Collecting Statds Metrics from the Agent - If configured, the Agent can publish the following StatsD metrics: - compose_ci.docker_pull.duration, tagged with: [image name] @@ -85,7 +91,16 @@ To configure Statsd publishing provide the following command-line flags to the A Example Usage: -agent start --statsd-host "192.1.1.9" --statsd-port 8192 --statsd-namespace "agent.prod" +``` +agent serve --statsd-host "192.1.1.9" --statsd-port 8192 --statsd-namespace "agent.prod" +``` If StatsD flags are not provided, the Agent will not publish any StatsD metric. +### `agent run [path]` + +Runs a single job. Useful for debugging or agent development. It takes the path to the job request YAML file as an argument + +### `agent version` + +Prints out the agent version diff --git a/config.example.yaml b/config.example.yaml new file mode 100644 index 00000000..ae485742 --- /dev/null +++ b/config.example.yaml @@ -0,0 +1,8 @@ +endpoint: "" +token: "" +no-https: false +shutdown-hook-path: "" +disconnect-after-job: false +env-vars: [] +files: [] +fail-on-missing-files: false diff --git a/go.mod b/go.mod index 24027068..e3a4d0a6 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,17 @@ module github.com/semaphoreci/agent require ( + github.com/creack/pty v1.1.17 github.com/dgrijalva/jwt-go v3.2.0+incompatible - github.com/ghodss/yaml v1.0.0 - github.com/gorilla/handlers v1.4.0 - github.com/gorilla/mux v1.6.2 - github.com/kr/pty v1.1.3 + github.com/gorilla/handlers v1.5.1 + github.com/gorilla/mux v1.8.0 github.com/mitchellh/panicwrap v1.0.0 github.com/renderedtext/go-watchman v0.0.0-20210705111746-70108070d074 - github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0 - github.com/spf13/pflag v1.0.3 - github.com/stretchr/testify v1.6.1 - golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613 - golang.org/x/sys v0.0.0-20190204103248-980327fe3c65 // indirect - gopkg.in/yaml.v2 v2.2.2 + github.com/sirupsen/logrus v1.8.1 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.8.1 + github.com/stretchr/testify v1.7.0 + gopkg.in/yaml.v2 v2.4.0 ) -go 1.13 +go 1.16 diff --git a/go.sum b/go.sum index f0195551..4ab0216b 100644 --- a/go.sum +++ b/go.sum @@ -1,39 +1,603 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= +github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gorilla/handlers v1.4.0 h1:XulKRWSQK5uChr4pEgSE4Tc/OcmnU9GJuSwdog/tZsA= -github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= -github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk= -github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/kr/pty v1.1.3 h1:/Um6a/ZmD5tF7peoOJ5oN5KMQ0DrGVQSXLNwyckutPk= -github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/panicwrap v1.0.0 h1:67zIyVakCIvcs69A0FGfZjBdPleaonSgGlXRSRlb6fE= github.com/mitchellh/panicwrap v1.0.0/go.mod h1:pKvZHwWrZowLUzftuFq7coarnxbBXU4aQh3N0BJOeeA= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= 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/renderedtext/agent v0.10.3 h1:IEE0tpbVYsul4EwH0p69vJd5SCUiHRtOb+wzp5ggIXQ= -github.com/renderedtext/go-watchman v0.0.0-20200730135545-ce6ef348090b h1:sZjcW+SvItRxFg0QPm5WvntUclh9FNfXKjHNdMHF7Dg= -github.com/renderedtext/go-watchman v0.0.0-20200730135545-ce6ef348090b/go.mod h1:03mXTO745K1BK5zKy7Z6/uaMCw05BNhvGyC4A3AwVxo= -github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0 h1:X9XMOYjxEfAYSy3xK1DzO5dMkkWhs9E9UCcS1IERx2k= -github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0/go.mod h1:Ad7IjTpvzZO8Fl0vh9AzQ+j/jYZfyp2diGwI8m5q+ns= -github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/renderedtext/go-watchman v0.0.0-20210705111746-70108070d074 h1:6YLKgGc2PwDM3oEpAUavoiyjjpIH44HmjWSswwWTBa8= +github.com/renderedtext/go-watchman v0.0.0-20210705111746-70108070d074/go.mod h1:Z+qanDzSoUGCbcrTM7G6YCA9ST2KBdte7sCz+HQAp7I= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44= +github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613 h1:MQ/ZZiDsUapFFiMS+vzwXkCTeEKaum+Do5rINYJDmxc= -golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/sys v0.0.0-20190204103248-980327fe3c65 h1:kWe3kjq30rdgl/nMB+ZR7kWcMnlZZ35LNkvwLN7yBco= -golang.org/x/sys v0.0.0-20190204103248-980327fe3c65/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/alexcesaro/statsd.v2 v2.0.0 h1:FXkZSCZIH17vLCO5sO2UucTHsH9pc+17F6pl3JVCwMc= gopkg.in/alexcesaro/statsd.v2 v2.0.0/go.mod h1:i0ubccKGzBVNBpdGV5MocxyA/XlLUJzA7SLonnE4drU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/install.sh b/install.sh new file mode 100755 index 00000000..4cc30af3 --- /dev/null +++ b/install.sh @@ -0,0 +1,121 @@ +#!/bin/bash + +set -e +set -o pipefail + +AGENT_INSTALLATION_DIRECTORY="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +LOGGED_IN_USER=$(logname) + +if [[ "$EUID" -ne 0 ]]; then + echo "Please run with sudo." + exit 1 +fi + +if [[ -z $SEMAPHORE_ORGANIZATION ]]; then + read -p "Enter organization: " SEMAPHORE_ORGANIZATION + if [[ -z $SEMAPHORE_ORGANIZATION ]]; then + echo "Organization cannot be empty." + exit 1 + fi +fi + +if [[ -z $SEMAPHORE_REGISTRATION_TOKEN ]]; then + read -p "Enter registration token: " SEMAPHORE_REGISTRATION_TOKEN + if [[ -z $SEMAPHORE_REGISTRATION_TOKEN ]]; then + echo "Registration token cannot be empty." + exit 1 + fi +fi + +if [[ -z $SEMAPHORE_AGENT_INSTALLATION_USER ]]; then + read -p "Enter user [$LOGGED_IN_USER]: " SEMAPHORE_AGENT_INSTALLATION_USER + SEMAPHORE_AGENT_INSTALLATION_USER="${SEMAPHORE_AGENT_INSTALLATION_USER:=$LOGGED_IN_USER}" +fi + +if ! id "$SEMAPHORE_AGENT_INSTALLATION_USER" &>/dev/null; then + echo "User $SEMAPHORE_AGENT_INSTALLATION_USER does not exist. Exiting..." + exit 1 +fi + +# +# Download toolbox +# +echo "Installing toolbox..." +USER_HOME_DIRECTORY=$(sudo -u $SEMAPHORE_AGENT_INSTALLATION_USER -H bash -c 'echo $HOME') +TOOLBOX_DIRECTORY="$USER_HOME_DIRECTORY/.toolbox" +if [[ -d "$TOOLBOX_DIRECTORY" ]]; then + echo "Toolbox was already installed at $TOOLBOX_DIRECTORY. Overriding it..." + rm -rf "$TOOLBOX_DIRECTORY" +fi + +curl -sL "https://github.com/semaphoreci/toolbox/releases/latest/download/self-hosted-linux.tar" -o toolbox.tar +tar -xf toolbox.tar + +mv toolbox $TOOLBOX_DIRECTORY +sudo chown -R $SEMAPHORE_AGENT_INSTALLATION_USER:$SEMAPHORE_AGENT_INSTALLATION_USER $TOOLBOX_DIRECTORY + +sudo -u $SEMAPHORE_AGENT_INSTALLATION_USER -H bash $TOOLBOX_DIRECTORY/install-toolbox +echo "source ~/.toolbox/toolbox" >> $USER_HOME_DIRECTORY/.bash_profile +rm toolbox.tar + +# +# Create agent config +# +AGENT_CONFIG=$(cat <<-END +endpoint: "$SEMAPHORE_ORGANIZATION.semaphoreci.com" +token: "$SEMAPHORE_REGISTRATION_TOKEN" +no-https: false +shutdown-hook-path: "" +disconnect-after-job: false +env-vars: [] +files: [] +fail-on-missing-files: false +END +) + +AGENT_CONFIG_PATH="$AGENT_INSTALLATION_DIRECTORY/config.yaml" +echo "Creating agent config file at $AGENT_CONFIG_PATH..." +echo "$AGENT_CONFIG" > $AGENT_CONFIG_PATH +sudo chown $SEMAPHORE_AGENT_INSTALLATION_USER:$SEMAPHORE_AGENT_INSTALLATION_USER $AGENT_CONFIG_PATH + +# +# Create systemd service +# +SYSTEMD_SERVICE=$(cat <<-END +[Unit] +Description=Semaphore agent +After=network.target +StartLimitIntervalSec=0 + +[Service] +Type=simple +Restart=always +RestartSec=5 +User=$SEMAPHORE_AGENT_INSTALLATION_USER +WorkingDirectory=$AGENT_INSTALLATION_DIRECTORY +ExecStart=$AGENT_INSTALLATION_DIRECTORY/agent start --config-file $AGENT_CONFIG_PATH + +[Install] +WantedBy=multi-user.target +END +) + +SYSTEMD_PATH=/etc/systemd/system +SERVICE_NAME=semaphore-agent +SYSTEMD_SERVICE_PATH=$SYSTEMD_PATH/$SERVICE_NAME.service + +echo "Creating $SYSTEMD_SERVICE_PATH..." + +if [[ -f "$SYSTEMD_SERVICE_PATH" ]]; then + echo "systemd service already exists at $SYSTEMD_SERVICE_PATH. Overriding it..." + echo "$SYSTEMD_SERVICE" > $SYSTEMD_SERVICE_PATH + systemctl daemon-reload + echo "Restarting semaphore-agent service..." + systemctl restart semaphore-agent +else + echo "$SYSTEMD_SERVICE" > $SYSTEMD_SERVICE_PATH + echo "Starting semaphore-agent service..." + systemctl start semaphore-agent +fi + +echo "Done." \ No newline at end of file diff --git a/lint.toml b/lint.toml new file mode 100644 index 00000000..34bf477f --- /dev/null +++ b/lint.toml @@ -0,0 +1,25 @@ +ignoreGeneratedHeader = false +severity = "warning" +confidence = 0.8 +errorCode = 1 +warningCode = 1 + +[rule.blank-imports] +[rule.context-as-argument] +[rule.context-keys-type] +[rule.dot-imports] +[rule.error-return] +[rule.error-strings] +[rule.error-naming] +# [rule.exported] +[rule.if-return] +[rule.increment-decrement] +[rule.var-naming] +[rule.var-declaration] +[rule.package-comments] +[rule.range] +[rule.receiver-naming] +[rule.time-naming] +[rule.unexported-return] +[rule.indent-error-flow] +[rule.errorf] diff --git a/main.go b/main.go index fa65d9c9..869c36a7 100644 --- a/main.go +++ b/main.go @@ -3,25 +3,32 @@ package main import ( "fmt" "io" - "log" "math/rand" + "net/http" "os" + "strings" "time" "github.com/mitchellh/panicwrap" watchman "github.com/renderedtext/go-watchman" api "github.com/semaphoreci/agent/pkg/api" + "github.com/semaphoreci/agent/pkg/config" + "github.com/semaphoreci/agent/pkg/eventlogger" jobs "github.com/semaphoreci/agent/pkg/jobs" + listener "github.com/semaphoreci/agent/pkg/listener" server "github.com/semaphoreci/agent/pkg/server" + log "github.com/sirupsen/logrus" pflag "github.com/spf13/pflag" + "github.com/spf13/viper" ) var VERSION = "dev" func main() { - logFile := OpenLogfile() - log.SetOutput(logFile) - log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile) + logfile := OpenLogfile() + log.SetOutput(logfile) + log.SetFormatter(new(eventlogger.CustomFormatter)) + log.SetLevel(getLogLevel()) exitStatus, err := panicwrap.BasicWrap(panicHandler) if err != nil { @@ -40,11 +47,15 @@ func main() { action := os.Args[1] + httpClient := &http.Client{} + switch action { + case "start": + RunListener(httpClient, logfile) case "serve": - RunServer(logFile) + RunServer(httpClient, logfile) case "run": - RunSingleJob() + RunSingleJob(httpClient) case "version": fmt.Println(VERSION) } @@ -60,7 +71,151 @@ func OpenLogfile() io.Writer { return io.MultiWriter(f, os.Stdout) } -func RunServer(logfile io.Writer) { +func getLogLevel() log.Level { + logLevel := os.Getenv("LOG_LEVEL") + if logLevel == "" { + return log.InfoLevel + } + + level, err := log.ParseLevel(logLevel) + if err != nil { + log.Fatalf("Log level %s not supported", logLevel) + } + + return level +} + +func RunListener(httpClient *http.Client, logfile io.Writer) { + configFile := pflag.String(config.ConfigFile, "", "Config file") + _ = pflag.String(config.Endpoint, "", "Endpoint where agents are registered") + _ = pflag.String(config.Token, "", "Registration token") + _ = pflag.Bool(config.NoHTTPS, false, "Use http for communication") + _ = pflag.String(config.ShutdownHookPath, "", "Shutdown hook path") + _ = pflag.Bool(config.DisconnectAfterJob, false, "Disconnect after job") + _ = pflag.StringSlice(config.EnvVars, []string{}, "Export environment variables in jobs") + _ = pflag.StringSlice(config.Files, []string{}, "Inject files into container, when using docker compose executor") + _ = pflag.Bool(config.FailOnMissingFiles, false, "Fail job if files specified using --files are missing") + + pflag.Parse() + + if *configFile != "" { + loadConfigFile(*configFile) + } + + viper.BindPFlags(pflag.CommandLine) + + validateConfiguration() + + if viper.GetString(config.Endpoint) == "" { + log.Fatal("Semaphore endpoint was not specified. Exiting...") + } + + if viper.GetString(config.Token) == "" { + log.Fatal("Agent registration token was not specified. Exiting...") + } + + scheme := "https" + if viper.GetBool(config.NoHTTPS) { + scheme = "http" + } + + hostEnvVars, err := ParseEnvVars() + if err != nil { + log.Fatalf("Error parsing --env-vars: %v", err) + } + + fileInjections, err := ParseFiles() + if err != nil { + log.Fatalf("Error parsing --files: %v", err) + } + + config := listener.Config{ + Endpoint: viper.GetString(config.Endpoint), + Token: viper.GetString(config.Token), + RegisterRetryLimit: 30, + Scheme: scheme, + ShutdownHookPath: viper.GetString(config.ShutdownHookPath), + DisconnectAfterJob: viper.GetBool(config.DisconnectAfterJob), + EnvVars: hostEnvVars, + FileInjections: fileInjections, + FailOnMissingFiles: viper.GetBool(config.FailOnMissingFiles), + AgentVersion: VERSION, + } + + go func() { + _, err := listener.Start(httpClient, config, logfile) + if err != nil { + log.Panicf("Could not start agent: %v", err) + } + }() + + select {} +} + +func loadConfigFile(configFile string) { + viper.SetConfigFile(configFile) + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + log.Fatalf("Couldn't find config file %s: %v", configFile, err) + } else { + log.Fatalf("Error reading config file %s: %v", configFile, err) + } + } +} + +func validateConfiguration() { + contains := func(list []string, item string) bool { + for _, x := range list { + if x == item { + return true + } + } + + return false + } + + for _, key := range viper.AllKeys() { + if !contains(config.ValidConfigKeys, key) { + log.Fatalf("Unrecognized option '%s'. Exiting...", key) + } + } +} + +func ParseEnvVars() ([]config.HostEnvVar, error) { + vars := []config.HostEnvVar{} + for _, envVar := range viper.GetStringSlice(config.EnvVars) { + nameAndValue := strings.Split(envVar, "=") + if len(nameAndValue) != 2 { + return nil, fmt.Errorf("%s is not a valid environment variable", envVar) + } + + vars = append(vars, config.HostEnvVar{ + Name: nameAndValue[0], + Value: nameAndValue[1], + }) + } + + return vars, nil +} + +func ParseFiles() ([]config.FileInjection, error) { + fileInjections := []config.FileInjection{} + for _, file := range viper.GetStringSlice(config.Files) { + hostPathAndDestination := strings.Split(file, ":") + if len(hostPathAndDestination) != 2 { + return nil, fmt.Errorf("%s is not a valid file injection", file) + } + + fileInjections = append(fileInjections, config.FileInjection{ + HostPath: hostPathAndDestination[0], + Destination: hostPathAndDestination[1], + }) + } + + return fileInjections, nil +} + +func RunServer(httpClient *http.Client, logfile io.Writer) { authTokenSecret := pflag.String("auth-token-secret", "", "Auth token for accessing the server") port := pflag.Int("port", 8000, "Port of the server") host := pflag.String("host", "0.0.0.0", "Host of the server") @@ -80,7 +235,7 @@ func RunServer(logfile io.Writer) { // Initialize watchman err := watchman.Configure(*statsdHost, *statsdPort, *statsdNamespace) if err != nil { - log.Printf("(err) Failed to configure statsd connection with watchman. Error: %s", err.Error()) + log.Errorf("Failed to configure statsd connection with watchman. Error: %s", err.Error()) } } @@ -92,17 +247,24 @@ func RunServer(logfile io.Writer) { VERSION, logfile, []byte(*authTokenSecret), + httpClient, ).Serve() } -func RunSingleJob() { +func RunSingleJob(httpClient *http.Client) { request, err := api.NewRequestFromYamlFile(os.Args[2]) if err != nil { panic(err) } - job, err := jobs.NewJob(request) + job, err := jobs.NewJobWithOptions(&jobs.JobOptions{ + Request: request, + Client: httpClient, + ExposeKvmDevice: true, + FileInjections: []config.FileInjection{}, + }) + if err != nil { panic(err) } diff --git a/pkg/api/job_request.go b/pkg/api/job_request.go index 5ab6edbc..9c2f284b 100644 --- a/pkg/api/job_request.go +++ b/pkg/api/job_request.go @@ -51,6 +51,12 @@ type Callbacks struct { TeardownFinished string `json:"teardown_finished" yaml:"teardown_finished"` } +type Logger struct { + Method string `json:"method" yaml:"method"` + URL string `json:"url" yaml:"url"` + Token string `json:"token" yaml:"token"` +} + type PublicKey string func (p *PublicKey) Decode() ([]byte, error) { @@ -71,6 +77,7 @@ type JobRequest struct { EnvVars []EnvVar `json:"env_vars" yaml:"env_vars"` Files []File `json:"files" yaml:"file"` Callbacks Callbacks `json:"callbacks" yaml:"callbacks"` + Logger Logger `json:"logger" yaml:"logger"` } func NewRequestFromJSON(content []byte) (*JobRequest, error) { diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 00000000..6a1a1d7d --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,46 @@ +package config + +import "os" + +const ( + ConfigFile = "config-file" + Endpoint = "endpoint" + Token = "token" + NoHTTPS = "no-https" + ShutdownHookPath = "shutdown-hook-path" + DisconnectAfterJob = "disconnect-after-job" + EnvVars = "env-vars" + Files = "files" + FailOnMissingFiles = "fail-on-missing-files" +) + +var ValidConfigKeys = []string{ + ConfigFile, + Endpoint, + Token, + NoHTTPS, + ShutdownHookPath, + DisconnectAfterJob, + EnvVars, + Files, + FailOnMissingFiles, +} + +type HostEnvVar struct { + Name string + Value string +} + +type FileInjection struct { + HostPath string + Destination string +} + +func (f *FileInjection) CheckFileExists() error { + _, err := os.Stat(f.HostPath) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/eventlogger/default.go b/pkg/eventlogger/default.go index 54404681..979f607c 100644 --- a/pkg/eventlogger/default.go +++ b/pkg/eventlogger/default.go @@ -1,5 +1,26 @@ package eventlogger +import ( + "errors" + "fmt" + + "github.com/semaphoreci/agent/pkg/api" +) + +const LoggerMethodPull = "pull" +const LoggerMethodPush = "push" + +func CreateLogger(request *api.JobRequest) (*Logger, error) { + switch request.Logger.Method { + case LoggerMethodPull: + return Default() + case LoggerMethodPush: + return DefaultHTTP(request) + default: + return nil, fmt.Errorf("unknown logger type") + } +} + func Default() (*Logger, error) { backend, err := NewFileBackend("/tmp/job_log.json") if err != nil { @@ -19,6 +40,29 @@ func Default() (*Logger, error) { return logger, nil } +func DefaultHTTP(request *api.JobRequest) (*Logger, error) { + if request.Logger.URL == "" { + return nil, errors.New("HTTP logger needs a URL") + } + + backend, err := NewHTTPBackend(request.Logger.URL, request.Logger.Token) + if err != nil { + return nil, err + } + + logger, err := NewLogger(backend) + if err != nil { + return nil, err + } + + err = logger.Open() + if err != nil { + return nil, err + } + + return logger, nil +} + func DefaultTestLogger() (*Logger, *InMemoryBackend) { backend, err := NewInMemoryBackend() if err != nil { diff --git a/pkg/eventlogger/filebackend.go b/pkg/eventlogger/filebackend.go index da4ad519..7f07f907 100644 --- a/pkg/eventlogger/filebackend.go +++ b/pkg/eventlogger/filebackend.go @@ -5,8 +5,9 @@ import ( "encoding/json" "fmt" "io" - "log" "os" + + log "github.com/sirupsen/logrus" ) type FileBackend struct { @@ -35,7 +36,7 @@ func (l *FileBackend) Write(event interface{}) error { l.file.Write([]byte(jsonString)) l.file.Write([]byte("\n")) - log.Printf("%s", jsonString) + log.Debugf("%s", jsonString) return nil } @@ -44,11 +45,12 @@ func (l *FileBackend) Close() error { return nil } -func (l *FileBackend) Stream(startLine int, writter io.Writer) error { +func (l *FileBackend) Stream(startLine int, writer io.Writer) (int, error) { fd, err := os.OpenFile(l.path, os.O_RDONLY, os.ModePerm) if err != nil { - return err + return startLine, err } + defer fd.Close() reader := bufio.NewReader(fd) @@ -58,7 +60,7 @@ func (l *FileBackend) Stream(startLine int, writter io.Writer) error { line, err := reader.ReadString('\n') if err != nil { if err != io.EOF { - return err + return lineIndex, err } break @@ -67,10 +69,11 @@ func (l *FileBackend) Stream(startLine int, writter io.Writer) error { if lineIndex < startLine { lineIndex++ continue + } else { + lineIndex++ + fmt.Fprintln(writer, line) } - - fmt.Fprintln(writter, line) } - return nil + return lineIndex, nil } diff --git a/pkg/eventlogger/formatter.go b/pkg/eventlogger/formatter.go new file mode 100644 index 00000000..f049564a --- /dev/null +++ b/pkg/eventlogger/formatter.go @@ -0,0 +1,16 @@ +package eventlogger + +import ( + "fmt" + "time" + + log "github.com/sirupsen/logrus" +) + +type CustomFormatter struct { +} + +func (f *CustomFormatter) Format(entry *log.Entry) ([]byte, error) { + log := fmt.Sprintf("%-20s: %s\n", entry.Time.UTC().Format(time.StampMilli), entry.Message) + return []byte(log), nil +} diff --git a/pkg/eventlogger/httpbackend.go b/pkg/eventlogger/httpbackend.go new file mode 100644 index 00000000..369167d5 --- /dev/null +++ b/pkg/eventlogger/httpbackend.go @@ -0,0 +1,135 @@ +package eventlogger + +import ( + "bytes" + "fmt" + "net/http" + "sync" + "time" + + "github.com/semaphoreci/agent/pkg/retry" + log "github.com/sirupsen/logrus" +) + +type HTTPBackend struct { + client *http.Client + url string + token string + fileBackend FileBackend + startFrom int + streamChan chan bool + pushLock sync.Mutex +} + +func NewHTTPBackend(url, token string) (*HTTPBackend, error) { + fileBackend, err := NewFileBackend("/tmp/job_log.json") + if err != nil { + return nil, err + } + + httpBackend := HTTPBackend{ + client: &http.Client{}, + url: url, + token: token, + fileBackend: *fileBackend, + startFrom: 0, + } + + httpBackend.startPushingLogs() + + return &httpBackend, nil +} + +func (l *HTTPBackend) Open() error { + return l.fileBackend.Open() +} + +func (l *HTTPBackend) Write(event interface{}) error { + return l.fileBackend.Write(event) +} + +func (l *HTTPBackend) startPushingLogs() { + log.Debugf("Logs will be pushed to %s", l.url) + + ticker := time.NewTicker(time.Second) + l.streamChan = make(chan bool) + + go func() { + for { + select { + case <-ticker.C: + err := l.pushLogs() + if err != nil { + log.Errorf("Error pushing logs: %v", err) + // we don't retry the request here because a new one will happen in 1s, + // so we only retry these requests on Close() + } + case <-l.streamChan: + ticker.Stop() + return + } + } + }() +} + +func (l *HTTPBackend) stopStreaming() { + if l.streamChan != nil { + close(l.streamChan) + } + + log.Debug("Stopped streaming logs") +} + +func (l *HTTPBackend) pushLogs() error { + l.pushLock.Lock() + defer l.pushLock.Unlock() + + buffer := bytes.NewBuffer([]byte{}) + nextStartFrom, err := l.fileBackend.Stream(l.startFrom, buffer) + if err != nil { + return err + } + + if l.startFrom == nextStartFrom { + log.Debugf("No logs to push - skipping") + // no logs to stream + return nil + } + + url := fmt.Sprintf("%s?start_from=%d", l.url, l.startFrom) + log.Debugf("Pushing logs to %s", url) + request, err := http.NewRequest("POST", url, buffer) + if err != nil { + return err + } + + request.Header.Set("Content-Type", "text/plain") + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", l.token)) + response, err := l.client.Do(request) + if err != nil { + return err + } + + if response.StatusCode != 200 { + return fmt.Errorf("request to %s failed: %s", url, response.Status) + } + + l.startFrom = nextStartFrom + return nil +} + +func (l *HTTPBackend) Close() error { + l.stopStreaming() + + err := retry.RetryWithConstantWait("Push logs", 5, time.Second, func() error { + return l.pushLogs() + }) + + if err != nil { + log.Errorf("Could not push all logs to %s: %v", l.url, err) + } else { + log.Infof("All logs successfully pushed to %s", l.url) + } + + return l.fileBackend.Close() +} diff --git a/pkg/executors/authorized_keys.go b/pkg/executors/authorized_keys.go index 9a11abd2..444d91e1 100644 --- a/pkg/executors/authorized_keys.go +++ b/pkg/executors/authorized_keys.go @@ -8,6 +8,10 @@ import ( ) func InjectEntriesToAuthorizedKeys(keys []api.PublicKey) error { + if len(keys) == 0 { + return nil + } + sshDirectory := filepath.Join(UserHomeDir(), ".ssh") err := os.MkdirAll(sshDirectory, os.ModePerm) diff --git a/pkg/executors/docker_compose_executor.go b/pkg/executors/docker_compose_executor.go index fdf07ede..8304f339 100644 --- a/pkg/executors/docker_compose_executor.go +++ b/pkg/executors/docker_compose_executor.go @@ -4,18 +4,19 @@ import ( "bufio" "fmt" "io/ioutil" - "log" "os" "os/exec" "path" "strings" "time" - pty "github.com/kr/pty" + pty "github.com/creack/pty" watchman "github.com/renderedtext/go-watchman" api "github.com/semaphoreci/agent/pkg/api" + "github.com/semaphoreci/agent/pkg/config" eventlogger "github.com/semaphoreci/agent/pkg/eventlogger" shell "github.com/semaphoreci/agent/pkg/shell" + log "github.com/sirupsen/logrus" ) type DockerComposeExecutor struct { @@ -27,18 +28,30 @@ type DockerComposeExecutor struct { dockerConfiguration api.Compose dockerComposeManifestPath string mainContainerName string + exposeKvmDevice bool + fileInjections []config.FileInjection + FailOnMissingFiles bool } -func NewDockerComposeExecutor(request *api.JobRequest, logger *eventlogger.Logger) *DockerComposeExecutor { +type DockerComposeExecutorOptions struct { + ExposeKvmDevice bool + FileInjections []config.FileInjection + FailOnMissingFiles bool +} + +func NewDockerComposeExecutor(request *api.JobRequest, logger *eventlogger.Logger, options DockerComposeExecutorOptions) *DockerComposeExecutor { return &DockerComposeExecutor{ Logger: logger, jobRequest: request, dockerConfiguration: request.Compose, + exposeKvmDevice: options.ExposeKvmDevice, + fileInjections: options.FileInjections, + FailOnMissingFiles: options.FailOnMissingFiles, dockerComposeManifestPath: "/tmp/docker-compose.yml", tmpDirectory: "/tmp/agent-temp-directory", // make a better random name // during testing the name main gets taken up, if we make it random we avoid headaches - mainContainerName: fmt.Sprintf("%s", request.Compose.Containers[0].Name), + mainContainerName: request.Compose.Containers[0].Name, } } @@ -53,27 +66,51 @@ func (e *DockerComposeExecutor) Prepare() int { return 1 } - compose := ConstructDockerComposeFile(e.dockerConfiguration) - log.Println("Compose File:") - log.Println(compose) + filesToInject, err := e.findValidFilesToInject() + if err != nil { + log.Errorf("Error injecting files: %v", err) + return 1 + } + + compose := ConstructDockerComposeFile(e.dockerConfiguration, e.exposeKvmDevice, filesToInject) + log.Debug("Compose File:") + log.Debug(compose) ioutil.WriteFile(e.dockerComposeManifestPath, []byte(compose), 0644) return e.setUpSSHJumpPoint() } +func (e *DockerComposeExecutor) findValidFilesToInject() ([]config.FileInjection, error) { + filesToInject := []config.FileInjection{} + for _, fileInjection := range e.fileInjections { + err := fileInjection.CheckFileExists() + if err == nil { + filesToInject = append(filesToInject, fileInjection) + } else { + if e.FailOnMissingFiles { + return nil, err + } + + log.Warningf("Error injecting file %s - ignoring it: %v", fileInjection.HostPath, err) + } + } + + return filesToInject, nil +} + func (e *DockerComposeExecutor) executeHostCommands() error { hostCommands := e.jobRequest.Compose.HostSetupCommands for _, c := range hostCommands { - log.Println("Executing Host Command:", c.Directive) + log.Debug("Executing Host Command:", c.Directive) cmd := exec.Command("bash", "-c", c.Directive) out, err := cmd.CombinedOutput() - log.Println(string(out)) + log.Debug(string(out)) if err != nil { - log.Println("Error:", err) + log.Errorf("Error: %v", err) return err } } @@ -84,7 +121,7 @@ func (e *DockerComposeExecutor) setUpSSHJumpPoint() int { err := InjectEntriesToAuthorizedKeys(e.jobRequest.SSHPublicKeys) if err != nil { - log.Printf("Failed to inject authorized keys: %+v", err) + log.Errorf("Failed to inject authorized keys: %+v", err) return 1 } @@ -117,7 +154,7 @@ func (e *DockerComposeExecutor) setUpSSHJumpPoint() int { err = SetUpSSHJumpPoint(script) if err != nil { - log.Printf("Failed to set up SSH jump point: %+v", err) + log.Errorf("Failed to set up SSH jump point: %+v", err) return 1 } @@ -127,13 +164,13 @@ func (e *DockerComposeExecutor) setUpSSHJumpPoint() int { func (e *DockerComposeExecutor) Start() int { exitCode := e.injectImagePullSecrets() if exitCode != 0 { - log.Printf("[SHELL] Failed to set up image pull secrets") + log.Error("[SHELL] Failed to set up image pull secrets") return exitCode } exitCode = e.pullDockerImages() if exitCode != 0 { - log.Printf("Failed to pull images") + log.Error("Failed to pull images") return exitCode } @@ -157,14 +194,16 @@ func (e *DockerComposeExecutor) startBashSession() int { e.Logger.LogCommandOutput("Starting a new bash session.\n") - log.Printf("Starting stateful shell") + log.Debug("Starting stateful shell") cmd := exec.Command( "docker-compose", - "--no-ansi", + "--ansi", + "never", "-f", e.dockerComposeManifestPath, "run", + "--rm", "--name", e.mainContainerName, "-v", @@ -177,7 +216,7 @@ func (e *DockerComposeExecutor) startBashSession() int { shell, err := shell.NewShell(cmd, e.tmpDirectory) if err != nil { - log.Printf("Failed to start stateful shell err: %+v", err) + log.Errorf("Failed to start stateful shell err: %+v", err) e.Logger.LogCommandOutput("Failed to start the docker image\n") e.Logger.LogCommandOutput(err.Error()) @@ -188,7 +227,7 @@ func (e *DockerComposeExecutor) startBashSession() int { err = shell.Start() if err != nil { - log.Printf("Failed to start stateful shell err: %+v", err) + log.Errorf("Failed to start stateful shell err: %+v", err) e.Logger.LogCommandOutput("Failed to start the docker image\n") e.Logger.LogCommandOutput(err.Error()) @@ -450,7 +489,7 @@ func (e *DockerComposeExecutor) injectImagePullSecretsForGCR(envVars []api.EnvVa } func (e *DockerComposeExecutor) pullDockerImages() int { - log.Printf("Pulling docker images") + log.Debug("Pulling docker images") directive := "Pulling docker images..." commandStartedAt := int(time.Now().Unix()) e.SubmitDockerStats("compose.docker.pull.rate") @@ -474,16 +513,18 @@ func (e *DockerComposeExecutor) pullDockerImages() int { cmd := exec.Command( "docker-compose", - "--no-ansi", + "--ansi", + "never", "-f", e.dockerComposeManifestPath, "run", + "--rm", e.mainContainerName, "true") tty, err := pty.Start(cmd) if err != nil { - log.Printf("Failed to initialize docker pull, err: %+v", err) + log.Errorf("Failed to initialize docker pull, err: %+v", err) return 1 } @@ -494,7 +535,7 @@ func (e *DockerComposeExecutor) pullDockerImages() int { break } - log.Println("(tty) ", line) + log.Debug("(tty) ", line) e.Logger.LogCommandOutput(line + "\n") } @@ -502,12 +543,12 @@ func (e *DockerComposeExecutor) pullDockerImages() int { exitCode := 0 if err := cmd.Wait(); err != nil { - log.Println("Docker pull failed", err) + log.Errorf("Docker pull failed: %v", err) e.SubmitDockerStats("compose.docker.error.rate") exitCode = 1 } - log.Println("Docker pull finished. Exit Code", exitCode) + log.Infof("Docker pull finished. Exit Code: %d", exitCode) commandFinishedAt := int(time.Now().Unix()) e.SubmitDockerPullTime(commandFinishedAt - commandStartedAt) @@ -516,9 +557,9 @@ func (e *DockerComposeExecutor) pullDockerImages() int { return exitCode } -func (e *DockerComposeExecutor) ExportEnvVars(envVars []api.EnvVar) int { +func (e *DockerComposeExecutor) ExportEnvVars(envVars []api.EnvVar, hostEnvVars []config.HostEnvVar) int { commandStartedAt := int(time.Now().Unix()) - directive := fmt.Sprintf("Exporting environment variables") + directive := "Exporting environment variables" exitCode := 0 e.Logger.LogCommandStarted(directive) @@ -544,6 +585,11 @@ func (e *DockerComposeExecutor) ExportEnvVars(envVars []api.EnvVar) int { envFile += fmt.Sprintf("export %s=%s\n", env.Name, ShellQuote(string(value))) } + for _, env := range hostEnvVars { + e.Logger.LogCommandOutput(fmt.Sprintf("Exporting %s\n", env.Name)) + envFile += fmt.Sprintf("export %s=%s\n", env.Name, ShellQuote(env.Value)) + } + envPath := fmt.Sprintf("%s/.env", e.tmpDirectory) err := ioutil.WriteFile(envPath, []byte(envFile), 0644) @@ -568,7 +614,7 @@ func (e *DockerComposeExecutor) ExportEnvVars(envVars []api.EnvVar) int { } func (e *DockerComposeExecutor) InjectFiles(files []api.File) int { - directive := fmt.Sprintf("Injecting Files") + directive := "Injecting Files" commandStartedAt := int(time.Now().Unix()) exitCode := 0 @@ -668,16 +714,18 @@ func (e *DockerComposeExecutor) RunCommand(command string, silent bool, alias st } func (e *DockerComposeExecutor) Stop() int { - log.Println("Starting the process killing procedure") + log.Debug("Starting the process killing procedure") - err := e.Shell.Close() - if err != nil { - log.Printf("Process killing procedure returned an erorr %+v\n", err) + if e.Shell != nil { + err := e.Shell.Close() + if err != nil { + log.Errorf("Process killing procedure returned an error %+v\n", err) - return 0 + return 0 + } } - log.Printf("Process killing finished without errors") + log.Debug("Process killing finished without errors") return 0 } diff --git a/pkg/executors/docker_compose_executor_test.go b/pkg/executors/docker_compose_executor_test.go index a2fefd6c..f3482c40 100644 --- a/pkg/executors/docker_compose_executor_test.go +++ b/pkg/executors/docker_compose_executor_test.go @@ -7,6 +7,7 @@ import ( "time" api "github.com/semaphoreci/agent/pkg/api" + "github.com/semaphoreci/agent/pkg/config" eventlogger "github.com/semaphoreci/agent/pkg/eventlogger" assert "github.com/stretchr/testify/assert" ) @@ -48,7 +49,10 @@ func startComposeExecutor() (*DockerComposeExecutor, *eventlogger.Logger, *event testLogger, testLoggerBackend := eventlogger.DefaultTestLogger() - e := NewDockerComposeExecutor(request, testLogger) + e := NewDockerComposeExecutor(request, testLogger, DockerComposeExecutorOptions{ + ExposeKvmDevice: true, + FileInjections: []config.FileInjection{}, + }) if code := e.Prepare(); code != 0 { panic("Prapare failed") @@ -77,7 +81,7 @@ func Test__DockerComposeExecutor(t *testing.T) { api.EnvVar{Name: "A", Value: "Zm9vCg=="}, } - e.ExportEnvVars(envVars) + e.ExportEnvVars(envVars, []config.HostEnvVar{}) e.RunCommand("echo $A", false, "") files := []api.File{ diff --git a/pkg/executors/docker_compose_file.go b/pkg/executors/docker_compose_file.go index 819ae565..b762e939 100644 --- a/pkg/executors/docker_compose_file.go +++ b/pkg/executors/docker_compose_file.go @@ -5,14 +5,21 @@ import ( "fmt" api "github.com/semaphoreci/agent/pkg/api" + "github.com/semaphoreci/agent/pkg/config" ) type DockerComposeFile struct { - configuration api.Compose + configuration api.Compose + exposeKvmDevice bool + fileInjections []config.FileInjection } -func ConstructDockerComposeFile(conf api.Compose) string { - f := DockerComposeFile{configuration: conf} +func ConstructDockerComposeFile(conf api.Compose, exposeKvmDevice bool, fileInjections []config.FileInjection) string { + f := DockerComposeFile{ + configuration: conf, + exposeKvmDevice: exposeKvmDevice, + fileInjections: fileInjections, + } return f.Construct() } @@ -41,8 +48,11 @@ func (f *DockerComposeFile) Service(container api.Container) string { result := "" result += fmt.Sprintf(" %s:\n", container.Name) result += fmt.Sprintf(" image: %s\n", container.Image) - result += " devices:\n" - result += " - \"/dev/kvm:/dev/kvm\"\n" + + if f.exposeKvmDevice { + result += " devices:\n" + result += " - \"/dev/kvm:/dev/kvm\"\n" + } if container.Command != "" { result += fmt.Sprintf(" command: %s\n", container.Command) @@ -80,5 +90,12 @@ func (f *DockerComposeFile) ServiceWithLinks(c api.Container, links []api.Contai } } + if len(f.fileInjections) > 0 { + result += " volumes:\n" + for _, fileInjection := range f.fileInjections { + result += fmt.Sprintf(" - %s:%s\n", fileInjection.HostPath, fileInjection.Destination) + } + } + return result } diff --git a/pkg/executors/docker_compose_file_test.go b/pkg/executors/docker_compose_file_test.go index a7354207..75811bc1 100644 --- a/pkg/executors/docker_compose_file_test.go +++ b/pkg/executors/docker_compose_file_test.go @@ -5,6 +5,7 @@ import ( "testing" api "github.com/semaphoreci/agent/pkg/api" + "github.com/semaphoreci/agent/pkg/config" assert "github.com/stretchr/testify/assert" ) @@ -58,6 +59,6 @@ services: ` - compose := ConstructDockerComposeFile(conf) + compose := ConstructDockerComposeFile(conf, true, []config.FileInjection{}) assert.Equal(t, expected, compose) } diff --git a/pkg/executors/executor.go b/pkg/executors/executor.go index 8621a453..ef3b5009 100644 --- a/pkg/executors/executor.go +++ b/pkg/executors/executor.go @@ -1,16 +1,14 @@ package executors import ( - "fmt" - api "github.com/semaphoreci/agent/pkg/api" - eventlogger "github.com/semaphoreci/agent/pkg/eventlogger" + "github.com/semaphoreci/agent/pkg/config" ) type Executor interface { Prepare() int Start() int - ExportEnvVars([]api.EnvVar) int + ExportEnvVars([]api.EnvVar, []config.HostEnvVar) int InjectFiles([]api.File) int RunCommand(string, bool, string) int Stop() int @@ -19,14 +17,3 @@ type Executor interface { const ExecutorTypeShell = "shell" const ExecutorTypeDockerCompose = "dockercompose" - -func CreateExecutor(request *api.JobRequest, logger *eventlogger.Logger) (Executor, error) { - switch request.Executor { - case ExecutorTypeShell: - return NewShellExecutor(request, logger), nil - case ExecutorTypeDockerCompose: - return NewDockerComposeExecutor(request, logger), nil - default: - return nil, fmt.Errorf("Uknown executor type") - } -} diff --git a/pkg/executors/shell_executor.go b/pkg/executors/shell_executor.go index 02497f30..5204ba1c 100644 --- a/pkg/executors/shell_executor.go +++ b/pkg/executors/shell_executor.go @@ -3,15 +3,16 @@ package executors import ( "fmt" "io/ioutil" - "log" "os/exec" "path" "strings" "time" api "github.com/semaphoreci/agent/pkg/api" + "github.com/semaphoreci/agent/pkg/config" eventlogger "github.com/semaphoreci/agent/pkg/eventlogger" shell "github.com/semaphoreci/agent/pkg/shell" + log "github.com/sirupsen/logrus" ) type ShellExecutor struct { @@ -40,7 +41,7 @@ func (e *ShellExecutor) setUpSSHJumpPoint() int { err := InjectEntriesToAuthorizedKeys(e.jobRequest.SSHPublicKeys) if err != nil { - log.Printf("Failed to inject authorized keys: %+v", err) + log.Errorf("Failed to inject authorized keys: %+v", err) return 1 } @@ -56,7 +57,7 @@ func (e *ShellExecutor) setUpSSHJumpPoint() int { err = SetUpSSHJumpPoint(script) if err != nil { - log.Printf("Failed to set up SSH jump point: %+v", err) + log.Errorf("Failed to set up SSH jump point: %+v", err) return 1 } @@ -68,7 +69,7 @@ func (e *ShellExecutor) Start() int { shell, err := shell.NewShell(cmd, e.tmpDirectory) if err != nil { - log.Println(shell) + log.Debug(shell) return 1 } @@ -76,16 +77,16 @@ func (e *ShellExecutor) Start() int { err = e.Shell.Start() if err != nil { - log.Println(err) + log.Error(err) return 1 } return 0 } -func (e *ShellExecutor) ExportEnvVars(envVars []api.EnvVar) int { +func (e *ShellExecutor) ExportEnvVars(envVars []api.EnvVar, hostEnvVars []config.HostEnvVar) int { commandStartedAt := int(time.Now().Unix()) - directive := fmt.Sprintf("Exporting environment variables") + directive := "Exporting environment variables" exitCode := 0 e.Logger.LogCommandStarted(directive) @@ -111,6 +112,11 @@ func (e *ShellExecutor) ExportEnvVars(envVars []api.EnvVar) int { envFile += fmt.Sprintf("export %s=%s\n", env.Name, ShellQuote(string(value))) } + for _, env := range hostEnvVars { + e.Logger.LogCommandOutput(fmt.Sprintf("Exporting %s\n", env.Name)) + envFile += fmt.Sprintf("export %s=%s\n", env.Name, ShellQuote(env.Value)) + } + err := ioutil.WriteFile("/tmp/.env", []byte(envFile), 0644) if err != nil { @@ -132,7 +138,7 @@ func (e *ShellExecutor) ExportEnvVars(envVars []api.EnvVar) int { } func (e *ShellExecutor) InjectFiles(files []api.File) int { - directive := fmt.Sprintf("Injecting Files") + directive := "Injecting Files" commandStartedAt := int(time.Now().Unix()) exitCode := 0 @@ -231,14 +237,14 @@ func (e *ShellExecutor) RunCommand(command string, silent bool, alias string) in } func (e *ShellExecutor) Stop() int { - log.Println("Starting the process killing procedure") + log.Debug("Starting the process killing procedure") err := e.Shell.Close() if err != nil { - fmt.Println(err) + log.Error(err) } - log.Printf("Process killing finished without errors") + log.Debug("Process killing finished without errors") return 0 } diff --git a/pkg/executors/shell_executor_test.go b/pkg/executors/shell_executor_test.go index 483c9e4c..aca1ec82 100644 --- a/pkg/executors/shell_executor_test.go +++ b/pkg/executors/shell_executor_test.go @@ -6,6 +6,7 @@ import ( "time" api "github.com/semaphoreci/agent/pkg/api" + "github.com/semaphoreci/agent/pkg/config" eventlogger "github.com/semaphoreci/agent/pkg/eventlogger" testsupport "github.com/semaphoreci/agent/test/support" assert "github.com/stretchr/testify/assert" @@ -40,7 +41,7 @@ func Test__ShellExecutor(t *testing.T) { api.EnvVar{Name: "A", Value: "Zm9vCg=="}, } - e.ExportEnvVars(envVars) + e.ExportEnvVars(envVars, []config.HostEnvVar{}) e.RunCommand("echo $A", false, "") files := []api.File{ diff --git a/pkg/httputils/httputils.go b/pkg/httputils/httputils.go new file mode 100644 index 00000000..fbf36a25 --- /dev/null +++ b/pkg/httputils/httputils.go @@ -0,0 +1,5 @@ +package httputils + +func IsSuccessfulCode(code int) bool { + return code >= 200 && code < 300 +} diff --git a/pkg/jobs/job.go b/pkg/jobs/job.go index 4a199c10..7af5188e 100644 --- a/pkg/jobs/job.go +++ b/pkg/jobs/job.go @@ -3,19 +3,24 @@ package jobs import ( "bytes" "fmt" - "log" + "net/http" "time" api "github.com/semaphoreci/agent/pkg/api" + "github.com/semaphoreci/agent/pkg/config" eventlogger "github.com/semaphoreci/agent/pkg/eventlogger" executors "github.com/semaphoreci/agent/pkg/executors" - pester "github.com/sethgrid/pester" + httputils "github.com/semaphoreci/agent/pkg/httputils" + "github.com/semaphoreci/agent/pkg/retry" + log "github.com/sirupsen/logrus" ) -const JOB_PASSED = "passed" -const JOB_FAILED = "failed" +const JobPassed = "passed" +const JobFailed = "failed" +const JobStopped = "stopped" type Job struct { + Client *http.Client Request *api.JobRequest Logger *eventlogger.Logger @@ -23,41 +28,53 @@ type Job struct { JobLogArchived bool Stopped bool + Finished bool +} + +type JobOptions struct { + Request *api.JobRequest + Client *http.Client + ExposeKvmDevice bool + FileInjections []config.FileInjection + FailOnMissingFiles bool +} - // - // The Teardown phase can be entered either after: - // - a regular job execution ends - // - from the Job.Stop procedure - // - // With this lock, we are making sure that only one Teardown is - // executed. This solves the race condition where both the job finishes - // and the job stops at the same time. - // - TeardownLock Lock +func NewJob(request *api.JobRequest, client *http.Client) (*Job, error) { + return NewJobWithOptions(&JobOptions{ + Request: request, + Client: client, + ExposeKvmDevice: true, + FileInjections: []config.FileInjection{}, + FailOnMissingFiles: false, + }) } -func NewJob(request *api.JobRequest) (*Job, error) { - log.Printf("Constructing an executor for the job") +func NewJobWithOptions(options *JobOptions) (*Job, error) { + if options.Request.Executor == "" { + log.Infof("No executor specified - using %s executor", executors.ExecutorTypeShell) + options.Request.Executor = executors.ExecutorTypeShell + } - if request.Executor == "" { - request.Executor = executors.ExecutorTypeShell + if options.Request.Logger.Method == "" { + log.Infof("No logger method specified - using %s logger method", eventlogger.LoggerMethodPull) + options.Request.Logger.Method = eventlogger.LoggerMethodPull } - logger, err := eventlogger.Default() + logger, err := eventlogger.CreateLogger(options.Request) if err != nil { return nil, err } - executor, err := executors.CreateExecutor(request, logger) + executor, err := CreateExecutor(options.Request, logger, *options) if err != nil { return nil, err } - log.Printf("Job Request %+v\n", request) - log.Printf("Constructed job") + log.Debugf("Job Request %+v", options.Request) return &Job{ - Request: request, + Client: options.Client, + Request: options.Request, Executor: executor, JobLogArchived: false, Stopped: false, @@ -65,10 +82,43 @@ func NewJob(request *api.JobRequest) (*Job, error) { }, nil } +func CreateExecutor(request *api.JobRequest, logger *eventlogger.Logger, jobOptions JobOptions) (executors.Executor, error) { + switch request.Executor { + case executors.ExecutorTypeShell: + return executors.NewShellExecutor(request, logger), nil + case executors.ExecutorTypeDockerCompose: + executorOptions := executors.DockerComposeExecutorOptions{ + ExposeKvmDevice: jobOptions.ExposeKvmDevice, + FileInjections: jobOptions.FileInjections, + FailOnMissingFiles: jobOptions.FailOnMissingFiles, + } + + return executors.NewDockerComposeExecutor(request, logger, executorOptions), nil + default: + return nil, fmt.Errorf("unknown executor type") + } +} + +type RunOptions struct { + EnvVars []config.HostEnvVar + FileInjections []config.FileInjection + OnSuccessfulTeardown func() + OnFailedTeardown func() +} + func (job *Job) Run() { - log.Printf("Job Started") + job.RunWithOptions(RunOptions{ + EnvVars: []config.HostEnvVar{}, + FileInjections: []config.FileInjection{}, + OnSuccessfulTeardown: nil, + OnFailedTeardown: nil, + }) +} + +func (job *Job) RunWithOptions(options RunOptions) { + log.Infof("Running job %s", job.Request.ID) executorRunning := false - result := JOB_FAILED + result := JobFailed job.Logger.LogJobStarted() @@ -76,74 +126,103 @@ func (job *Job) Run() { if exitCode == 0 { executorRunning = true } else { - log.Printf("Executor failed to boot up") + log.Error("Executor failed to boot up") } if executorRunning { - result = job.RunRegularCommands() - - log.Printf("Regular Commands Finished. Result: %s", result) - - log.Printf("Exporting job result") - + result = job.RunRegularCommands(options.EnvVars) + log.Debug("Exporting job result") job.RunCommandsUntilFirstFailure([]api.Command{ - api.Command{ + { Directive: fmt.Sprintf("export SEMAPHORE_JOB_RESULT=%s", result), }, }) - log.Printf("Starting Epilogue Always Commands.") - job.RunCommandsUntilFirstFailure(job.Request.EpilogueAlwaysCommands) - - if result == JOB_PASSED { - log.Printf("Starting Epilogue On Pass Commands.") - job.RunCommandsUntilFirstFailure(job.Request.EpilogueOnPassCommands) - } else { - log.Printf("Starting Epilogue On Fail Commands.") - job.RunCommandsUntilFirstFailure(job.Request.EpilogueOnFailCommands) + if result != JobStopped { + job.handleEpilogues(result) } } - job.Teardown(result) + err := job.Teardown(result) + if err != nil { + callFuncIfNotNull(options.OnFailedTeardown) + } else { + callFuncIfNotNull(options.OnSuccessfulTeardown) + } + + job.Finished = true + + // the executor is already stopped when the job is stopped, so there's no need to stop it again + if !job.Stopped { + job.Executor.Stop() + } } func (job *Job) PrepareEnvironment() int { exitCode := job.Executor.Prepare() if exitCode != 0 { - log.Printf("Failed to prepare executor") + log.Error("Failed to prepare executor") return exitCode } exitCode = job.Executor.Start() if exitCode != 0 { - log.Printf("Failed to start executor") + log.Error("Failed to start executor") return exitCode } return 0 } -func (job *Job) RunRegularCommands() string { - exitCode := job.Executor.ExportEnvVars(job.Request.EnvVars) +func (job *Job) RunRegularCommands(hostEnvVars []config.HostEnvVar) string { + exitCode := job.Executor.ExportEnvVars(job.Request.EnvVars, hostEnvVars) if exitCode != 0 { - log.Printf("Failed to export env vars") + log.Error("Failed to export env vars") - return JOB_FAILED + return JobFailed } exitCode = job.Executor.InjectFiles(job.Request.Files) if exitCode != 0 { - log.Printf("Failed to inject files") + log.Error("Failed to inject files") - return JOB_FAILED + return JobFailed } exitCode = job.RunCommandsUntilFirstFailure(job.Request.Commands) - if exitCode == 0 { - return JOB_PASSED + if job.Stopped { + log.Info("Regular commands were stopped") + return JobStopped + } else if exitCode == 0 { + log.Info("Regular commands finished successfully") + return JobPassed } else { - return JOB_FAILED + log.Info("Regular commands finished with failure") + return JobFailed + } +} + +func (job *Job) handleEpilogues(result string) { + job.executeIfNotStopped(func() { + log.Info("Starting epilogue always commands") + job.RunCommandsUntilFirstFailure(job.Request.EpilogueAlwaysCommands) + }) + + job.executeIfNotStopped(func() { + if result == JobPassed { + log.Info("Starting epilogue on pass commands") + job.RunCommandsUntilFirstFailure(job.Request.EpilogueOnPassCommands) + } else { + log.Info("Starting epilogue on fail commands") + job.RunCommandsUntilFirstFailure(job.Request.EpilogueOnFailCommands) + } + }) +} + +func (job *Job) executeIfNotStopped(callback func()) { + if !job.Stopped && callback != nil { + callback() } } @@ -166,76 +245,97 @@ func (job *Job) RunCommandsUntilFirstFailure(commands []api.Command) int { return lastExitCode } -func (job *Job) Teardown(result string) { - if !job.TeardownLock.TryLock() { - log.Printf("[warning] Duplicate attempts to enter the Teardown phase") - return +func (job *Job) Teardown(result string) error { + // if job was stopped during the epilogues, result should be stopped + if job.Stopped { + result = JobStopped } - log.Printf("Job Teardown Started") + err := job.SendFinishedCallback(result) + if err != nil { + log.Errorf("Could not send finished callback: %v", err) + return err + } - log.Printf("Sending finished callback.") - job.SendFinishedCallback(result) job.Logger.LogJobFinished(result) - log.Printf("Waiting for archivator") + if job.Request.Logger.Method == eventlogger.LoggerMethodPull { + log.Debug("Waiting for archivator") - for { - if job.JobLogArchived { - break - } else { - time.Sleep(1000 * time.Millisecond) + for { + if job.JobLogArchived { + break + } else { + time.Sleep(1000 * time.Millisecond) + } } - } - job.SendTeardownFinishedCallback() + log.Debug("Archivator finished") + } - log.Printf("Archivator finished") + err = job.Logger.Close() + if err != nil { + log.Errorf("Error closing logger: %+v", err) + } - err := job.Logger.Close() + err = job.SendTeardownFinishedCallback() if err != nil { - log.Printf("Event Logger error %+v", err) + log.Errorf("Could not send teardown finished callback: %v", err) + return err } - log.Printf("Job Teardown Finished") + log.Info("Job teardown finished") + return nil } -func (j *Job) Stop() { - log.Printf("Stopping job") +func (job *Job) Stop() { + log.Info("Stopping job") - j.Stopped = true + job.Stopped = true - log.Printf("Invoking process stopping") + log.Debug("Invoking process stopping") PreventPanicPropagation(func() { - j.Executor.Stop() + job.Executor.Stop() }) - - log.Printf("Process stopping finished. Entering the Teardown phase.") - - j.Teardown("stopped") } func (job *Job) SendFinishedCallback(result string) error { payload := fmt.Sprintf(`{"result": "%s"}`, result) - - return job.SendCallback(job.Request.Callbacks.Finished, payload) + log.Infof("Sending finished callback: %+v", payload) + return retry.RetryWithConstantWait("Send finished callback", 60, time.Second, func() error { + return job.SendCallback(job.Request.Callbacks.Finished, payload) + }) } func (job *Job) SendTeardownFinishedCallback() error { - return job.SendCallback(job.Request.Callbacks.TeardownFinished, "{}") + log.Info("Sending teardown finished callback") + return retry.RetryWithConstantWait("Send teardown finished callback", 60, time.Second, func() error { + return job.SendCallback(job.Request.Callbacks.TeardownFinished, "{}") + }) } func (job *Job) SendCallback(url string, payload string) error { - log.Printf("Sending callback: %s with %+v\n", url, payload) + log.Debugf("Sending callback to %s: %+v", url, payload) + request, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(payload))) + if err != nil { + return err + } - client := pester.New() - client.MaxRetries = 100 - client.KeepLog = true + response, err := job.Client.Do(request) + if err != nil { + return err + } - resp, err := client.Post(url, "application/json", bytes.NewBuffer([]byte(payload))) + if !httputils.IsSuccessfulCode(response.StatusCode) { + return fmt.Errorf("callback to %s got HTTP %d", url, response.StatusCode) + } - log.Printf("%+v\n", resp) + return nil +} - return err +func callFuncIfNotNull(function func()) { + if function != nil { + function() + } } diff --git a/pkg/listener/job_processor.go b/pkg/listener/job_processor.go new file mode 100644 index 00000000..2c242221 --- /dev/null +++ b/pkg/listener/job_processor.go @@ -0,0 +1,248 @@ +package listener + +import ( + "net/http" + "os" + "os/exec" + "os/signal" + "syscall" + "time" + + "github.com/semaphoreci/agent/pkg/api" + "github.com/semaphoreci/agent/pkg/config" + jobs "github.com/semaphoreci/agent/pkg/jobs" + selfhostedapi "github.com/semaphoreci/agent/pkg/listener/selfhostedapi" + "github.com/semaphoreci/agent/pkg/retry" + log "github.com/sirupsen/logrus" +) + +func StartJobProcessor(httpClient *http.Client, apiClient *selfhostedapi.API, config Config) (*JobProcessor, error) { + p := &JobProcessor{ + HTTPClient: httpClient, + APIClient: apiClient, + LastSuccessfulSync: time.Now(), + State: selfhostedapi.AgentStateWaitingForJobs, + SyncInterval: 5 * time.Second, + DisconnectRetryAttempts: 100, + ShutdownHookPath: config.ShutdownHookPath, + DisconnectAfterJob: config.DisconnectAfterJob, + EnvVars: config.EnvVars, + FileInjections: config.FileInjections, + FailOnMissingFiles: config.FailOnMissingFiles, + } + + go p.Start() + + p.SetupInteruptHandler() + + return p, nil +} + +type JobProcessor struct { + HTTPClient *http.Client + APIClient *selfhostedapi.API + State selfhostedapi.AgentState + CurrentJobID string + CurrentJob *jobs.Job + SyncInterval time.Duration + LastSyncErrorAt *time.Time + LastSuccessfulSync time.Time + DisconnectRetryAttempts int + ShutdownHookPath string + StopSync bool + DisconnectAfterJob bool + EnvVars []config.HostEnvVar + FileInjections []config.FileInjection + FailOnMissingFiles bool +} + +func (p *JobProcessor) Start() { + go p.SyncLoop() +} + +func (p *JobProcessor) SyncLoop() { + for { + if p.StopSync { + break + } + + p.Sync() + time.Sleep(p.SyncInterval) + } +} + +func (p *JobProcessor) Sync() { + request := &selfhostedapi.SyncRequest{ + State: p.State, + JobID: p.CurrentJobID, + } + + response, err := p.APIClient.Sync(request) + if err != nil { + p.HandleSyncError(err) + return + } + + p.LastSuccessfulSync = time.Now() + p.ProcessSyncResponse(response) +} + +func (p *JobProcessor) HandleSyncError(err error) { + log.Errorf("[SYNC ERR] Failed to sync with API: %v", err) + + now := time.Now() + + p.LastSyncErrorAt = &now + + if time.Now().Add(-10 * time.Minute).After(p.LastSuccessfulSync) { + p.Shutdown("Unable to sync with Semaphore for over 10 minutes.", 1) + } +} + +func (p *JobProcessor) ProcessSyncResponse(response *selfhostedapi.SyncResponse) { + switch response.Action { + case selfhostedapi.AgentActionContinue: + // continue what I'm doing, no action needed + return + + case selfhostedapi.AgentActionRunJob: + go p.RunJob(response.JobID) + return + + case selfhostedapi.AgentActionStopJob: + go p.StopJob(response.JobID) + return + + case selfhostedapi.AgentActionShutdown: + p.Shutdown("Agent Shutdown requested by Semaphore", 0) + + case selfhostedapi.AgentActionWaitForJobs: + p.WaitForJobs() + } +} + +func (p *JobProcessor) RunJob(jobID string) { + p.State = selfhostedapi.AgentStateStartingJob + p.CurrentJobID = jobID + + jobRequest, err := p.getJobWithRetries(p.CurrentJobID) + if err != nil { + log.Errorf("Could not get job %s: %v", jobID, err) + p.State = selfhostedapi.AgentStateFailedToFetchJob + return + } + + job, err := jobs.NewJobWithOptions(&jobs.JobOptions{ + Request: jobRequest, + Client: p.HTTPClient, + ExposeKvmDevice: false, + FileInjections: p.FileInjections, + FailOnMissingFiles: p.FailOnMissingFiles, + }) + + if err != nil { + log.Errorf("Could not construct job %s: %v", jobID, err) + p.State = selfhostedapi.AgentStateFailedToConstructJob + return + } + + p.State = selfhostedapi.AgentStateRunningJob + p.CurrentJobID = jobID + p.CurrentJob = job + + go job.RunWithOptions(jobs.RunOptions{ + EnvVars: p.EnvVars, + FileInjections: p.FileInjections, + OnSuccessfulTeardown: p.JobFinished, + OnFailedTeardown: func() { + if p.DisconnectAfterJob { + p.Shutdown("Job finished with error", 1) + } else { + p.State = selfhostedapi.AgentStateFailedToSendCallback + } + }, + }) +} + +func (p *JobProcessor) getJobWithRetries(jobID string) (*api.JobRequest, error) { + var jobRequest *api.JobRequest + err := retry.RetryWithConstantWait("Get job", 10, 3*time.Second, func() error { + job, err := p.APIClient.GetJob(jobID) + if err != nil { + return err + } + + jobRequest = job + return nil + }) + + return jobRequest, err +} + +func (p *JobProcessor) StopJob(jobID string) { + p.CurrentJobID = jobID + p.State = selfhostedapi.AgentStateStoppingJob + + p.CurrentJob.Stop() +} + +func (p *JobProcessor) JobFinished() { + if p.DisconnectAfterJob { + p.Shutdown("Job finished", 0) + } else { + p.State = selfhostedapi.AgentStateFinishedJob + } +} + +func (p *JobProcessor) WaitForJobs() { + p.CurrentJobID = "" + p.CurrentJob = nil + p.State = selfhostedapi.AgentStateWaitingForJobs +} + +func (p *JobProcessor) SetupInteruptHandler() { + c := make(chan os.Signal) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + <-c + p.Shutdown("Ctrl+C pressed in Terminal", 0) + }() +} + +func (p *JobProcessor) disconnect() { + p.StopSync = true + log.Info("Disconnecting the Agent from Semaphore") + + err := retry.RetryWithConstantWait("Disconnect", p.DisconnectRetryAttempts, time.Second, func() error { + _, err := p.APIClient.Disconnect() + return err + }) + + if err != nil { + log.Errorf("Failed to disconnect from Semaphore even after %d tries: %v", p.DisconnectRetryAttempts, err) + } else { + log.Info("Disconnected.") + } +} + +func (p *JobProcessor) Shutdown(reason string, code int) { + p.disconnect() + p.executeShutdownHook() + log.Info(reason) + log.Info("Shutting down... Good bye!") + os.Exit(code) +} + +func (p *JobProcessor) executeShutdownHook() { + if p.ShutdownHookPath != "" { + log.Infof("Executing shutdown hook from %s", p.ShutdownHookPath) + cmd := exec.Command("bash", p.ShutdownHookPath) + output, err := cmd.Output() + if err != nil { + log.Errorf("Error executing shutdown hook: %v", err) + log.Errorf("Output: %s", string(output)) + } else { + log.Infof("Output: %s", string(output)) + } + } +} diff --git a/pkg/listener/listener.go b/pkg/listener/listener.go new file mode 100644 index 00000000..215089a7 --- /dev/null +++ b/pkg/listener/listener.go @@ -0,0 +1,115 @@ +package listener + +import ( + "fmt" + "io" + "math/rand" + "net/http" + "os" + "time" + + "github.com/semaphoreci/agent/pkg/config" + selfhostedapi "github.com/semaphoreci/agent/pkg/listener/selfhostedapi" + osinfo "github.com/semaphoreci/agent/pkg/osinfo" + "github.com/semaphoreci/agent/pkg/retry" + log "github.com/sirupsen/logrus" +) + +type Listener struct { + JobProcessor *JobProcessor + Config Config + Client *selfhostedapi.API +} + +type Config struct { + Endpoint string + RegisterRetryLimit int + Token string + Scheme string + ShutdownHookPath string + DisconnectAfterJob bool + EnvVars []config.HostEnvVar + FileInjections []config.FileInjection + FailOnMissingFiles bool + AgentVersion string +} + +func Start(httpClient *http.Client, config Config, logger io.Writer) (*Listener, error) { + listener := &Listener{ + Config: config, + Client: selfhostedapi.New(httpClient, config.Scheme, config.Endpoint, config.Token), + } + + listener.DisplayHelloMessage() + + log.Info("Starting Agent") + log.Info("Registering Agent") + err := listener.Register() + if err != nil { + return listener, err + } + + log.Info("Starting to poll for jobs") + jobProcessor, err := StartJobProcessor(httpClient, listener.Client, listener.Config) + if err != nil { + return listener, err + } + + listener.JobProcessor = jobProcessor + + return listener, nil +} + +func (l *Listener) DisplayHelloMessage() { + fmt.Println(" ") + fmt.Println(" 00000000000 ") + fmt.Println(" 0000000000000000 ") + fmt.Println(" 00000000000000000000 ") + fmt.Println(" 00000000000 0000000000 ") + fmt.Println(" 11 00000000 11 000000000 ") + fmt.Println(" 111111 000000 1111111 000000 ") + fmt.Println("111111111 00 111111111 00 ") + fmt.Println(" 111111111 1111111111 ") + fmt.Println(" 1111111111111111111 ") + fmt.Println(" 111111111111111 ") + fmt.Println(" 111111111 ") + fmt.Println(" ") +} + +const nameLetters = "abcdefghijklmnopqrstuvwxyz123456789" +const nameLength = 20 + +func (l *Listener) Name() string { + b := make([]byte, nameLength) + for i := range b { + b[i] = nameLetters[rand.Intn(len(nameLetters))] + } + return string(b) +} + +func (l *Listener) Register() error { + req := &selfhostedapi.RegisterRequest{ + Version: l.Config.AgentVersion, + Name: l.Name(), + PID: os.Getpid(), + OS: osinfo.Name(), + Arch: osinfo.Arch(), + Hostname: osinfo.Hostname(), + } + + err := retry.RetryWithConstantWait("Register", l.Config.RegisterRetryLimit, time.Second, func() error { + resp, err := l.Client.Register(req) + if err != nil { + return err + } + + l.Client.SetAccessToken(resp.Token) + return nil + }) + + if err != nil { + return fmt.Errorf("failed to register agent: %v", err) + } + + return nil +} diff --git a/pkg/listener/selfhostedapi/api.go b/pkg/listener/selfhostedapi/api.go new file mode 100644 index 00000000..c4456585 --- /dev/null +++ b/pkg/listener/selfhostedapi/api.go @@ -0,0 +1,37 @@ +package selfhostedapi + +import ( + "fmt" + "net/http" +) + +type API struct { + Endpoint string + Scheme string + + RegisterToken string + AccessToken string + + client *http.Client +} + +func New(httpClient *http.Client, scheme string, endpoint string, token string) *API { + return &API{ + Endpoint: endpoint, + RegisterToken: token, + Scheme: scheme, + client: httpClient, + } +} + +func (a *API) authorize(req *http.Request, token string) { + req.Header.Set("Authorization", "Token "+token) +} + +func (a *API) SetAccessToken(token string) { + a.AccessToken = token +} + +func (a *API) BasePath() string { + return fmt.Sprintf("%s://%s/api/v1/self_hosted_agents", a.Scheme, a.Endpoint) +} diff --git a/pkg/listener/selfhostedapi/disconnect.go b/pkg/listener/selfhostedapi/disconnect.go new file mode 100644 index 00000000..06b19c08 --- /dev/null +++ b/pkg/listener/selfhostedapi/disconnect.go @@ -0,0 +1,37 @@ +package selfhostedapi + +import ( + "fmt" + "io/ioutil" + "net/http" +) + +func (a *API) DisconnectPath() string { + return a.BasePath() + "/disconnect" +} + +func (a *API) Disconnect() (string, error) { + r, err := http.NewRequest("POST", a.DisconnectPath(), nil) + if err != nil { + return "", err + } + + a.authorize(r, a.AccessToken) + + resp, err := a.client.Do(r) + if err != nil { + return "", err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + if resp.StatusCode != 200 { + return "", fmt.Errorf("error while disconnecting, status: %d, body: %s", resp.StatusCode, string(body)) + } + + return string(body), nil +} diff --git a/pkg/listener/selfhostedapi/get_job.go b/pkg/listener/selfhostedapi/get_job.go new file mode 100644 index 00000000..94954918 --- /dev/null +++ b/pkg/listener/selfhostedapi/get_job.go @@ -0,0 +1,45 @@ +package selfhostedapi + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "github.com/semaphoreci/agent/pkg/api" +) + +func (a *API) GetJobPath(jobID string) string { + return a.BasePath() + fmt.Sprintf("/jobs/%s", jobID) +} + +func (a *API) GetJob(jobID string) (*api.JobRequest, error) { + r, err := http.NewRequest("GET", a.GetJobPath(jobID), nil) + if err != nil { + return nil, err + } + + a.authorize(r, a.AccessToken) + + resp, err := a.client.Do(r) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to describe job, got HTTP %d", resp.StatusCode) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + response := &api.JobRequest{} + if err := json.Unmarshal(body, response); err != nil { + return nil, err + } + + return response, nil +} diff --git a/pkg/listener/selfhostedapi/logs.go b/pkg/listener/selfhostedapi/logs.go new file mode 100644 index 00000000..033a7df9 --- /dev/null +++ b/pkg/listener/selfhostedapi/logs.go @@ -0,0 +1,31 @@ +package selfhostedapi + +import ( + "bytes" + "fmt" + "net/http" +) + +func (a *API) LogsPath(jobID string) string { + return a.BasePath() + fmt.Sprintf("/jobs/%s/logs", jobID) +} + +func (a *API) Logs(jobID string, batch *bytes.Buffer) error { + r, err := http.NewRequest("POST", a.LogsPath(jobID), batch) + if err != nil { + return err + } + + a.authorize(r, a.AccessToken) + + resp, err := a.client.Do(r) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to submit logs, got HTTP %d", resp.StatusCode) + } + + return nil +} diff --git a/pkg/listener/selfhostedapi/register.go b/pkg/listener/selfhostedapi/register.go new file mode 100644 index 00000000..b79aba0b --- /dev/null +++ b/pkg/listener/selfhostedapi/register.go @@ -0,0 +1,67 @@ +package selfhostedapi + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + httputils "github.com/semaphoreci/agent/pkg/httputils" + log "github.com/sirupsen/logrus" +) + +type RegisterRequest struct { + Name string `json:"name"` + Version string `json:"version"` + PID int `json:"pid"` + OS string `json:"os"` + Arch string `json:"arch"` + Hostname string `json:"hostname"` +} + +type RegisterResponse struct { + Name string `json:"name"` + Token string `json:"token"` +} + +func (a *API) RegisterPath() string { + return a.BasePath() + "/register" +} + +func (a *API) Register(req *RegisterRequest) (*RegisterResponse, error) { + b, err := json.Marshal(req) + if err != nil { + return nil, err + } + + r, err := http.NewRequest("POST", a.RegisterPath(), bytes.NewBuffer(b)) + if err != nil { + return nil, err + } + + a.authorize(r, a.RegisterToken) + + resp, err := a.client.Do(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if !httputils.IsSuccessfulCode(resp.StatusCode) { + return nil, fmt.Errorf("register request to %s got HTTP %d", a.RegisterPath(), resp.StatusCode) + } + + response := &RegisterResponse{} + if err := json.Unmarshal(body, response); err != nil { + log.Debug(string(body)) + return nil, err + } + + return response, nil +} diff --git a/pkg/listener/selfhostedapi/sync.go b/pkg/listener/selfhostedapi/sync.go new file mode 100644 index 00000000..868659fc --- /dev/null +++ b/pkg/listener/selfhostedapi/sync.go @@ -0,0 +1,83 @@ +package selfhostedapi + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + log "github.com/sirupsen/logrus" +) + +type AgentState string +type AgentAction string + +const AgentStateWaitingForJobs = "waiting-for-jobs" +const AgentStateStartingJob = "starting-job" +const AgentStateRunningJob = "running-job" +const AgentStateStoppingJob = "stopping-job" +const AgentStateFinishedJob = "finished-job" +const AgentStateFailedToFetchJob = "failed-to-fetch-job" +const AgentStateFailedToConstructJob = "failed-to-construct-job" +const AgentStateFailedToSendCallback = "failed-to-send-callback" + +const AgentActionWaitForJobs = "wait-for-jobs" +const AgentActionRunJob = "run-job" +const AgentActionStopJob = "stop-job" +const AgentActionShutdown = "shutdown" +const AgentActionContinue = "continue" + +type SyncRequest struct { + State AgentState `json:"state"` + JobID string `json:"job_id"` +} + +type SyncResponse struct { + Action AgentAction `json:"action"` + JobID string `json:"job_id"` +} + +func (a *API) SyncPath() string { + return a.BasePath() + "/sync" +} + +func (a *API) Sync(req *SyncRequest) (*SyncResponse, error) { + b, err := json.Marshal(req) + if err != nil { + return nil, err + } + + log.Infof("SYNC request (state: %s, job: %s)", req.State, req.JobID) + + r, err := http.NewRequest("POST", a.SyncPath(), bytes.NewBuffer(b)) + if err != nil { + return nil, err + } + + a.authorize(r, a.AccessToken) + + resp, err := a.client.Do(r) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to sync with upstream, got HTTP %d", resp.StatusCode) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + response := &SyncResponse{} + if err := json.Unmarshal(body, response); err != nil { + return nil, err + } + + log.Infof("SYNC response (action: %s, job: %s)", response.Action, response.JobID) + + return response, nil +} diff --git a/pkg/osinfo/osinfo.go b/pkg/osinfo/osinfo.go new file mode 100644 index 00000000..7a207e8a --- /dev/null +++ b/pkg/osinfo/osinfo.go @@ -0,0 +1,115 @@ +package osinfo + +import ( + "fmt" + "os" + "os/exec" + "runtime" + "strings" +) + +func Name() string { + switch runtime.GOOS { + case "linux": + return namelinux() + case "darwin": + return namemac() + default: + // TODO handle other OSes + return "" + } +} + +func Hostname() string { + hostname, err := os.Hostname() + if err != nil { + return "" + } + + return hostname +} + +func Arch() string { + switch runtime.GOARCH { + case "amd64": + return "x86_64" + + case "386": + return "x86" + + default: + return runtime.GOARCH + } +} + +func namemac() string { + o1, err := exec.Command("sw_vers", "-productName").Output() + if err != nil { + return "" + } + + o2, err := exec.Command("sw_vers", "-productVersion").Output() + if err != nil { + return "" + } + + o3, err := exec.Command("sw_vers", "-buildVersion").Output() + if err != nil { + return "" + } + + productName := strings.TrimSpace(string(o1)) + productVersion := strings.TrimSpace(string(o2)) + buildVersion := strings.TrimSpace(string(o3)) + + return fmt.Sprintf("%s %s %s", productName, productVersion, buildVersion) +} + +func namelinux() string { + out, err := exec.Command("cat", "/etc/os-release", "/etc/lsb-release").Output() + if err != nil { + return "" + } + + // The format of the file looks like this (example) + // + // NAME="Ubuntu" + // VERSION="14.04.5 LTS, Trusty Tahr" + // ID=ubuntu + // ID_LIKE=debian + // PRETTY_NAME="Ubuntu 14.04.5 LTS" + // VERSION_ID="14.04" + // HOME_URL="http://www.ubuntu.com/" + // SUPPORT_URL="http://help.ubuntu.com/" + // BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/" + // + + lines := strings.Split(string(out), "\n") + + findValue := func(key string) (string, bool) { + for _, line := range lines { + if strings.HasPrefix(line, key+"=") { + name := strings.Split(line, key+"=")[1] + + if name[0] != '"' { + return name, true + } + + // if the value is wrapped in quotes, remove the quotes + return strings.Split(name, "\"")[1], true + } + } + + return "", false + } + + if name, ok := findValue("PRETTY_NAME"); ok { + return name + } + + if name, ok := findValue("NAME"); ok { + return name + } + + return "" +} diff --git a/pkg/osinfo/osinfo_test.go b/pkg/osinfo/osinfo_test.go new file mode 100644 index 00000000..fa97cdb2 --- /dev/null +++ b/pkg/osinfo/osinfo_test.go @@ -0,0 +1,17 @@ +package osinfo + +import ( + "testing" + + require "github.com/stretchr/testify/require" +) + +func Test__Name(t *testing.T) { + // TBH, it is hard to write a test for this that would work on + // all environments. + // The only test I can think of is that the returned string is not empty, + // and that it doesn't crash. + + name := Name() + require.NotEmpty(t, name) +} diff --git a/pkg/retry/retry.go b/pkg/retry/retry.go new file mode 100644 index 00000000..41fcb71d --- /dev/null +++ b/pkg/retry/retry.go @@ -0,0 +1,24 @@ +package retry + +import ( + "fmt" + "time" + + log "github.com/sirupsen/logrus" +) + +func RetryWithConstantWait(task string, maxAttempts int, wait time.Duration, f func() error) error { + for attempt := 1; ; attempt++ { + err := f() + if err == nil { + return nil + } + + if attempt >= maxAttempts { + return fmt.Errorf("[%s] failed after [%d] attempts - giving up: %v", task, attempt, err) + } + + log.Errorf("[%s] attempt [%d] failed with [%v] - retrying in %s", task, attempt, err, wait) + time.Sleep(wait) + } +} diff --git a/pkg/retry/retry_test.go b/pkg/retry/retry_test.go new file mode 100644 index 00000000..f53b6967 --- /dev/null +++ b/pkg/retry/retry_test.go @@ -0,0 +1,29 @@ +package retry + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func Test__NoRetriesIfFirstAttemptIsSuccessful(t *testing.T) { + attempts := 0 + err := RetryWithConstantWait("test", 5, 100*time.Millisecond, func() error { + attempts++ + return nil + }) + assert.Equal(t, attempts, 1) + assert.Nil(t, err) +} + +func Test__GivesUpAfterMaxRetries(t *testing.T) { + attempts := 0 + err := RetryWithConstantWait("test", 5, 100*time.Millisecond, func() error { + attempts++ + return errors.New("bad error") + }) + assert.Equal(t, attempts, 5) + assert.NotNil(t, err) +} diff --git a/pkg/server/server.go b/pkg/server/server.go index 48be188b..8931d690 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "io/ioutil" - "log" "net/http" "os" "strconv" @@ -14,8 +13,10 @@ import ( mux "github.com/gorilla/mux" api "github.com/semaphoreci/agent/pkg/api" + "github.com/semaphoreci/agent/pkg/config" eventlogger "github.com/semaphoreci/agent/pkg/eventlogger" jobs "github.com/semaphoreci/agent/pkg/jobs" + log "github.com/sirupsen/logrus" ) type Server struct { @@ -33,12 +34,14 @@ type Server struct { ActiveJob *jobs.Job router *mux.Router + + HTTPClient *http.Client } const ServerStateWaitingForJob = "waiting-for-job" const ServerStateJobReceived = "job-received" -func NewServer(host string, port int, tlsCertPath, tlsKeyPath, version string, logfile io.Writer, jwtSecret []byte) *Server { +func NewServer(host string, port int, tlsCertPath, tlsKeyPath, version string, logfile io.Writer, jwtSecret []byte, httpClient *http.Client) *Server { router := mux.NewRouter().StrictSlash(true) server := &Server{ @@ -51,6 +54,7 @@ func NewServer(host string, port int, tlsCertPath, tlsKeyPath, version string, l Logfile: logfile, router: router, Version: version, + HTTPClient: httpClient, } jwtMiddleware := CreateJwtMiddleware(jwtSecret) @@ -78,7 +82,7 @@ func NewServer(host string, port int, tlsCertPath, tlsKeyPath, version string, l func (s *Server) Serve() { address := fmt.Sprintf("%s:%d", s.Host, s.Port) - fmt.Printf("Agent %s listening on https://%s\n", s.Version, address) + log.Infof("Agent %s listening on https://%s\n", s.Version, address) loggedRouter := handlers.LoggingHandler(s.Logfile, s.router) @@ -118,15 +122,15 @@ func (s *Server) JobLogs(w http.ResponseWriter, r *http.Request) { logFile, ok := s.ActiveJob.Logger.Backend.(*eventlogger.FileBackend) if !ok { - log.Printf("Failed to stream job logs") + log.Error("Failed to stream job logs") http.Error(w, err.Error(), 500) fmt.Fprintf(w, `{"message": "%s"}`, "Failed to open logfile") } - err = logFile.Stream(startFromLine, w) + _, err = logFile.Stream(startFromLine, w) if err != nil { - log.Printf("Error while streaming logs") + log.Errorf("Error while streaming logs: %v", err) http.Error(w, err.Error(), 500) fmt.Fprintf(w, `{"message": "%s"}`, err) @@ -152,26 +156,25 @@ func (s *Server) AgentLogs(w http.ResponseWriter, r *http.Request) { } func (s *Server) Run(w http.ResponseWriter, r *http.Request) { - log.Printf("New job arrived. Agent version %s.", s.Version) + log.Infof("New job arrived. Agent version %s.", s.Version) - log.Printf("Reading content of the request") + log.Debug("Reading content of the request") body, err := ioutil.ReadAll(r.Body) defer r.Body.Close() if err != nil { - log.Printf("Failed to read the content of the job, returning 500") + log.Errorf("Failed to read the content of the job, returning 500: %v", err) http.Error(w, err.Error(), 500) return } - log.Printf("Parsing job request") + log.Debug("Parsing job request") request, err := api.NewRequestFromJSON(body) if err != nil { - log.Printf("Failed to parse job request, returning 422") - log.Printf("%+v", err) + log.Errorf("Failed to parse job request, returning 422: %v", err) - http.Error(w, err.Error(), 422) + http.Error(w, err.Error(), http.StatusUnprocessableEntity) fmt.Fprintf(w, `{"message": "%s"}`, err) return } @@ -181,36 +184,40 @@ func (s *Server) Run(w http.ResponseWriter, r *http.Request) { // idempotent call fmt.Fprint(w, `{"message": "ok"}`) return - } else { - log.Printf("A job is already running, returning 422") - - w.WriteHeader(422) - fmt.Fprintf(w, `{"message": "a job is already running"}`) - return } + + log.Warn("A job is already running, returning 422") + + w.WriteHeader(422) + fmt.Fprintf(w, `{"message": "a job is already running"}`) + return } - log.Printf("Creating new job") - job, err := jobs.NewJob(request) + log.Debug("Creating new job") + job, err := jobs.NewJobWithOptions(&jobs.JobOptions{ + Request: request, + Client: s.HTTPClient, + ExposeKvmDevice: true, + FileInjections: []config.FileInjection{}, + }) if err != nil { - log.Printf("Failed to create a new job, returning 500") + log.Errorf("Failed to create a new job, returning 500: %v", err) http.Error(w, err.Error(), 500) fmt.Fprintf(w, `{"message": "%s"}`, err) return } - log.Printf("Setting up Active Job context") + log.Debug("Setting up Active Job context") s.ActiveJob = job - log.Printf("Starting job execution") + log.Debug("Starting job execution") go s.ActiveJob.Run() - log.Printf("Setting state to '%s'", ServerStateJobReceived) + log.Debugf("Setting state to '%s'", ServerStateJobReceived) s.State = ServerStateJobReceived - log.Printf("Respongind with OK") fmt.Fprint(w, `{"message": "ok"}`) } @@ -219,8 +226,3 @@ func (s *Server) Stop(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) } - -func (s *Server) unsuported(w http.ResponseWriter) { - w.WriteHeader(400) - fmt.Fprintf(w, `{"message": "not supported"}`) -} diff --git a/pkg/shell/output_buffer.go b/pkg/shell/output_buffer.go index 98c1b064..7d297426 100644 --- a/pkg/shell/output_buffer.go +++ b/pkg/shell/output_buffer.go @@ -1,10 +1,11 @@ package shell import ( - "log" "strings" "time" "unicode/utf8" + + log "github.com/sirupsen/logrus" ) // @@ -60,7 +61,7 @@ func (b *OutputBuffer) Flush() (string, bool) { timeSinceLastAppend = time.Now().Sub(*b.lastAppend) } - log.Printf("Flushing. %d bytes in the buffer", len(b.bytes)) + log.Debugf("Flushing. %d bytes in the buffer", len(b.bytes)) // We don't want to flush too often. // diff --git a/pkg/shell/process.go b/pkg/shell/process.go index 955335f2..b7abe0ff 100644 --- a/pkg/shell/process.go +++ b/pkg/shell/process.go @@ -4,12 +4,13 @@ import ( "flag" "fmt" "io/ioutil" - "log" "math/rand" "regexp" "strconv" "strings" "time" + + log "github.com/sirupsen/logrus" ) type Process struct { @@ -71,7 +72,7 @@ func (p *Process) StreamToStdout() { break } - log.Printf("Stream to stdout: %#v", data) + log.Debugf("Stream to stdout: %#v", data) p.OnStdoutCallback(data) } @@ -112,7 +113,7 @@ func (p *Process) Run() { err := p.loadCommand() if err != nil { - log.Printf("err: %v", err) + log.Errorf("Err: %v", err) return } @@ -157,18 +158,18 @@ func (p *Process) loadCommand() error { func (p *Process) readBufferSize() int { if flag.Lookup("test.v") == nil { return 100 - } else { - // simulating the worst kind of baud rate - // random in size, and possibly very short + } - // The implementation needs to handle everything. - rand.Seed(time.Now().UnixNano()) + // simulating the worst kind of baud rate + // random in size, and possibly very short - min := 1 - max := 20 + // The implementation needs to handle everything. + rand.Seed(time.Now().UnixNano()) - return rand.Intn(max-min) + min - } + min := 1 + max := 20 + + return rand.Intn(max-min) + min } // @@ -177,21 +178,21 @@ func (p *Process) readBufferSize() int { func (p *Process) read() error { buffer := make([]byte, p.readBufferSize()) - log.Println("Reading started") + log.Debug("Reading started") n, err := p.Shell.Read(&buffer) if err != nil { - log.Printf("Error while reading from the tty. Error: '%s'.", err.Error()) + log.Errorf("Error while reading from the tty. Error: '%s'.", err.Error()) return err } p.inputBuffer = append(p.inputBuffer, buffer[0:n]...) - log.Printf("reading data from shell. Input buffer: %#v", string(p.inputBuffer)) + log.Debugf("reading data from shell. Input buffer: %#v", string(p.inputBuffer)) return nil } func (p *Process) waitForStartMarker() error { - log.Println("Waiting for start marker", p.startMark) + log.Debugf("Waiting for start marker %s", p.startMark) // // Fill the output buffer, until the start marker appears @@ -222,7 +223,7 @@ func (p *Process) waitForStartMarker() error { } } - log.Println("Start marker found", p.startMark) + log.Debugf("Start marker found %s", p.startMark) return nil } @@ -232,7 +233,7 @@ func (p *Process) endMarkerHeaderIndex() int { } func (p *Process) scan() error { - log.Println("Scan started") + log.Debug("Scan started") err := p.waitForStartMarker() if err != nil { @@ -248,10 +249,10 @@ func (p *Process) scan() error { p.flushInputBufferTill(index) } - log.Println("Start of end marker detected, entering buffering mode.") + log.Debug("Start of end marker detected, entering buffering mode.") if match := p.commandEndRegex.FindStringSubmatch(string(p.inputBuffer)); len(match) == 2 { - log.Println("End marker detected. Exit code: ", match[1]) + log.Debug("End marker detected. Exit code: ", match[1]) exitCode = match[1] break @@ -286,17 +287,17 @@ func (p *Process) scan() error { p.flushOutputBuffer() - log.Println("Command output finished") - log.Println("Parsing exit code", exitCode) + log.Debug("Command output finished") + log.Debugf("Parsing exit code %s", exitCode) code, err := strconv.Atoi(exitCode) if err != nil { - log.Printf("Error while parsing exit code, err: %v", err) + log.Errorf("Error while parsing exit code, err: %v", err) return err } - log.Printf("Parsing exit code fininished %d", code) + log.Debugf("Parsing exit code finished %d", code) p.ExitCode = code return nil diff --git a/pkg/shell/shell.go b/pkg/shell/shell.go index af861885..6caf0bff 100644 --- a/pkg/shell/shell.go +++ b/pkg/shell/shell.go @@ -2,14 +2,15 @@ package shell import ( "bufio" + "errors" "fmt" - "log" "os" "os/exec" "strings" "time" - pty "github.com/kr/pty" + pty "github.com/creack/pty" + log "github.com/sirupsen/logrus" ) type Shell struct { @@ -30,11 +31,11 @@ func NewShell(bootCommand *exec.Cmd, storagePath string) (*Shell, error) { } func (s *Shell) Start() error { - log.Printf("Starting stateful shell") + log.Debug("Starting stateful shell") tty, err := pty.Start(s.BootCommand) if err != nil { - log.Printf("Failed to start stateful shell") + log.Errorf("Failed to start stateful shell: %v", err) return err } @@ -67,10 +68,10 @@ func (s *Shell) handleAbruptShellCloses() { msg = err.Error() } - log.Printf("Shell unexpectedly closed with %s. Closing associated TTY.", msg) + log.Debugf("Shell closed with %s. Closing associated TTY", msg) s.TTY.Close() - log.Printf("Publishing an exit signal.") + log.Debugf("Publishing an exit signal: %s", msg) s.ExitSignal <- msg }() } @@ -95,7 +96,7 @@ func (s *Shell) Read(buffer *([]byte)) (int, error) { } func (s *Shell) Write(instruction string) (int, error) { - log.Printf("Sending Instruction: %s", instruction) + log.Debugf("Sending Instruction: %s", instruction) done := make(chan bool, 1) @@ -142,12 +143,12 @@ func (s *Shell) silencePromptAndDisablePS1() error { // We wait until marker is displayed in the output - log.Println("Waiting for initialization") + log.Debug("Waiting for initialization") for stdoutScanner.Scan() { text := stdoutScanner.Text() - log.Printf("(tty) %s\n", text) + log.Debugf("(tty) %s\n", text) if strings.Contains(text, "executable file not found") { return fmt.Errorf(text) @@ -166,16 +167,20 @@ func (s *Shell) NewProcess(command string) *Process { } func (s *Shell) Close() error { - err := s.TTY.Close() - if err != nil { - log.Printf("Closing the TTY returned an error") - return err + if s.TTY != nil { + err := s.TTY.Close() + if err != nil { + log.Errorf("Closing the TTY returned an error: %v", err) + return err + } } - err = s.BootCommand.Process.Kill() - if err != nil { - log.Printf("Process killing procedure returned an erorr %+v\n", err) - return err + if s.BootCommand.Process != nil { + err := s.BootCommand.Process.Kill() + if err != nil && !errors.Is(err, os.ErrProcessDone) { + log.Errorf("Process killing procedure returned an error %+v", err) + return err + } } return nil diff --git a/test/e2e.rb b/test/e2e.rb index 6c1d4b43..9041fe77 100644 --- a/test/e2e.rb +++ b/test/e2e.rb @@ -4,160 +4,91 @@ require 'tempfile' require 'json' +require 'yaml' require 'timeout' require 'base64' -$JOB_ID = `uuidgen`.strip +require_relative "./e2e_support/api_mode" +require_relative "./e2e_support/listener_mode" -# based on secret passed to the running server -$TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.gLEycyHdyRRzUpauBxdDFmxT5KoOApFO5MHuvWPgFtY" +$JOB_ID = `uuidgen`.strip +$LOGGER = "" + +$strategy = nil + +case ENV["TEST_MODE"] +when "api" then + $strategy = ApiMode.new + $LOGGER = '{ "method": "pull" }' +when "listen" then + $strategy = ListenerMode.new + + $LOGGER = <<-JSON + { + "method": "push", + "url": "http://hub:4567/api/v1/logs/#{$JOB_ID}", + "token": "jwtToken" + } + JSON + + if !$AGENT_CONFIG + $AGENT_CONFIG = { + "endpoint" => "hub:4567", + "token" => "321h1l2jkh1jk42341", + "no-https" => true, + "shutdown-hook-path" => "", + "disconnect-after-job" => false, + "env-vars" => [], + "files" => [], + "fail-on-missing-files" => false + } + end +else + raise "Testing Mode not set" +end -$AGENT_PORT_IN_TESTS = 30000 +$strategy.boot_up_agent def start_job(request) - r = Tempfile.new - r.write(request) - r.close - - puts "============================" - puts "Sending job request to Agent" - - output = `curl -H "Authorization: Bearer #{$TOKEN}" --fail -X POST -k "https://0.0.0.0:30000/jobs" --data @#{r.path}` - - abort "Failed to send: #{output}" if $?.exitstatus != 0 + $strategy.start_job(request) end def stop_job - puts "============================" - puts "Stopping job..." - - output = `curl -H "Authorization: Bearer #{$TOKEN}" --fail -X POST -k "https://0.0.0.0:30000/jobs/terminate"` - - abort "Failed to stob job: #{output}" if $?.exitstatus != 0 + $strategy.stop_job() end def wait_for_command_to_start(cmd) - puts "=========================" - puts "Waiting for command to start '#{cmd}'" - - Timeout.timeout(60 * 2) do - loop do - `curl -H "Authorization: Bearer #{$TOKEN}" --fail -k "https://0.0.0.0:30000/job_logs" | grep "#{cmd}"` - - if $?.exitstatus == 0 - break - else - sleep 1 - end - end - end + $strategy.wait_for_command_to_start(cmd) end def wait_for_job_to_finish - puts "=========================" - puts "Waiting for job to finish" - - Timeout.timeout(60 * 2) do - loop do - `curl -H "Authorization: Bearer #{$TOKEN}" --fail -k "https://0.0.0.0:30000/job_logs" | grep "job_finished"` - - if $?.exitstatus == 0 - break - else - sleep 1 - end - end - end + $strategy.wait_for_job_to_finish() end def assert_job_log(expected_log) - puts "=========================" - puts "Asserting Job Logs" - - actual_log = `curl -H "Authorization: Bearer #{$TOKEN}" -k "https://0.0.0.0:30000/jobs/#{$JOB_ID}/log"` - - puts "-----------------------------------" - puts actual_log - puts "-----------------------------------" - - abort "Failed to fetch logs: #{actual_log}" if $?.exitstatus != 0 - - actual_log = actual_log.split("\n").map(&:strip).reject(&:empty?) - expected_log = expected_log.split("\n").map(&:strip).reject(&:empty?) - - index_in_actual_logs = 0 - index_in_expected_logs = 0 - - while index_in_actual_logs < actual_log.length && index_in_expected_logs < expected_log.length - begin - puts "Comparing log lines Actual=#{index_in_actual_logs} Expected=#{index_in_expected_logs}" - - expected_log_line = expected_log[index_in_expected_logs] - actual_log_line = actual_log[index_in_actual_logs] - - puts " actual: #{actual_log_line}" - puts " expected: #{expected_log_line}" - - actual_log_line_json = Hash[JSON.parse(actual_log_line).sort] - - if expected_log_line =~ /\*\*\* LONG_OUTPUT \*\*\*/ - if actual_log_line_json["event"] == "cmd_output" - # if we have a *** LONG_OUTPUT *** marker - - # we go to next actual log line - # but we stay on the same expected log line - - index_in_actual_logs += 1 - - next - else - # end of the LONG_OUTPUT marker, we increase the expected log line - # and in the next iteration we will compare regularly again - index_in_expected_logs += 1 - - next - end - else - # if there is no marker, we compare the JSONs - # we ignore the timestamps because they change every time - - expected_log_line_json = Hash[JSON.parse(expected_log_line).sort] - - if expected_log_line_json.keys != actual_log_line_json.keys - abort "(fail) JSON keys are different." - end - - expected_log_line_json.keys.each do |key| - # Special case when we want to ignore only the output - # ignore expected entries with '*' - - next if expected_log_line_json[key] == "*" + $strategy.assert_job_log(expected_log) +end - if expected_log_line_json[key] != actual_log_line_json[key] - abort "(fail) Values for '#{key}' are not equal." - end - end +def finished_callback_url + $strategy.finished_callback_url +end - index_in_actual_logs += 1 - index_in_expected_logs += 1 - end +def teardown_callback_url + $strategy.teardown_callback_url +end - puts "success" - rescue - puts "" - puts "Line Number: Actual=#{index_in_actual_logs} Expected=#{index_in_expected_logs}" - puts "Expected: '#{expected_log_line}'" - puts "Actual: '#{actual_log_line}'" +def wait_for_job_to_get_stuck + $strategy.wait_for_job_to_get_stuck +end - abort "(fail) Failed to parse log line" - end - end +def shutdown_agent + $strategy.shutdown_agent +end - if index_in_actual_logs != actual_log.length - abort "(fail) There are unchecked log lines from the actual log" - end +def wait_for_agent_to_shutdown + $strategy.wait_for_agent_to_shutdown +end - if index_in_expected_logs != expected_log.length - abort "(fail) There are unchecked log lines from the expected log" - end +def bad_callback_url + "https://httpbin.org/status/500" end diff --git a/test/e2e/docker/broken_unicode.rb b/test/e2e/docker/broken_unicode.rb index 798ffb09..2f18c62b 100644 --- a/test/e2e/docker/broken_unicode.rb +++ b/test/e2e/docker/broken_unicode.rb @@ -31,9 +31,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/check_dev_kvm.rb b/test/e2e/docker/check_dev_kvm.rb index adda9d36..14384bf8 100644 --- a/test/e2e/docker/check_dev_kvm.rb +++ b/test/e2e/docker/check_dev_kvm.rb @@ -29,36 +29,64 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON wait_for_job_to_finish -assert_job_log <<-LOG - {"event":"job_started", "timestamp":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Pulling docker images..."} - *** LONG_OUTPUT *** - {"event":"cmd_finished", "timestamp":"*", "directive":"Pulling docker images...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Starting the docker image..."} - {"event":"cmd_output", "timestamp":"*", "output":"Starting a new bash session.\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Starting the docker image...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} - - - {"event":"cmd_started", "timestamp":"*", "directive":"ls /dev | grep kvm"} - {"event":"cmd_output", "timestamp":"*", "output":"kvm\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"ls /dev | grep kvm","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"job_finished", "timestamp":"*", "result":"passed"} - -LOG +case ENV["TEST_MODE"] +when "api" then + assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Pulling docker images..."} + *** LONG_OUTPUT *** + {"event":"cmd_finished", "timestamp":"*", "directive":"Pulling docker images...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Starting the docker image..."} + {"event":"cmd_output", "timestamp":"*", "output":"Starting a new bash session.\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Starting the docker image...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"ls /dev | grep kvm"} + {"event":"cmd_output", "timestamp":"*", "output":"kvm\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"ls /dev | grep kvm","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} + {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} + LOG +when "listen" then + assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Pulling docker images..."} + *** LONG_OUTPUT *** + {"event":"cmd_finished", "timestamp":"*", "directive":"Pulling docker images...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Starting the docker image..."} + {"event":"cmd_output", "timestamp":"*", "output":"Starting a new bash session.\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Starting the docker image...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"ls /dev | grep kvm"} + {"event":"cmd_finished", "timestamp":"*", "directive":"ls /dev | grep kvm","event":"cmd_finished","exit_code":1,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed"} + {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"failed"} + LOG + +else + raise "Testing Mode not set" +end diff --git a/test/e2e/docker/command_aliases.rb b/test/e2e/docker/command_aliases.rb index 5dbbcdb8..acc109da 100644 --- a/test/e2e/docker/command_aliases.rb +++ b/test/e2e/docker/command_aliases.rb @@ -29,9 +29,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/container_custom_name.rb b/test/e2e/docker/container_custom_name.rb index b2f1c14c..342e3eff 100644 --- a/test/e2e/docker/container_custom_name.rb +++ b/test/e2e/docker/container_custom_name.rb @@ -19,10 +19,10 @@ }, "env_vars": [ - { "name": "A", "value": "#{`echo "hello" | base64`}" }, - { "name": "B", "value": "#{`echo "how are you?" | base64`}" }, - { "name": "C", "value": "#{`echo "quotes ' quotes" | base64`}" }, - { "name": "D", "value": "#{`echo '$PATH:/etc/a' | base64`}" } + { "name": "A", "value": "#{`echo "hello" | base64 | tr -d '\n'`}" }, + { "name": "B", "value": "#{`echo "how are you?" | base64 | tr -d '\n'`}" }, + { "name": "C", "value": "#{`echo "quotes ' quotes" | base64 | tr -d '\n'`}" }, + { "name": "D", "value": "#{`echo '$PATH:/etc/a' | base64 | tr -d '\n'`}" } ], "files": [], @@ -37,9 +37,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/container_env_vars.rb b/test/e2e/docker/container_env_vars.rb index 31dfa32c..9967c7e4 100644 --- a/test/e2e/docker/container_env_vars.rb +++ b/test/e2e/docker/container_env_vars.rb @@ -15,7 +15,7 @@ "name": "main", "image": "ruby:2.6", "env_vars": [ - { "name": "FOO", "value": "#{`echo "bar" | base64`}" } + { "name": "FOO", "value": "#{`echo "bar" | base64 | tr -d '\n'`}" } ] } ] @@ -30,9 +30,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/container_options.rb b/test/e2e/docker/container_options.rb index 0d04a6e0..8a3bbc60 100644 --- a/test/e2e/docker/container_options.rb +++ b/test/e2e/docker/container_options.rb @@ -35,9 +35,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/docker_in_docker.rb b/test/e2e/docker/docker_in_docker.rb index 7723d3ef..274047fc 100644 --- a/test/e2e/docker/docker_in_docker.rb +++ b/test/e2e/docker/docker_in_docker.rb @@ -29,9 +29,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/docker_private_image_ecr.rb b/test/e2e/docker/docker_private_image_ecr.rb index 85bb18ea..4a072794 100644 --- a/test/e2e/docker/docker_private_image_ecr.rb +++ b/test/e2e/docker/docker_private_image_ecr.rb @@ -20,10 +20,10 @@ "image_pull_credentials": [ { "env_vars": [ - { "name": "DOCKER_CREDENTIAL_TYPE", "value": "#{Base64.encode64("AWS_ECR")}" }, - { "name": "AWS_REGION", "value": "#{Base64.encode64(ENV['AWS_REGION'])}" }, - { "name": "AWS_ACCESS_KEY_ID", "value": "#{Base64.encode64(ENV['AWS_ACCESS_KEY_ID'])}" }, - { "name": "AWS_SECRET_ACCESS_KEY", "value": "#{Base64.encode64(ENV['AWS_SECRET_ACCESS_KEY'])}" } + { "name": "DOCKER_CREDENTIAL_TYPE", "value": "#{Base64.strict_encode64("AWS_ECR")}" }, + { "name": "AWS_REGION", "value": "#{Base64.strict_encode64(ENV['AWS_REGION'])}" }, + { "name": "AWS_ACCESS_KEY_ID", "value": "#{Base64.strict_encode64(ENV['AWS_ACCESS_KEY_ID'])}" }, + { "name": "AWS_SECRET_ACCESS_KEY", "value": "#{Base64.strict_encode64(ENV['AWS_SECRET_ACCESS_KEY'])}" } ] } ] @@ -40,9 +40,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/docker_private_image_ecr_bad_creds.rb b/test/e2e/docker/docker_private_image_ecr_bad_creds.rb index a4f729a1..3a353723 100644 --- a/test/e2e/docker/docker_private_image_ecr_bad_creds.rb +++ b/test/e2e/docker/docker_private_image_ecr_bad_creds.rb @@ -20,10 +20,10 @@ "image_pull_credentials": [ { "env_vars": [ - { "name": "DOCKER_CREDENTIAL_TYPE", "value": "#{Base64.encode64("AWS_ECR")}" }, - { "name": "AWS_REGION", "value": "#{Base64.encode64(ENV['AWS_REGION'])}" }, - { "name": "AWS_ACCESS_KEY_ID", "value": "#{Base64.encode64("AAABBBCCCDDDEEEFFF")}" }, - { "name": "AWS_SECRET_ACCESS_KEY", "value": "#{Base64.encode64('abcdefghijklmnop')}" } + { "name": "DOCKER_CREDENTIAL_TYPE", "value": "#{Base64.strict_encode64("AWS_ECR")}" }, + { "name": "AWS_REGION", "value": "#{Base64.strict_encode64(ENV['AWS_REGION'])}" }, + { "name": "AWS_ACCESS_KEY_ID", "value": "#{Base64.strict_encode64("AAABBBCCCDDDEEEFFF")}" }, + { "name": "AWS_SECRET_ACCESS_KEY", "value": "#{Base64.strict_encode64('abcdefghijklmnop')}" } ] } ] @@ -40,9 +40,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/docker_private_image_gcr.rb b/test/e2e/docker/docker_private_image_gcr.rb index 105cb05f..fac6ab3a 100644 --- a/test/e2e/docker/docker_private_image_gcr.rb +++ b/test/e2e/docker/docker_private_image_gcr.rb @@ -20,8 +20,8 @@ "image_pull_credentials": [ { "env_vars": [ - { "name": "DOCKER_CREDENTIAL_TYPE", "value": "#{Base64.encode64("GCR")}" }, - { "name": "GCR_HOSTNAME", "value": "#{Base64.encode64(ENV['GCR_HOSTNAME'])}" } + { "name": "DOCKER_CREDENTIAL_TYPE", "value": "#{Base64.strict_encode64("GCR")}" }, + { "name": "GCR_HOSTNAME", "value": "#{Base64.strict_encode64(ENV['GCR_HOSTNAME'])}" } ], "files": [ { "path": "/tmp/gcr/keyfile.json", "content": "#{ENV['GCR_KEYFILE']}", "mode": "0755" } @@ -41,9 +41,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/docker_private_image_gcr_bad_creds.rb b/test/e2e/docker/docker_private_image_gcr_bad_creds.rb index 14c87aaa..4b8ef9bd 100644 --- a/test/e2e/docker/docker_private_image_gcr_bad_creds.rb +++ b/test/e2e/docker/docker_private_image_gcr_bad_creds.rb @@ -20,8 +20,8 @@ "image_pull_credentials": [ { "env_vars": [ - { "name": "DOCKER_CREDENTIAL_TYPE", "value": "#{Base64.encode64("GCR")}" }, - { "name": "GCR_HOSTNAME", "value": "#{Base64.encode64(ENV['GCR_HOSTNAME'])}" } + { "name": "DOCKER_CREDENTIAL_TYPE", "value": "#{Base64.strict_encode64("GCR")}" }, + { "name": "GCR_HOSTNAME", "value": "#{Base64.strict_encode64(ENV['GCR_HOSTNAME'])}" } ], "files": [ { "path": "/tmp/gcr/keyfile.json", "content": "#{ENV['GCR_KEYFILE_BAD']}", "mode": "0755" } @@ -41,9 +41,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/docker_registry_private_image.rb b/test/e2e/docker/docker_registry_private_image.rb index 12b8471c..93997489 100644 --- a/test/e2e/docker/docker_registry_private_image.rb +++ b/test/e2e/docker/docker_registry_private_image.rb @@ -20,10 +20,10 @@ "image_pull_credentials": [ { "env_vars": [ - { "name": "DOCKER_CREDENTIAL_TYPE", "value": "#{Base64.encode64("GenericDocker")}" }, - { "name": "DOCKER_URL", "value": "#{Base64.encode64(ENV['DOCKER_URL'])}" }, - { "name": "DOCKER_USERNAME", "value": "#{Base64.encode64(ENV['DOCKER_USERNAME'])}" }, - { "name": "DOCKER_PASSWORD", "value": "#{Base64.encode64(ENV['DOCKER_PASSWORD'])}" } + { "name": "DOCKER_CREDENTIAL_TYPE", "value": "#{Base64.strict_encode64("GenericDocker")}" }, + { "name": "DOCKER_URL", "value": "#{Base64.strict_encode64(ENV['DOCKER_URL'])}" }, + { "name": "DOCKER_USERNAME", "value": "#{Base64.strict_encode64(ENV['DOCKER_USERNAME'])}" }, + { "name": "DOCKER_PASSWORD", "value": "#{Base64.strict_encode64(ENV['DOCKER_PASSWORD'])}" } ] } ] @@ -40,9 +40,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/docker_registry_private_image_bad_creds.rb b/test/e2e/docker/docker_registry_private_image_bad_creds.rb index 6a25857a..edde0a26 100644 --- a/test/e2e/docker/docker_registry_private_image_bad_creds.rb +++ b/test/e2e/docker/docker_registry_private_image_bad_creds.rb @@ -20,10 +20,10 @@ "image_pull_credentials": [ { "env_vars": [ - { "name": "DOCKER_CREDENTIAL_TYPE", "value": "#{Base64.encode64("GenericDocker")}" }, - { "name": "DOCKER_URL", "value": "#{Base64.encode64(ENV['DOCKER_URL'])}" }, - { "name": "DOCKER_USERNAME", "value": "#{Base64.encode64("lasagna")}" }, - { "name": "DOCKER_PASSWORD", "value": "#{Base64.encode64("spaghetti")}" } + { "name": "DOCKER_CREDENTIAL_TYPE", "value": "#{Base64.strict_encode64("GenericDocker")}" }, + { "name": "DOCKER_URL", "value": "#{Base64.strict_encode64(ENV['DOCKER_URL'])}" }, + { "name": "DOCKER_USERNAME", "value": "#{Base64.strict_encode64("lasagna")}" }, + { "name": "DOCKER_PASSWORD", "value": "#{Base64.strict_encode64("spaghetti")}" } ] } ] @@ -40,9 +40,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/dockerhub_private_image.rb b/test/e2e/docker/dockerhub_private_image.rb index 16721ffb..6982d5b1 100644 --- a/test/e2e/docker/dockerhub_private_image.rb +++ b/test/e2e/docker/dockerhub_private_image.rb @@ -20,9 +20,9 @@ "image_pull_credentials": [ { "env_vars": [ - { "name": "DOCKER_CREDENTIAL_TYPE", "value": "#{Base64.encode64("DockerHub")}" }, - { "name": "DOCKERHUB_USERNAME", "value": "#{Base64.encode64("semaphoreagentprivatepuller")}" }, - { "name": "DOCKERHUB_PASSWORD", "value": "#{Base64.encode64("semaphoreagentprivatepullerpassword")}" } + { "name": "DOCKER_CREDENTIAL_TYPE", "value": "#{Base64.strict_encode64("DockerHub")}" }, + { "name": "DOCKERHUB_USERNAME", "value": "#{Base64.strict_encode64("semaphoreagentprivatepuller")}" }, + { "name": "DOCKERHUB_PASSWORD", "value": "#{Base64.strict_encode64("semaphoreagentprivatepullerpassword")}" } ] } ] @@ -39,9 +39,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/dockerhub_private_image_bad_creds.rb b/test/e2e/docker/dockerhub_private_image_bad_creds.rb index 092c7f71..36a1db31 100644 --- a/test/e2e/docker/dockerhub_private_image_bad_creds.rb +++ b/test/e2e/docker/dockerhub_private_image_bad_creds.rb @@ -20,9 +20,9 @@ "image_pull_credentials": [ { "env_vars": [ - { "name": "DOCKER_CREDENTIAL_TYPE", "value": "#{Base64.encode64("DockerHub")}" }, - { "name": "DOCKERHUB_USERNAME", "value": "#{Base64.encode64("lasagna")}" }, - { "name": "DOCKERHUB_PASSWORD", "value": "#{Base64.encode64("spaghetti")}" } + { "name": "DOCKER_CREDENTIAL_TYPE", "value": "#{Base64.strict_encode64("DockerHub")}" }, + { "name": "DOCKERHUB_USERNAME", "value": "#{Base64.strict_encode64("lasagna")}" }, + { "name": "DOCKERHUB_PASSWORD", "value": "#{Base64.strict_encode64("spaghetti")}" } ] } ] @@ -39,9 +39,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/env_vars.rb b/test/e2e/docker/env_vars.rb index 63e02dea..0fb40206 100644 --- a/test/e2e/docker/env_vars.rb +++ b/test/e2e/docker/env_vars.rb @@ -19,10 +19,10 @@ }, "env_vars": [ - { "name": "A", "value": "#{`echo "hello" | base64`}" }, - { "name": "B", "value": "#{`echo "how are you?" | base64`}" }, - { "name": "C", "value": "#{`echo "quotes ' quotes" | base64`}" }, - { "name": "D", "value": "#{`echo '$PATH:/etc/a' | base64`}" } + { "name": "A", "value": "#{`echo "hello" | base64 | tr -d '\n'`}" }, + { "name": "B", "value": "#{`echo "how are you?" | base64 | tr -d '\n'`}" }, + { "name": "C", "value": "#{`echo "quotes ' quotes" | base64 | tr -d '\n'`}" }, + { "name": "D", "value": "#{`echo '$PATH:/etc/a' | base64 | tr -d '\n'`}" } ], "files": [], @@ -37,9 +37,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/epilogue_on_fail.rb b/test/e2e/docker/epilogue_on_fail.rb index b6c64e59..7c9d5d08 100644 --- a/test/e2e/docker/epilogue_on_fail.rb +++ b/test/e2e/docker/epilogue_on_fail.rb @@ -39,9 +39,10 @@ ], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/epilogue_on_pass.rb b/test/e2e/docker/epilogue_on_pass.rb index 1d2ac0c8..0786c07e 100644 --- a/test/e2e/docker/epilogue_on_pass.rb +++ b/test/e2e/docker/epilogue_on_pass.rb @@ -39,9 +39,10 @@ ], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/failed_job.rb b/test/e2e/docker/failed_job.rb index 6c828e8a..1475322d 100644 --- a/test/e2e/docker/failed_job.rb +++ b/test/e2e/docker/failed_job.rb @@ -29,9 +29,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/file_injection.rb b/test/e2e/docker/file_injection.rb index bc6c0553..01384668 100644 --- a/test/e2e/docker/file_injection.rb +++ b/test/e2e/docker/file_injection.rb @@ -21,9 +21,9 @@ "env_vars": [], "files": [ - { "path": "test.txt", "content": "#{`echo "hello" | base64`}", "mode": "0644" }, - { "path": "/a/b/c", "content": "#{`echo "hello" | base64`}", "mode": "0644" }, - { "path": "/tmp/a", "content": "#{`echo "hello" | base64`}", "mode": "+x" } + { "path": "test.txt", "content": "#{`echo "hello" | base64 | tr -d '\n'`}", "mode": "0644" }, + { "path": "/a/b/c", "content": "#{`echo "hello" | base64 | tr -d '\n'`}", "mode": "0644" }, + { "path": "/tmp/a", "content": "#{`echo "hello" | base64 | tr -d '\n'`}", "mode": "+x" } ], "commands": [ @@ -35,9 +35,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/file_injection_broken_file_mode.rb b/test/e2e/docker/file_injection_broken_file_mode.rb index c22c0b16..ad4e3a87 100644 --- a/test/e2e/docker/file_injection_broken_file_mode.rb +++ b/test/e2e/docker/file_injection_broken_file_mode.rb @@ -21,7 +21,7 @@ "env_vars": [], "files": [ - { "path": "test.txt", "content": "#{`echo "hello" | base64`}", "mode": "obviously broken" } + { "path": "test.txt", "content": "#{`echo "hello" | base64 | tr -d '\n'`}", "mode": "obviously broken" } ], "commands": [ @@ -33,9 +33,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/hello_world.rb b/test/e2e/docker/hello_world.rb index 4a845a61..d56577da 100644 --- a/test/e2e/docker/hello_world.rb +++ b/test/e2e/docker/hello_world.rb @@ -29,9 +29,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/host_setup_commands.rb b/test/e2e/docker/host_setup_commands.rb index f34259a2..dd77fe2e 100644 --- a/test/e2e/docker/host_setup_commands.rb +++ b/test/e2e/docker/host_setup_commands.rb @@ -32,9 +32,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/job_stopping.rb b/test/e2e/docker/job_stopping.rb index 5cc72a70..17c4de7d 100644 --- a/test/e2e/docker/job_stopping.rb +++ b/test/e2e/docker/job_stopping.rb @@ -30,9 +30,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/job_stopping_on_epilogue.rb b/test/e2e/docker/job_stopping_on_epilogue.rb new file mode 100644 index 00000000..4fe622fa --- /dev/null +++ b/test/e2e/docker/job_stopping_on_epilogue.rb @@ -0,0 +1,52 @@ +#!/bin/ruby +# rubocop:disable all + +require_relative '../../e2e' + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + + "env_vars": [], + + "files": [], + + "commands": [ + { "directive": "echo 'here'" } + ], + + "epilogue_always_commands": [ + { "directive": "sleep infinity" } + ], + + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_command_to_start("sleep infinity") + +sleep 1 + +stop_job + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"echo 'here'"} + {"event":"cmd_output", "timestamp":"*", "output":"here\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo 'here'","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} + {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"sleep infinity"} + {"event":"cmd_finished", "timestamp":"*", "directive":"sleep infinity","exit_code":1,"finished_at":"*","started_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"stopped"} +LOG diff --git a/test/e2e/docker/multiple_containers.rb b/test/e2e/docker/multiple_containers.rb index 090fc7bd..fbb6f51f 100644 --- a/test/e2e/docker/multiple_containers.rb +++ b/test/e2e/docker/multiple_containers.rb @@ -27,15 +27,16 @@ "files": [], "commands": [ - { "directive": "docker ps -a | grep db | wc -l" } + { "directive": "docker ps -a | grep postgres | wc -l" } ], "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON @@ -55,9 +56,9 @@ {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"docker ps -a | grep db | wc -l"} + {"event":"cmd_started", "timestamp":"*", "directive":"docker ps -a | grep postgres | wc -l"} {"event":"cmd_output", "timestamp":"*", "output":"1\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"docker ps -a | grep db | wc -l","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_finished", "timestamp":"*", "directive":"docker ps -a | grep postgres | wc -l","exit_code":0,"finished_at":"*","started_at":"*"} {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} {"event":"job_finished", "timestamp":"*", "result":"passed"} diff --git a/test/e2e/docker/no_bash.rb b/test/e2e/docker/no_bash.rb index b0723a5c..d151cb6b 100644 --- a/test/e2e/docker/no_bash.rb +++ b/test/e2e/docker/no_bash.rb @@ -38,9 +38,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/non_existing_image.rb b/test/e2e/docker/non_existing_image.rb index c3488db3..6955dfac 100644 --- a/test/e2e/docker/non_existing_image.rb +++ b/test/e2e/docker/non_existing_image.rb @@ -29,9 +29,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/ssh_jump_points.rb b/test/e2e/docker/ssh_jump_points.rb index 4fecb840..171bd9a6 100644 --- a/test/e2e/docker/ssh_jump_points.rb +++ b/test/e2e/docker/ssh_jump_points.rb @@ -65,9 +65,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/stty_restoration.rb b/test/e2e/docker/stty_restoration.rb index 3e1fe237..8c5356f9 100644 --- a/test/e2e/docker/stty_restoration.rb +++ b/test/e2e/docker/stty_restoration.rb @@ -30,9 +30,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/unicode.rb b/test/e2e/docker/unicode.rb index 575db2c7..33b13d72 100644 --- a/test/e2e/docker/unicode.rb +++ b/test/e2e/docker/unicode.rb @@ -32,9 +32,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/docker/unknown_command.rb b/test/e2e/docker/unknown_command.rb index f52f63e1..92e7a40f 100644 --- a/test/e2e/docker/unknown_command.rb +++ b/test/e2e/docker/unknown_command.rb @@ -29,9 +29,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/self-hosted/broken_finished_callback.rb b/test/e2e/self-hosted/broken_finished_callback.rb new file mode 100644 index 00000000..8d3b4081 --- /dev/null +++ b/test/e2e/self-hosted/broken_finished_callback.rb @@ -0,0 +1,28 @@ +#!/bin/ruby +# rubocop:disable all + +require_relative '../../e2e' + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + + "env_vars": [], + + "files": [], + + "commands": [ + { "directive": "echo 'hello'" } + ], + + "epilogue_always_commands": [], + + "callbacks": { + "finished": "#{bad_callback_url}", + "teardown_finished": "#{bad_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_job_to_get_stuck diff --git a/test/e2e/self-hosted/broken_get_job.rb b/test/e2e/self-hosted/broken_get_job.rb new file mode 100644 index 00000000..5de7b711 --- /dev/null +++ b/test/e2e/self-hosted/broken_get_job.rb @@ -0,0 +1,30 @@ +#!/bin/ruby +# rubocop:disable all + +require_relative '../../e2e' + +$JOB_ID = "bad-job-id" + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + + "env_vars": [], + + "files": [], + + "commands": [ + { "directive": "echo 'hello'" } + ], + + "epilogue_always_commands": [], + + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_job_to_get_stuck diff --git a/test/e2e/self-hosted/broken_teardown_callback.rb b/test/e2e/self-hosted/broken_teardown_callback.rb new file mode 100644 index 00000000..25893778 --- /dev/null +++ b/test/e2e/self-hosted/broken_teardown_callback.rb @@ -0,0 +1,28 @@ +#!/bin/ruby +# rubocop:disable all + +require_relative '../../e2e' + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + + "env_vars": [], + + "files": [], + + "commands": [ + { "directive": "echo 'hello'" } + ], + + "epilogue_always_commands": [], + + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{bad_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_job_to_get_stuck diff --git a/test/e2e/self-hosted/docker_compose_fail_on_missing_host_files.rb b/test/e2e/self-hosted/docker_compose_fail_on_missing_host_files.rb new file mode 100644 index 00000000..04edd0d3 --- /dev/null +++ b/test/e2e/self-hosted/docker_compose_fail_on_missing_host_files.rb @@ -0,0 +1,64 @@ +#!/bin/ruby +# rubocop:disable all + +File.write("/tmp/agent/file1.txt", "Hello from file1.txt") +File.write("/tmp/agent/file2.txt", "Hello from file2.txt") + +$AGENT_CONFIG = { + "endpoint" => "hub:4567", + "token" => "321h1l2jkh1jk42341", + "no-https" => true, + "shutdown-hook-path" => "", + "disconnect-after-job" => false, + "env-vars" => [], + "files" => [ + "/tmp/agent/file1.txt:/tmp/agent/file1.txt", + "/tmp/agent/file2.txt:/tmp/agent/file2.txt", + "/tmp/agent/notfound.txt:/tmp/agent/notfound.txt" + ], + "fail-on-missing-files" => true +} + +require_relative '../../e2e' + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + + "executor": "dockercompose", + + "compose": { + "containers": [ + { + "name": "main", + "image": "ruby:2.6" + } + ] + }, + + "env_vars": [], + + "files": [], + + "commands": [ + { "directive": "cat /tmp/agent/file1.txt" }, + { "directive": "cat /tmp/agent/file2.txt" }, + { "directive": "cat /tmp/agent/notfound.txt" } + ], + + "epilogue_always_commands": [], + + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + {"event":"job_finished", "timestamp":"*", "result":"failed"} +LOG diff --git a/test/e2e/self-hosted/docker_compose_host_env_vars.rb b/test/e2e/self-hosted/docker_compose_host_env_vars.rb new file mode 100644 index 00000000..34a6149f --- /dev/null +++ b/test/e2e/self-hosted/docker_compose_host_env_vars.rb @@ -0,0 +1,100 @@ +#!/bin/ruby +# rubocop:disable all + +$AGENT_CONFIG = { + "endpoint" => "hub:4567", + "token" => "321h1l2jkh1jk42341", + "no-https" => true, + "shutdown-hook-path" => "", + "disconnect-after-job" => false, + "env-vars" => [ + "A=hello", + "B=how are you?", + "C=quotes ' quotes", + "D=$PATH:/etc/a" + ], + "files" => [], + "fail-on-missing-files" => false +} + +require_relative '../../e2e' + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + + "executor": "dockercompose", + + "compose": { + "containers": [ + { + "name": "main", + "image": "ruby:2.6" + } + ] + }, + + "env_vars": [], + + "files": [], + + "commands": [ + { "directive": "echo $A" }, + { "directive": "echo $B" }, + { "directive": "echo $C" }, + { "directive": "echo $D" } + ], + + "epilogue_always_commands": [], + + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Pulling docker images..."} + *** LONG_OUTPUT *** + {"event":"cmd_finished", "timestamp":"*", "directive":"Pulling docker images...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Starting the docker image..."} + {"event":"cmd_output", "timestamp":"*", "output":"Starting a new bash session.\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Starting the docker image...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting A\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting B\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting C\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting D\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"echo $A"} + {"event":"cmd_output", "timestamp":"*", "output":"hello\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo $A","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"echo $B"} + {"event":"cmd_output", "timestamp":"*", "output":"how are you?\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo $B","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"echo $C"} + {"event":"cmd_output", "timestamp":"*", "output":"quotes ' quotes\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo $C","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"echo $D"} + {"event":"cmd_output", "timestamp":"*", "output":"$PATH:/etc/a\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo $D","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} + {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} +LOG diff --git a/test/e2e/self-hosted/docker_compose_host_files.rb b/test/e2e/self-hosted/docker_compose_host_files.rb new file mode 100644 index 00000000..236d41dd --- /dev/null +++ b/test/e2e/self-hosted/docker_compose_host_files.rb @@ -0,0 +1,87 @@ +#!/bin/ruby +# rubocop:disable all + +File.write("/tmp/agent/file1.txt", "Hello from file1.txt") +File.write("/tmp/agent/file2.txt", "Hello from file2.txt") + +$AGENT_CONFIG = { + "endpoint" => "hub:4567", + "token" => "321h1l2jkh1jk42341", + "no-https" => true, + "shutdown-hook-path" => "", + "disconnect-after-job" => false, + "env-vars" => [], + "files" => [ + "/tmp/agent/file1.txt:/tmp/agent/file1.txt", + "/tmp/agent/file2.txt:/tmp/agent/file2.txt" + ], + "fail-on-missing-files" => false +} + +require_relative '../../e2e' + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + + "executor": "dockercompose", + + "compose": { + "containers": [ + { + "name": "main", + "image": "ruby:2.6" + } + ] + }, + + "env_vars": [], + + "files": [], + + "commands": [ + { "directive": "cat /tmp/agent/file1.txt" }, + { "directive": "cat /tmp/agent/file2.txt" } + ], + + "epilogue_always_commands": [], + + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Pulling docker images..."} + *** LONG_OUTPUT *** + {"event":"cmd_finished", "timestamp":"*", "directive":"Pulling docker images...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Starting the docker image..."} + {"event":"cmd_output", "timestamp":"*", "output":"Starting a new bash session.\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Starting the docker image...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"cat /tmp/agent/file1.txt"} + {"event":"cmd_output", "timestamp":"*", "output":"Hello from file1.txt"} + {"event":"cmd_finished", "timestamp":"*", "directive":"cat /tmp/agent/file1.txt","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"cat /tmp/agent/file2.txt"} + {"event":"cmd_output", "timestamp":"*", "output":"Hello from file2.txt"} + {"event":"cmd_finished", "timestamp":"*", "directive":"cat /tmp/agent/file2.txt","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} + {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} +LOG diff --git a/test/e2e/self-hosted/docker_compose_missing_host_files.rb b/test/e2e/self-hosted/docker_compose_missing_host_files.rb new file mode 100644 index 00000000..335c21ae --- /dev/null +++ b/test/e2e/self-hosted/docker_compose_missing_host_files.rb @@ -0,0 +1,93 @@ +#!/bin/ruby +# rubocop:disable all + +File.write("/tmp/agent/file1.txt", "Hello from file1.txt") +File.write("/tmp/agent/file2.txt", "Hello from file2.txt") + +$AGENT_CONFIG = { + "endpoint" => "hub:4567", + "token" => "321h1l2jkh1jk42341", + "no-https" => true, + "shutdown-hook-path" => "", + "disconnect-after-job" => false, + "env-vars" => [], + "files" => [ + "/tmp/agent/file1.txt:/tmp/agent/file1.txt", + "/tmp/agent/file2.txt:/tmp/agent/file2.txt", + "/tmp/agent/notfound.txt:/tmp/agent/notfound.txt" + ], + "fail-on-missing-files" => false +} + +require_relative '../../e2e' + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + + "executor": "dockercompose", + + "compose": { + "containers": [ + { + "name": "main", + "image": "ruby:2.6" + } + ] + }, + + "env_vars": [], + + "files": [], + + "commands": [ + { "directive": "cat /tmp/agent/file1.txt" }, + { "directive": "cat /tmp/agent/file2.txt" }, + { "directive": "cat /tmp/agent/notfound.txt" } + ], + + "epilogue_always_commands": [], + + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Pulling docker images..."} + *** LONG_OUTPUT *** + {"event":"cmd_finished", "timestamp":"*", "directive":"Pulling docker images...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Starting the docker image..."} + {"event":"cmd_output", "timestamp":"*", "output":"Starting a new bash session.\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Starting the docker image...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"cat /tmp/agent/file1.txt"} + {"event":"cmd_output", "timestamp":"*", "output":"Hello from file1.txt"} + {"event":"cmd_finished", "timestamp":"*", "directive":"cat /tmp/agent/file1.txt","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"cat /tmp/agent/file2.txt"} + {"event":"cmd_output", "timestamp":"*", "output":"Hello from file2.txt"} + {"event":"cmd_finished", "timestamp":"*", "directive":"cat /tmp/agent/file2.txt","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"cat /tmp/agent/notfound.txt"} + {"event":"cmd_output", "timestamp":"*", "output":"cat: /tmp/agent/notfound.txt: No such file or directory\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"cat /tmp/agent/notfound.txt","exit_code":1,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed"} + {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"failed"} +LOG diff --git a/test/e2e/self-hosted/no_ssh_jump_points.rb b/test/e2e/self-hosted/no_ssh_jump_points.rb new file mode 100644 index 00000000..47af62de --- /dev/null +++ b/test/e2e/self-hosted/no_ssh_jump_points.rb @@ -0,0 +1,48 @@ +#!/bin/ruby +# rubocop:disable all + +require_relative '../../e2e' + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + + "ssh_public_keys": [], + + "env_vars": [], + + "files": [], + + "commands": [ + { "directive": "ls -1q ~/.ssh | wc -l" } + ], + + "epilogue_always_commands": [], + + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"ls -1q ~/.ssh | wc -l"} + {"event":"cmd_output", "timestamp":"*", "output":"0\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"ls -1q ~/.ssh | wc -l","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} + {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} +LOG \ No newline at end of file diff --git a/test/e2e/self-hosted/shell_host_env_vars.rb b/test/e2e/self-hosted/shell_host_env_vars.rb new file mode 100644 index 00000000..df2cc8d3 --- /dev/null +++ b/test/e2e/self-hosted/shell_host_env_vars.rb @@ -0,0 +1,81 @@ +#!/bin/ruby +# rubocop:disable all + +$AGENT_CONFIG = { + "endpoint" => "hub:4567", + "token" => "321h1l2jkh1jk42341", + "no-https" => true, + "shutdown-hook-path" => "", + "disconnect-after-job" => false, + "env-vars" => [ + "A=hello", + "B=how are you?", + "C=quotes ' quotes", + "D=$PATH:/etc/a" + ], + "files" => [], + "fail-on-missing-files" => false +} + +require_relative '../../e2e' + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + + "env_vars": [], + + "files": [], + + "commands": [ + { "directive": "echo $A" }, + { "directive": "echo $B" }, + { "directive": "echo $C" }, + { "directive": "echo $D" } + ], + + "epilogue_always_commands": [], + + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting A\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting B\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting C\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting D\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"echo $A"} + {"event":"cmd_output", "timestamp":"*", "output":"hello\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo $A","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"echo $B"} + {"event":"cmd_output", "timestamp":"*", "output":"how are you?\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo $B","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"echo $C"} + {"event":"cmd_output", "timestamp":"*", "output":"quotes ' quotes\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo $C","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"echo $D"} + {"event":"cmd_output", "timestamp":"*", "output":"$PATH:/etc/a\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo $D","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} + {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} +LOG diff --git a/test/e2e/self-hosted/shutdown.rb b/test/e2e/self-hosted/shutdown.rb new file mode 100644 index 00000000..d1fc0037 --- /dev/null +++ b/test/e2e/self-hosted/shutdown.rb @@ -0,0 +1,47 @@ +#!/bin/ruby +# rubocop:disable all + +require_relative '../../e2e' + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + + "env_vars": [], + + "files": [], + + "commands": [ + { "directive": "sleep infinity" } + ], + + "epilogue_always_commands": [], + + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_command_to_start("sleep infinity") + +shutdown_agent + +wait_for_agent_to_shutdown + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"sleep infinity"} + {"event":"cmd_finished", "timestamp":"*", "directive":"sleep infinity","exit_code":1,"finished_at":"*","started_at":"*"} + + {"event":"job_finished", "timestamp":"*", "result":"stopped"} +LOG diff --git a/test/e2e/self-hosted/shutdown_while_waiting.rb b/test/e2e/self-hosted/shutdown_while_waiting.rb new file mode 100644 index 00000000..5d996e3a --- /dev/null +++ b/test/e2e/self-hosted/shutdown_while_waiting.rb @@ -0,0 +1,8 @@ +#!/bin/ruby +# rubocop:disable all + +require_relative '../../e2e' + +shutdown_agent + +wait_for_agent_to_shutdown diff --git a/test/e2e/shell/broken_unicode.rb b/test/e2e/shell/broken_unicode.rb index 72698164..9e5bb347 100644 --- a/test/e2e/shell/broken_unicode.rb +++ b/test/e2e/shell/broken_unicode.rb @@ -20,9 +20,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/shell/command_aliases.rb b/test/e2e/shell/command_aliases.rb index 719f89df..f91ba630 100644 --- a/test/e2e/shell/command_aliases.rb +++ b/test/e2e/shell/command_aliases.rb @@ -18,9 +18,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/shell/env_vars.rb b/test/e2e/shell/env_vars.rb index 095e3630..dda413b7 100644 --- a/test/e2e/shell/env_vars.rb +++ b/test/e2e/shell/env_vars.rb @@ -8,10 +8,10 @@ "id": "#{$JOB_ID}", "env_vars": [ - { "name": "A", "value": "#{`echo "hello" | base64`}" }, - { "name": "B", "value": "#{`echo "how are you?" | base64`}" }, - { "name": "C", "value": "#{`echo "quotes ' quotes" | base64`}" }, - { "name": "D", "value": "#{`echo '$PATH:/etc/a' | base64`}" } + { "name": "A", "value": "#{`echo "hello" | base64`.strip}" }, + { "name": "B", "value": "#{`echo "how are you?" | base64`.strip}" }, + { "name": "C", "value": "#{`echo "quotes ' quotes" | base64`.strip}" }, + { "name": "D", "value": "#{`echo '$PATH:/etc/a' | base64`.strip}" } ], "files": [], @@ -26,9 +26,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/shell/epilogue_on_fail.rb b/test/e2e/shell/epilogue_on_fail.rb index 244c4450..b963cd34 100644 --- a/test/e2e/shell/epilogue_on_fail.rb +++ b/test/e2e/shell/epilogue_on_fail.rb @@ -28,9 +28,10 @@ ], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/shell/epilogue_on_pass.rb b/test/e2e/shell/epilogue_on_pass.rb index ee0c7404..5122ebbf 100644 --- a/test/e2e/shell/epilogue_on_pass.rb +++ b/test/e2e/shell/epilogue_on_pass.rb @@ -28,9 +28,10 @@ ], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/shell/failed_job.rb b/test/e2e/shell/failed_job.rb index 459428a3..c6f128e6 100644 --- a/test/e2e/shell/failed_job.rb +++ b/test/e2e/shell/failed_job.rb @@ -18,9 +18,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/shell/file_injection.rb b/test/e2e/shell/file_injection.rb index 1036e6ba..d578049a 100644 --- a/test/e2e/shell/file_injection.rb +++ b/test/e2e/shell/file_injection.rb @@ -10,9 +10,9 @@ "env_vars": [], "files": [ - { "path": "test.txt", "content": "#{`echo "hello" | base64`}", "mode": "0644" }, - { "path": "/a/b/c", "content": "#{`echo "hello" | base64`}", "mode": "0644" }, - { "path": "/tmp/a", "content": "#{`echo "hello" | base64`}", "mode": "+x" } + { "path": "test.txt", "content": "#{`echo "hello" | base64`.strip}", "mode": "0644" }, + { "path": "/a/b/c", "content": "#{`echo "hello" | base64`.strip}", "mode": "0644" }, + { "path": "/tmp/a", "content": "#{`echo "hello" | base64`.strip}", "mode": "+x" } ], "commands": [ @@ -24,9 +24,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/shell/file_injection_broken_file_mode.rb b/test/e2e/shell/file_injection_broken_file_mode.rb index 69b60eaf..e7e30046 100644 --- a/test/e2e/shell/file_injection_broken_file_mode.rb +++ b/test/e2e/shell/file_injection_broken_file_mode.rb @@ -10,7 +10,7 @@ "env_vars": [], "files": [ - { "path": "test.txt", "content": "#{`echo "hello" | base64`}", "mode": "obviously broken" } + { "path": "test.txt", "content": "#{`echo "hello" | base64`.strip}", "mode": "obviously broken" } ], "commands": [ @@ -22,9 +22,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/shell/hello_world.rb b/test/e2e/shell/hello_world.rb index 4b49b7fa..c6b2b8b8 100644 --- a/test/e2e/shell/hello_world.rb +++ b/test/e2e/shell/hello_world.rb @@ -18,9 +18,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/shell/job_stopping.rb b/test/e2e/shell/job_stopping.rb index 99f28735..74a69153 100644 --- a/test/e2e/shell/job_stopping.rb +++ b/test/e2e/shell/job_stopping.rb @@ -19,9 +19,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/shell/job_stopping_on_epilogue.rb b/test/e2e/shell/job_stopping_on_epilogue.rb new file mode 100644 index 00000000..4fe622fa --- /dev/null +++ b/test/e2e/shell/job_stopping_on_epilogue.rb @@ -0,0 +1,52 @@ +#!/bin/ruby +# rubocop:disable all + +require_relative '../../e2e' + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + + "env_vars": [], + + "files": [], + + "commands": [ + { "directive": "echo 'here'" } + ], + + "epilogue_always_commands": [ + { "directive": "sleep infinity" } + ], + + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_command_to_start("sleep infinity") + +sleep 1 + +stop_job + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"echo 'here'"} + {"event":"cmd_output", "timestamp":"*", "output":"here\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo 'here'","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} + {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"sleep infinity"} + {"event":"cmd_finished", "timestamp":"*", "directive":"sleep infinity","exit_code":1,"finished_at":"*","started_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"stopped"} +LOG diff --git a/test/e2e/shell/killing_root_bash.rb b/test/e2e/shell/killing_root_bash.rb index 7f3719bf..71be096d 100644 --- a/test/e2e/shell/killing_root_bash.rb +++ b/test/e2e/shell/killing_root_bash.rb @@ -28,9 +28,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/shell/set_e.rb b/test/e2e/shell/set_e.rb index 513783f1..e22d6fe4 100644 --- a/test/e2e/shell/set_e.rb +++ b/test/e2e/shell/set_e.rb @@ -30,9 +30,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/shell/set_pipefail.rb b/test/e2e/shell/set_pipefail.rb index fb6608e8..932dc0ce 100644 --- a/test/e2e/shell/set_pipefail.rb +++ b/test/e2e/shell/set_pipefail.rb @@ -30,9 +30,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/shell/ssh_jump_points.rb b/test/e2e/shell/ssh_jump_points.rb index c99fc181..d2357450 100644 --- a/test/e2e/shell/ssh_jump_points.rb +++ b/test/e2e/shell/ssh_jump_points.rb @@ -54,9 +54,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/shell/stty_restoration.rb b/test/e2e/shell/stty_restoration.rb index ae9f04df..002f5554 100644 --- a/test/e2e/shell/stty_restoration.rb +++ b/test/e2e/shell/stty_restoration.rb @@ -19,9 +19,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/shell/unicode.rb b/test/e2e/shell/unicode.rb index 189bd279..9a54f627 100644 --- a/test/e2e/shell/unicode.rb +++ b/test/e2e/shell/unicode.rb @@ -33,9 +33,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e/shell/unknown_command.rb b/test/e2e/shell/unknown_command.rb index 3faea64e..fd419e99 100644 --- a/test/e2e/shell/unknown_command.rb +++ b/test/e2e/shell/unknown_command.rb @@ -18,9 +18,10 @@ "epilogue_always_commands": [], "callbacks": { - "finished": "https://httpbin.org/status/200", - "teardown_finished": "https://httpbin.org/status/200" - } + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} } JSON diff --git a/test/e2e_support/api_mode.rb b/test/e2e_support/api_mode.rb new file mode 100644 index 00000000..2454bdf1 --- /dev/null +++ b/test/e2e_support/api_mode.rb @@ -0,0 +1,181 @@ +# rubocop:disable all + +$AGENT_PORT_IN_TESTS = 30000 +$AGENT_SSH_PORT_IN_TESTS = 2222 + +# based on secret passed to the running server +$TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.gLEycyHdyRRzUpauBxdDFmxT5KoOApFO5MHuvWPgFtY" + +class ApiMode + def boot_up_agent + system "docker stop $(docker ps -q)" + system "docker rm $(docker ps -qa)" + system "docker build -t agent -f Dockerfile.test ." + system "docker run --privileged --device /dev/ptmx -v /tmp/agent-temp-directory/:/tmp/agent-temp-directory -v /var/run/docker.sock:/var/run/docker.sock -p #{$AGENT_PORT_IN_TESTS}:8000 -p #{$AGENT_SSH_PORT_IN_TESTS}:22 --name agent -tdi agent bash -c \"service ssh restart && nohup ./agent serve --auth-token-secret 'TzRVcspTmxhM9fUkdi1T/0kVXNETCi8UdZ8dLM8va4E' & sleep infinity\"" + + pingable = nil + until pingable + puts "Waiting for agent to start" + + `curl -H "Authorization: Bearer #{$TOKEN}" --fail -X GET -k "https://0.0.0.0:30000/is_alive"` + + pingable = ($?.exitstatus == 0) + end + end + + def start_job(request) + r = Tempfile.new + r.write(request) + r.close + + puts "============================" + puts "Sending job request to Agent" + + output = `curl -H "Authorization: Bearer #{$TOKEN}" --fail -X POST -k "https://0.0.0.0:30000/jobs" --data @#{r.path}` + + abort "Failed to send: #{output}" if $?.exitstatus != 0 + end + + def stop_job + puts "============================" + puts "Stopping job..." + + output = `curl -H "Authorization: Bearer #{$TOKEN}" --fail -X POST -k "https://0.0.0.0:30000/jobs/terminate"` + + abort "Failed to stob job: #{output}" if $?.exitstatus != 0 + end + + def wait_for_command_to_start(cmd) + puts "=========================" + puts "Waiting for command to start '#{cmd}'" + + Timeout.timeout(60 * 2) do + loop do + `curl -H "Authorization: Bearer #{$TOKEN}" --fail -k "https://0.0.0.0:30000/job_logs" | grep "#{cmd}"` + + if $?.exitstatus == 0 + break + else + sleep 1 + end + end + end + end + + def wait_for_job_to_finish + puts "=========================" + puts "Waiting for job to finish" + + Timeout.timeout(60 * 2) do + loop do + `curl -H "Authorization: Bearer #{$TOKEN}" --fail -k "https://0.0.0.0:30000/job_logs" | grep "job_finished"` + + if $?.exitstatus == 0 + break + else + sleep 1 + end + end + end + end + + def finished_callback_url + "https://httpbin.org/status/200" + end + + def teardown_callback_url + "https://httpbin.org/status/200" + end + + def assert_job_log(expected_log) + puts "=========================" + puts "Asserting Job Logs" + + actual_log = `curl -H "Authorization: Bearer #{$TOKEN}" -k "https://0.0.0.0:30000/jobs/#{$JOB_ID}/log"` + + puts "-----------------------------------" + puts actual_log + puts "-----------------------------------" + + abort "Failed to fetch logs: #{actual_log}" if $?.exitstatus != 0 + + actual_log = actual_log.split("\n").map(&:strip).reject(&:empty?) + expected_log = expected_log.split("\n").map(&:strip).reject(&:empty?) + + index_in_actual_logs = 0 + index_in_expected_logs = 0 + + while index_in_actual_logs < actual_log.length && index_in_expected_logs < expected_log.length + begin + puts "Comparing log lines Actual=#{index_in_actual_logs} Expected=#{index_in_expected_logs}" + + expected_log_line = expected_log[index_in_expected_logs] + actual_log_line = actual_log[index_in_actual_logs] + + puts " actual: #{actual_log_line}" + puts " expected: #{expected_log_line}" + + actual_log_line_json = Hash[JSON.parse(actual_log_line).sort] + + if expected_log_line =~ /\*\*\* LONG_OUTPUT \*\*\*/ + if actual_log_line_json["event"] == "cmd_output" + # if we have a *** LONG_OUTPUT *** marker + + # we go to next actual log line + # but we stay on the same expected log line + + index_in_actual_logs += 1 + + next + else + # end of the LONG_OUTPUT marker, we increase the expected log line + # and in the next iteration we will compare regularly again + index_in_expected_logs += 1 + + next + end + else + # if there is no marker, we compare the JSONs + # we ignore the timestamps because they change every time + + expected_log_line_json = Hash[JSON.parse(expected_log_line).sort] + + if expected_log_line_json.keys != actual_log_line_json.keys + abort "(fail) JSON keys are different." + end + + expected_log_line_json.keys.each do |key| + # Special case when we want to ignore only the output + # ignore expected entries with '*' + + next if expected_log_line_json[key] == "*" + + if expected_log_line_json[key] != actual_log_line_json[key] + abort "(fail) Values for '#{key}' are not equal." + end + end + + index_in_actual_logs += 1 + index_in_expected_logs += 1 + end + + puts "success" + rescue + puts "" + puts "Line Number: Actual=#{index_in_actual_logs} Expected=#{index_in_expected_logs}" + puts "Expected: '#{expected_log_line}'" + puts "Actual: '#{actual_log_line}'" + + abort "(fail) Failed to parse log line" + end + end + + if index_in_actual_logs != actual_log.length + abort "(fail) There are unchecked log lines from the actual log" + end + + if index_in_expected_logs != expected_log.length + abort "(fail) There are unchecked log lines from the expected log" + end + end +end diff --git a/test/e2e_support/docker-compose-listen.yml b/test/e2e_support/docker-compose-listen.yml new file mode 100644 index 00000000..e4b2574e --- /dev/null +++ b/test/e2e_support/docker-compose-listen.yml @@ -0,0 +1,35 @@ +version: '3.0' + +services: + agent: + build: + context: ../.. + dockerfile: Dockerfile.test + + command: 'bash -c "service ssh restart && ./agent start --config-file /tmp/agent/config.yaml"' + + ports: + - "30000:8000" + - "2222:22" + + links: + - hub:hub + + devices: + - /dev/ptmx + + volumes: + - /tmp/agent:/tmp/agent + - /tmp/agent-temp-directory:/tmp/agent-temp-directory + - /var/run/docker.sock:/var/run/docker.sock + + hub: + build: + context: ../hub_reference + dockerfile: Dockerfile + + ports: + - "4567:4567" + + volumes: + - ../hub_reference:/app diff --git a/test/e2e_support/listener_mode.rb b/test/e2e_support/listener_mode.rb new file mode 100644 index 00000000..3fa11612 --- /dev/null +++ b/test/e2e_support/listener_mode.rb @@ -0,0 +1,233 @@ +# rubocop:disable all + +class ListenerMode + + HUB_ENDPOINT = "http://localhost:4567" + + def boot_up_agent + File.write("/tmp/agent/config.yaml", $AGENT_CONFIG.to_yaml) + system "docker-compose -f test/e2e_support/docker-compose-listen.yml stop" + system "docker-compose -f test/e2e_support/docker-compose-listen.yml build" + system "docker-compose -f test/e2e_support/docker-compose-listen.yml up -d" + + wait_for_agent_to_register_in_the_hub + end + + def start_job(request) + File.write("/tmp/j1", request.to_json) + + system "curl -X POST -H 'Content-Type: application/json' -d @/tmp/j1 #{HUB_ENDPOINT}/private/schedule_job" + end + + def shutdown_agent + system "curl -X POST #{HUB_ENDPOINT}/private/schedule_shutdown" + end + + def wait_for_command_to_start(cmd) + puts "=========================" + puts "Waiting for command to start '#{cmd}'" + + Timeout.timeout(60 * 2) do + loop do + `curl -s #{HUB_ENDPOINT}/private/jobs/#{$JOB_ID}/logs | grep "#{cmd}"` + + if $?.exitstatus == 0 + break + else + sleep 1 + end + end + end + end + + def wait_for_job_to_get_stuck + puts "" + puts "Waiting for job to get stuck" + + loop do + response = `curl -s --fail -X GET -k "#{HUB_ENDPOINT}/api/v1/self_hosted_agents/jobs/#{$JOB_ID}/status"`.strip + puts "Job state #{response}" + + if response == "stuck" + return + else + sleep 1 + end + end + + sleep 5 + + puts + end + + def wait_for_agent_to_shutdown + puts "" + puts "Waiting for agent to shutdown" + + loop do + response = `curl -s --fail -X GET -k "#{HUB_ENDPOINT}/api/v1/self_hosted_agents/is_shutdown"`.strip + puts "Agent is shutdown: #{response}" + + if response == "true" + return + else + sleep 1 + end + end + + sleep 5 + + puts + end + + def wait_for_job_to_finish + puts "" + puts "Waiting for job to finish" + + loop do + response = `curl -s --fail -X GET -k "#{HUB_ENDPOINT}/api/v1/self_hosted_agents/jobs/#{$JOB_ID}/status"`.strip + puts "Job state #{response}" + + if response == "finished" + return + else + sleep 1 + end + end + + sleep 5 + + puts + end + + def stop_job + puts "Stopping job" + + system "curl -H --fail -X POST -k --data '' #{HUB_ENDPOINT}/private/schedule_stop/#{$JOB_ID}" + + puts 5 + end + + def assert_job_log(expected_log) + puts "=========================" + puts "Asserting Job Logs" + + sleep 10 + + actual_log = `curl --fail -s #{HUB_ENDPOINT}/private/jobs/#{$JOB_ID}/logs` + + puts "-----------------------------------" + puts actual_log + puts "-----------------------------------" + + abort "Failed to fetch logs: #{actual_log}" if $?.exitstatus != 0 + + actual_log = actual_log.split("\n").map(&:strip).reject(&:empty?) + expected_log = expected_log.split("\n").map(&:strip).reject(&:empty?) + + index_in_actual_logs = 0 + index_in_expected_logs = 0 + + while index_in_actual_logs < actual_log.length && index_in_expected_logs < expected_log.length + begin + puts "Comparing log lines Actual=#{index_in_actual_logs} Expected=#{index_in_expected_logs}" + + expected_log_line = expected_log[index_in_expected_logs] + actual_log_line = actual_log[index_in_actual_logs] + + puts " actual: #{actual_log_line}" + puts " expected: #{expected_log_line}" + + actual_log_line_json = Hash[JSON.parse(actual_log_line).sort] + + if expected_log_line =~ /\*\*\* LONG_OUTPUT \*\*\*/ + if actual_log_line_json["event"] == "cmd_output" + # if we have a *** LONG_OUTPUT *** marker + + # we go to next actual log line + # but we stay on the same expected log line + + index_in_actual_logs += 1 + + next + else + # end of the LONG_OUTPUT marker, we increase the expected log line + # and in the next iteration we will compare regularly again + index_in_expected_logs += 1 + + next + end + else + # if there is no marker, we compare the JSONs + # we ignore the timestamps because they change every time + + expected_log_line_json = Hash[JSON.parse(expected_log_line).sort] + + if expected_log_line_json.keys != actual_log_line_json.keys + abort "(fail) JSON keys are different." + end + + expected_log_line_json.keys.each do |key| + # Special case when we want to ignore only the output + # ignore expected entries with '*' + + next if expected_log_line_json[key] == "*" + + if expected_log_line_json[key] != actual_log_line_json[key] + abort "(fail) Values for '#{key}' are not equal." + end + end + + index_in_actual_logs += 1 + index_in_expected_logs += 1 + end + + puts "success" + rescue + puts "" + puts "Line Number: Actual=#{index_in_actual_logs} Expected=#{index_in_expected_logs}" + puts "Expected: '#{expected_log_line}'" + puts "Actual: '#{actual_log_line}'" + + abort "(fail) Failed to parse log line" + end + end + + if index_in_actual_logs != actual_log.length + abort "(fail) There are unchecked log lines from the actual log" + end + + if index_in_expected_logs != expected_log.length + abort "(fail) There are unchecked log lines from the expected log" + end + end + + def finished_callback_url + "http://hub:4567/jobs/#{$JOB_ID}/callbacks/finished" + end + + def teardown_callback_url + "http://hub:4567/jobs/#{$JOB_ID}/callbacks/finished" + end + + private + + def wait_for_agent_to_register_in_the_hub + puts "Waiting for agent to register in the hub " + + loop do + print "." + + response = `curl -s --fail -X GET -k "#{HUB_ENDPOINT}/private/is_registered"` + + if response == "yes" + break + else + sleep 1 + end + end + + puts + end + +end diff --git a/test/hub_reference/.gitignore b/test/hub_reference/.gitignore new file mode 100644 index 00000000..627b10ec --- /dev/null +++ b/test/hub_reference/.gitignore @@ -0,0 +1 @@ +vendor/bundle diff --git a/test/hub_reference/Dockerfile b/test/hub_reference/Dockerfile new file mode 100644 index 00000000..de94b69f --- /dev/null +++ b/test/hub_reference/Dockerfile @@ -0,0 +1,5 @@ +FROM ruby:2.7 + +WORKDIR /app + +CMD bundle config set path 'vendor/bundle' && bundle install && bundle exec ruby app.rb diff --git a/test/hub_reference/Gemfile b/test/hub_reference/Gemfile new file mode 100644 index 00000000..e03a992e --- /dev/null +++ b/test/hub_reference/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +gem 'sinatra' +gem 'thin' diff --git a/test/hub_reference/Gemfile.lock b/test/hub_reference/Gemfile.lock new file mode 100644 index 00000000..43fbd2f2 --- /dev/null +++ b/test/hub_reference/Gemfile.lock @@ -0,0 +1,31 @@ +GEM + remote: https://rubygems.org/ + specs: + daemons (1.4.0) + eventmachine (1.2.7) + mustermann (1.1.1) + ruby2_keywords (~> 0.0.1) + rack (2.2.3) + rack-protection (2.1.0) + rack + ruby2_keywords (0.0.4) + sinatra (2.1.0) + mustermann (~> 1.0) + rack (~> 2.2) + rack-protection (= 2.1.0) + tilt (~> 2.0) + thin (1.8.1) + daemons (~> 1.0, >= 1.0.9) + eventmachine (~> 1.0, >= 1.0.4) + rack (>= 1, < 3) + tilt (2.0.10) + +PLATFORMS + ruby + +DEPENDENCIES + sinatra + thin + +BUNDLED WITH + 2.1.4 diff --git a/test/hub_reference/app.rb b/test/hub_reference/app.rb new file mode 100644 index 00000000..b9a083f4 --- /dev/null +++ b/test/hub_reference/app.rb @@ -0,0 +1,172 @@ +# rubocop:disable all + +require "sinatra" +require "json" + +$stdout.sync = true + +set :bind, "0.0.0.0" +set :logging, false + +$registered = false +$disconnected = false +$should_shutdown = false +$jobs = [] +$payloads = {} +$job_states = {} +$finished = {} +$teardown = {} +$logs = [] + +before do + logger.level = 0 + + begin + request.body.rewind + + @json_request = JSON.parse(request.body.read) + rescue StandardError => e + end +end + +# +# The official API that is used by the agent to +# connect to Semaphore 2.0 +# + +post "/api/v1/self_hosted_agents/register" do + puts "[SYNC] Registration received" + $registered = true + + { + "access_token" => "dsjfaklsd123412341", + }.to_json +end + +post "/api/v1/self_hosted_agents/disconnect" do + puts "[SYNC] Disconnect received" + $disconnected = true +end + +post "/api/v1/self_hosted_agents/sync" do + puts "[SYNC] Request #{@json_request.to_json}" + + response = case @json_request["state"] + when "waiting-for-jobs" + if $should_shutdown + {"action" => "shutdown"} + elsif $jobs.size > 0 + job = $jobs.shift + + {"action" => "run-job", "job_id" => job["id"]} + else + {"action" => "continue"} + end + when "running-job" + job_id = @json_request["job_id"] + if $should_shutdown || $job_states[job_id] == "stopping" + {"action" => "stop-job"} + else + {"action" => "continue"} + end + when "stopping-job" + {"action" => "continue"} + when "finished-job" + $should_shutdown ? {"action" => "shutdown"} : {"action" => "continue"} + when "starting-job" + {"action" => "continue"} + when "failed-to-send-callback" + job_id = @json_request["job_id"] + $job_states[job_id] = "stuck" + $should_shutdown ? {"action" => "shutdown"} : {"action" => "continue"} + when "failed-to-fetch-job" + job_id = @json_request["job_id"] + $job_states[job_id] = "stuck" + $should_shutdown ? {"action" => "shutdown"} : {"action" => "continue"} + when "failed-to-construct-job" + $job_states[job_id] = "stuck" + $should_shutdown ? {"action" => "shutdown"} : {"action" => "continue"} + else + raise "unknown state" + end + + puts "[SYNC] Response #{response.to_json}" + response.to_json +end + +get "/api/v1/self_hosted_agents/jobs/:id" do + job_id = params["id"] + + if job_id == "bad-job-id" + halt 500, "error" + else + $payloads[params["id"]].to_json + end +end + +get "/api/v1/self_hosted_agents/jobs/:id/status" do + $job_states[params["id"]] +end + +get "/api/v1/self_hosted_agents/is_shutdown" do + "#{$disconnected}" +end + +post "/api/v1/logs/:id" do + request.body.rewind + events = request.body.read.split("\n") + + puts "Received #{events.length()} log events" + $logs += events + status 200 +end + +post "/jobs/:id/callbacks/finished" do + puts "[CALLBACK] Finished job #{params["id"]}" + $job_states[params["id"]] = "finished" +end + +post "/jobs/:id/callbacks/teardown" do + $teardown[params["id"]] = true +end + +# +# Private APIs. Only needed to contoll the flow +# of e2e tests in the Agent. +# + +get "/is_alive" do + "yes" +end + +get "/private/is_registered" do + $registered ? "yes" : "no" +end + +get "/private/jobs/:id/logs" do + puts "Fetching logs" + puts $logs.join("\n") + $logs.join("\n") +end + +post "/private/schedule_job" do + job = JSON.parse(@json_request) + puts "[PRIVATE] Scheduling job #{job["id"]}" + + puts "Scheduled job #{job["id"]}" + + $jobs << job + $payloads[job["id"]] = job + $job_states[job["id"]] = "running" +end + +post "/private/schedule_stop/:id" do + puts "Scheduled stop #{params["id"]}" + + $job_states[params["id"]] = "stopping" +end + +post "/private/schedule_shutdown" do + puts "Scheduled shutdown" + $should_shutdown = true +end From 88f2fc5c0847379e8cf13e2bc20df658af6faadf Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Fri, 12 Nov 2021 09:47:04 -0300 Subject: [PATCH 004/130] Use SEMAPHORE_AGENT_LOG_LEVEL (#134) --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 869c36a7..41cb1d89 100644 --- a/main.go +++ b/main.go @@ -72,7 +72,7 @@ func OpenLogfile() io.Writer { } func getLogLevel() log.Level { - logLevel := os.Getenv("LOG_LEVEL") + logLevel := os.Getenv("SEMAPHORE_AGENT_LOG_LEVEL") if logLevel == "" { return log.InfoLevel } From d21ce66d3d2aa9bb7c735a8a51eb135f8c0c756a Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Thu, 18 Nov 2021 15:18:53 -0300 Subject: [PATCH 005/130] Use github-release-bot-agent secret for release (#135) --- .semaphore/release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.semaphore/release.yml b/.semaphore/release.yml index 8339a26f..ab09c062 100644 --- a/.semaphore/release.yml +++ b/.semaphore/release.yml @@ -11,7 +11,7 @@ blocks: - name: GO111MODULE value: "on" secrets: - - name: sem-robot-ghtoken + - name: github-release-bot-agent prologue: commands: - sem-version go 1.16 @@ -21,4 +21,5 @@ blocks: jobs: - name: Sem Agent commands: + - export GITHUB_TOKEN=$ACCESS_TOKEN - curl -sL https://git.io/goreleaser | bash -s -- --rm-dist From 4e3453f411ad31f66e397788668661a9d1c1176b Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Tue, 23 Nov 2021 14:56:57 -0300 Subject: [PATCH 006/130] Set commit_author for goreleaser (#137) --- .goreleaser.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.goreleaser.yml b/.goreleaser.yml index 11d203fd..218969b4 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -43,6 +43,9 @@ brews: - tap: owner: semaphoreci name: homebrew-tap + commit_author: + name: release-bot-agent + email: contact+release-bot-agent@renderedtext.com folder: Formula homepage: https://semaphoreci.com description: Semaphore 2.0 agent. From 473d5f2667dcb08146430e17074d77f55b10f621 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Fri, 3 Dec 2021 15:58:40 -0300 Subject: [PATCH 007/130] Include additional env vars on installation script (#138) --- install.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/install.sh b/install.sh index 4cc30af3..91bf62e7 100755 --- a/install.sh +++ b/install.sh @@ -4,7 +4,6 @@ set -e set -o pipefail AGENT_INSTALLATION_DIRECTORY="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" -LOGGED_IN_USER=$(logname) if [[ "$EUID" -ne 0 ]]; then echo "Please run with sudo." @@ -28,6 +27,7 @@ if [[ -z $SEMAPHORE_REGISTRATION_TOKEN ]]; then fi if [[ -z $SEMAPHORE_AGENT_INSTALLATION_USER ]]; then + LOGGED_IN_USER=$(logname) read -p "Enter user [$LOGGED_IN_USER]: " SEMAPHORE_AGENT_INSTALLATION_USER SEMAPHORE_AGENT_INSTALLATION_USER="${SEMAPHORE_AGENT_INSTALLATION_USER:=$LOGGED_IN_USER}" fi @@ -61,12 +61,13 @@ rm toolbox.tar # # Create agent config # +SEMAPHORE_AGENT_DISCONNECT_AFTER_JOB=${SEMAPHORE_AGENT_DISCONNECT_AFTER_JOB:-false} AGENT_CONFIG=$(cat <<-END endpoint: "$SEMAPHORE_ORGANIZATION.semaphoreci.com" token: "$SEMAPHORE_REGISTRATION_TOKEN" no-https: false -shutdown-hook-path: "" -disconnect-after-job: false +shutdown-hook-path: "$SEMAPHORE_AGENT_SHUTDOWN_HOOK" +disconnect-after-job: $SEMAPHORE_AGENT_DISCONNECT_AFTER_JOB env-vars: [] files: [] fail-on-missing-files: false From 1f67db94ac29b0f2460992e9d6401dc92050b029 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Tue, 14 Dec 2021 13:53:16 -0300 Subject: [PATCH 008/130] Shutdown agent after idle timeout (#140) --- .semaphore/semaphore.yml | 1 + install.sh | 2 + main.go | 27 ++++-- pkg/config/config.go | 20 ++-- pkg/listener/job_processor.go | 126 +++++++++++++++++--------- pkg/listener/listener.go | 21 +++-- pkg/listener/shutdown_reason.go | 27 ++++++ test/e2e/self-hosted/shutdown_idle.rb | 34 +++++++ test/hub_reference/app.rb | 2 +- 9 files changed, 185 insertions(+), 75 deletions(-) create mode 100644 pkg/listener/shutdown_reason.go create mode 100644 test/e2e/self-hosted/shutdown_idle.rb diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 07c14075..f4a21bed 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -210,6 +210,7 @@ blocks: - docker_compose_fail_on_missing_host_files - shell_host_env_vars - shutdown + - shutdown_idle - shutdown_while_waiting promotions: diff --git a/install.sh b/install.sh index 91bf62e7..274b4958 100755 --- a/install.sh +++ b/install.sh @@ -62,12 +62,14 @@ rm toolbox.tar # Create agent config # SEMAPHORE_AGENT_DISCONNECT_AFTER_JOB=${SEMAPHORE_AGENT_DISCONNECT_AFTER_JOB:-false} +SEMAPHORE_AGENT_DISCONNECT_AFTER_IDLE_TIMEOUT=${SEMAPHORE_AGENT_DISCONNECT_AFTER_IDLE_TIMEOUT:-0} AGENT_CONFIG=$(cat <<-END endpoint: "$SEMAPHORE_ORGANIZATION.semaphoreci.com" token: "$SEMAPHORE_REGISTRATION_TOKEN" no-https: false shutdown-hook-path: "$SEMAPHORE_AGENT_SHUTDOWN_HOOK" disconnect-after-job: $SEMAPHORE_AGENT_DISCONNECT_AFTER_JOB +disconnect-after-idle-timeout: $SEMAPHORE_AGENT_DISCONNECT_AFTER_IDLE_TIMEOUT env-vars: [] files: [] fail-on-missing-files: false diff --git a/main.go b/main.go index 41cb1d89..e4d82ed7 100644 --- a/main.go +++ b/main.go @@ -92,6 +92,7 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { _ = pflag.Bool(config.NoHTTPS, false, "Use http for communication") _ = pflag.String(config.ShutdownHookPath, "", "Shutdown hook path") _ = pflag.Bool(config.DisconnectAfterJob, false, "Disconnect after job") + _ = pflag.Int(config.DisconnectAfterIdleTimeout, 0, "Disconnect after idle timeout, in seconds") _ = pflag.StringSlice(config.EnvVars, []string{}, "Export environment variables in jobs") _ = pflag.StringSlice(config.Files, []string{}, "Inject files into container, when using docker compose executor") _ = pflag.Bool(config.FailOnMissingFiles, false, "Fail job if files specified using --files are missing") @@ -114,6 +115,10 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { log.Fatal("Agent registration token was not specified. Exiting...") } + if viper.GetInt(config.DisconnectAfterIdleTimeout) < 0 { + log.Fatal("Idle timeout can't be negative. Exiting...") + } + scheme := "https" if viper.GetBool(config.NoHTTPS) { scheme = "http" @@ -129,17 +134,19 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { log.Fatalf("Error parsing --files: %v", err) } + idleTimeout := viper.GetInt(config.DisconnectAfterIdleTimeout) config := listener.Config{ - Endpoint: viper.GetString(config.Endpoint), - Token: viper.GetString(config.Token), - RegisterRetryLimit: 30, - Scheme: scheme, - ShutdownHookPath: viper.GetString(config.ShutdownHookPath), - DisconnectAfterJob: viper.GetBool(config.DisconnectAfterJob), - EnvVars: hostEnvVars, - FileInjections: fileInjections, - FailOnMissingFiles: viper.GetBool(config.FailOnMissingFiles), - AgentVersion: VERSION, + Endpoint: viper.GetString(config.Endpoint), + Token: viper.GetString(config.Token), + RegisterRetryLimit: 30, + Scheme: scheme, + ShutdownHookPath: viper.GetString(config.ShutdownHookPath), + DisconnectAfterJob: viper.GetBool(config.DisconnectAfterJob), + DisconnectAfterIdleTimeout: time.Duration(int64(idleTimeout) * int64(time.Second)), + EnvVars: hostEnvVars, + FileInjections: fileInjections, + FailOnMissingFiles: viper.GetBool(config.FailOnMissingFiles), + AgentVersion: VERSION, } go func() { diff --git a/pkg/config/config.go b/pkg/config/config.go index 6a1a1d7d..09d881fb 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -3,15 +3,16 @@ package config import "os" const ( - ConfigFile = "config-file" - Endpoint = "endpoint" - Token = "token" - NoHTTPS = "no-https" - ShutdownHookPath = "shutdown-hook-path" - DisconnectAfterJob = "disconnect-after-job" - EnvVars = "env-vars" - Files = "files" - FailOnMissingFiles = "fail-on-missing-files" + ConfigFile = "config-file" + Endpoint = "endpoint" + Token = "token" + NoHTTPS = "no-https" + ShutdownHookPath = "shutdown-hook-path" + DisconnectAfterJob = "disconnect-after-job" + DisconnectAfterIdleTimeout = "disconnect-after-idle-timeout" + EnvVars = "env-vars" + Files = "files" + FailOnMissingFiles = "fail-on-missing-files" ) var ValidConfigKeys = []string{ @@ -21,6 +22,7 @@ var ValidConfigKeys = []string{ NoHTTPS, ShutdownHookPath, DisconnectAfterJob, + DisconnectAfterIdleTimeout, EnvVars, Files, FailOnMissingFiles, diff --git a/pkg/listener/job_processor.go b/pkg/listener/job_processor.go index 2c242221..83b8ae13 100644 --- a/pkg/listener/job_processor.go +++ b/pkg/listener/job_processor.go @@ -1,6 +1,7 @@ package listener import ( + "fmt" "net/http" "os" "os/exec" @@ -18,17 +19,19 @@ import ( func StartJobProcessor(httpClient *http.Client, apiClient *selfhostedapi.API, config Config) (*JobProcessor, error) { p := &JobProcessor{ - HTTPClient: httpClient, - APIClient: apiClient, - LastSuccessfulSync: time.Now(), - State: selfhostedapi.AgentStateWaitingForJobs, - SyncInterval: 5 * time.Second, - DisconnectRetryAttempts: 100, - ShutdownHookPath: config.ShutdownHookPath, - DisconnectAfterJob: config.DisconnectAfterJob, - EnvVars: config.EnvVars, - FileInjections: config.FileInjections, - FailOnMissingFiles: config.FailOnMissingFiles, + HTTPClient: httpClient, + APIClient: apiClient, + LastSuccessfulSync: time.Now(), + LastStateChangeAt: time.Now(), + State: selfhostedapi.AgentStateWaitingForJobs, + SyncInterval: 5 * time.Second, + DisconnectRetryAttempts: 100, + ShutdownHookPath: config.ShutdownHookPath, + DisconnectAfterJob: config.DisconnectAfterJob, + DisconnectAfterIdleTimeout: config.DisconnectAfterIdleTimeout, + EnvVars: config.EnvVars, + FileInjections: config.FileInjections, + FailOnMissingFiles: config.FailOnMissingFiles, } go p.Start() @@ -39,21 +42,23 @@ func StartJobProcessor(httpClient *http.Client, apiClient *selfhostedapi.API, co } type JobProcessor struct { - HTTPClient *http.Client - APIClient *selfhostedapi.API - State selfhostedapi.AgentState - CurrentJobID string - CurrentJob *jobs.Job - SyncInterval time.Duration - LastSyncErrorAt *time.Time - LastSuccessfulSync time.Time - DisconnectRetryAttempts int - ShutdownHookPath string - StopSync bool - DisconnectAfterJob bool - EnvVars []config.HostEnvVar - FileInjections []config.FileInjection - FailOnMissingFiles bool + HTTPClient *http.Client + APIClient *selfhostedapi.API + State selfhostedapi.AgentState + CurrentJobID string + CurrentJob *jobs.Job + SyncInterval time.Duration + LastSyncErrorAt *time.Time + LastSuccessfulSync time.Time + LastStateChangeAt time.Time + DisconnectRetryAttempts int + ShutdownHookPath string + StopSync bool + DisconnectAfterJob bool + DisconnectAfterIdleTimeout time.Duration + EnvVars []config.HostEnvVar + FileInjections []config.FileInjection + FailOnMissingFiles bool } func (p *JobProcessor) Start() { @@ -71,7 +76,34 @@ func (p *JobProcessor) SyncLoop() { } } +func (p *JobProcessor) isIdle() bool { + return p.State == selfhostedapi.AgentStateWaitingForJobs +} + +func (p *JobProcessor) setState(newState selfhostedapi.AgentState) { + p.State = newState + p.LastStateChangeAt = time.Now() +} + +func (p *JobProcessor) shutdownIfIdle() { + if !p.isIdle() { + return + } + + if p.DisconnectAfterIdleTimeout == 0 { + return + } + + idleFor := time.Since(p.LastStateChangeAt) + if idleFor > p.DisconnectAfterIdleTimeout { + log.Infof("Agent has been idle for the past %v.", idleFor) + p.Shutdown(ShutdownReasonIdle, 0) + } +} + func (p *JobProcessor) Sync() { + p.shutdownIfIdle() + request := &selfhostedapi.SyncRequest{ State: p.State, JobID: p.CurrentJobID, @@ -95,7 +127,8 @@ func (p *JobProcessor) HandleSyncError(err error) { p.LastSyncErrorAt = &now if time.Now().Add(-10 * time.Minute).After(p.LastSuccessfulSync) { - p.Shutdown("Unable to sync with Semaphore for over 10 minutes.", 1) + log.Error("Unable to sync with Semaphore for over 10 minutes.") + p.Shutdown(ShutdownReasonUnableToSync, 1) } } @@ -114,7 +147,8 @@ func (p *JobProcessor) ProcessSyncResponse(response *selfhostedapi.SyncResponse) return case selfhostedapi.AgentActionShutdown: - p.Shutdown("Agent Shutdown requested by Semaphore", 0) + log.Info("Agent shutdown requested by Semaphore") + p.Shutdown(ShutdownReasonRequested, 0) case selfhostedapi.AgentActionWaitForJobs: p.WaitForJobs() @@ -122,13 +156,13 @@ func (p *JobProcessor) ProcessSyncResponse(response *selfhostedapi.SyncResponse) } func (p *JobProcessor) RunJob(jobID string) { - p.State = selfhostedapi.AgentStateStartingJob + p.setState(selfhostedapi.AgentStateStartingJob) p.CurrentJobID = jobID jobRequest, err := p.getJobWithRetries(p.CurrentJobID) if err != nil { log.Errorf("Could not get job %s: %v", jobID, err) - p.State = selfhostedapi.AgentStateFailedToFetchJob + p.setState(selfhostedapi.AgentStateFailedToFetchJob) return } @@ -142,12 +176,11 @@ func (p *JobProcessor) RunJob(jobID string) { if err != nil { log.Errorf("Could not construct job %s: %v", jobID, err) - p.State = selfhostedapi.AgentStateFailedToConstructJob + p.setState(selfhostedapi.AgentStateFailedToConstructJob) return } - p.State = selfhostedapi.AgentStateRunningJob - p.CurrentJobID = jobID + p.setState(selfhostedapi.AgentStateRunningJob) p.CurrentJob = job go job.RunWithOptions(jobs.RunOptions{ @@ -156,9 +189,9 @@ func (p *JobProcessor) RunJob(jobID string) { OnSuccessfulTeardown: p.JobFinished, OnFailedTeardown: func() { if p.DisconnectAfterJob { - p.Shutdown("Job finished with error", 1) + p.Shutdown(ShutdownReasonJobFinished, 1) } else { - p.State = selfhostedapi.AgentStateFailedToSendCallback + p.setState(selfhostedapi.AgentStateFailedToSendCallback) } }, }) @@ -181,23 +214,23 @@ func (p *JobProcessor) getJobWithRetries(jobID string) (*api.JobRequest, error) func (p *JobProcessor) StopJob(jobID string) { p.CurrentJobID = jobID - p.State = selfhostedapi.AgentStateStoppingJob + p.setState(selfhostedapi.AgentStateStoppingJob) p.CurrentJob.Stop() } func (p *JobProcessor) JobFinished() { if p.DisconnectAfterJob { - p.Shutdown("Job finished", 0) + p.Shutdown(ShutdownReasonJobFinished, 0) } else { - p.State = selfhostedapi.AgentStateFinishedJob + p.setState(selfhostedapi.AgentStateFinishedJob) } } func (p *JobProcessor) WaitForJobs() { p.CurrentJobID = "" p.CurrentJob = nil - p.State = selfhostedapi.AgentStateWaitingForJobs + p.setState(selfhostedapi.AgentStateWaitingForJobs) } func (p *JobProcessor) SetupInteruptHandler() { @@ -205,7 +238,8 @@ func (p *JobProcessor) SetupInteruptHandler() { signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c - p.Shutdown("Ctrl+C pressed in Terminal", 0) + log.Info("Ctrl+C pressed in Terminal") + p.Shutdown(ShutdownReasonInterrupted, 0) }() } @@ -225,18 +259,20 @@ func (p *JobProcessor) disconnect() { } } -func (p *JobProcessor) Shutdown(reason string, code int) { +func (p *JobProcessor) Shutdown(reason ShutdownReason, code int) { p.disconnect() - p.executeShutdownHook() - log.Info(reason) - log.Info("Shutting down... Good bye!") + p.executeShutdownHook(reason) + log.Infof("Agent shutting down due to: %s", reason) os.Exit(code) } -func (p *JobProcessor) executeShutdownHook() { +func (p *JobProcessor) executeShutdownHook(reason ShutdownReason) { if p.ShutdownHookPath != "" { log.Infof("Executing shutdown hook from %s", p.ShutdownHookPath) cmd := exec.Command("bash", p.ShutdownHookPath) + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, fmt.Sprintf("SEMAPHORE_AGENT_SHUTDOWN_REASON=%s", reason)) + output, err := cmd.Output() if err != nil { log.Errorf("Error executing shutdown hook: %v", err) diff --git a/pkg/listener/listener.go b/pkg/listener/listener.go index 215089a7..ec560403 100644 --- a/pkg/listener/listener.go +++ b/pkg/listener/listener.go @@ -22,16 +22,17 @@ type Listener struct { } type Config struct { - Endpoint string - RegisterRetryLimit int - Token string - Scheme string - ShutdownHookPath string - DisconnectAfterJob bool - EnvVars []config.HostEnvVar - FileInjections []config.FileInjection - FailOnMissingFiles bool - AgentVersion string + Endpoint string + RegisterRetryLimit int + Token string + Scheme string + ShutdownHookPath string + DisconnectAfterJob bool + DisconnectAfterIdleTimeout time.Duration + EnvVars []config.HostEnvVar + FileInjections []config.FileInjection + FailOnMissingFiles bool + AgentVersion string } func Start(httpClient *http.Client, config Config, logger io.Writer) (*Listener, error) { diff --git a/pkg/listener/shutdown_reason.go b/pkg/listener/shutdown_reason.go new file mode 100644 index 00000000..5ad7ea52 --- /dev/null +++ b/pkg/listener/shutdown_reason.go @@ -0,0 +1,27 @@ +package listener + +type ShutdownReason int64 + +const ( + ShutdownReasonIdle ShutdownReason = iota + ShutdownReasonJobFinished + ShutdownReasonUnableToSync + ShutdownReasonRequested + ShutdownReasonInterrupted +) + +func (s ShutdownReason) String() string { + switch s { + case ShutdownReasonIdle: + return "IDLE" + case ShutdownReasonJobFinished: + return "JOB_FINISHED" + case ShutdownReasonUnableToSync: + return "UNABLE_TO_SYNC" + case ShutdownReasonRequested: + return "REQUESTED" + case ShutdownReasonInterrupted: + return "INTERRUPTED" + } + return "UNKNOWN" +} diff --git a/test/e2e/self-hosted/shutdown_idle.rb b/test/e2e/self-hosted/shutdown_idle.rb new file mode 100644 index 00000000..bcfafbad --- /dev/null +++ b/test/e2e/self-hosted/shutdown_idle.rb @@ -0,0 +1,34 @@ +$AGENT_CONFIG = { + "endpoint" => "hub:4567", + "token" => "321h1l2jkh1jk42341", + "no-https" => true, + "shutdown-hook-path" => "", + "disconnect-after-job" => false, + "disconnect-after-idle-timeout" => 30, + "env-vars" => [], + "files" => [], + "fail-on-missing-files" => false +} + +require_relative '../../e2e' + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + "env_vars": [], + "files": [], + "commands": [ + { "directive": "sleep 5" } + ], + + "epilogue_always_commands": [], + + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_agent_to_shutdown diff --git a/test/hub_reference/app.rb b/test/hub_reference/app.rb index b9a083f4..ccb08ea7 100644 --- a/test/hub_reference/app.rb +++ b/test/hub_reference/app.rb @@ -72,7 +72,7 @@ when "stopping-job" {"action" => "continue"} when "finished-job" - $should_shutdown ? {"action" => "shutdown"} : {"action" => "continue"} + $should_shutdown ? {"action" => "shutdown"} : {"action" => "wait-for-jobs"} when "starting-job" {"action" => "continue"} when "failed-to-send-callback" From d77e1a1e1a45a8a50709dea36682278d726de778 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Mon, 3 Jan 2022 14:09:21 -0300 Subject: [PATCH 009/130] Allow installation without agent start (#141) --- install.sh | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/install.sh b/install.sh index 274b4958..18a64dc0 100755 --- a/install.sh +++ b/install.sh @@ -113,12 +113,20 @@ if [[ -f "$SYSTEMD_SERVICE_PATH" ]]; then echo "systemd service already exists at $SYSTEMD_SERVICE_PATH. Overriding it..." echo "$SYSTEMD_SERVICE" > $SYSTEMD_SERVICE_PATH systemctl daemon-reload - echo "Restarting semaphore-agent service..." - systemctl restart semaphore-agent + if [[ "$SEMAPHORE_AGENT_START" == "false" ]]; then + echo "Not restarting agent." + else + echo "Restarting semaphore-agent service..." + systemctl restart semaphore-agent + fi else echo "$SYSTEMD_SERVICE" > $SYSTEMD_SERVICE_PATH - echo "Starting semaphore-agent service..." - systemctl start semaphore-agent + if [[ "$SEMAPHORE_AGENT_START" == "false" ]]; then + echo "Not starting agent." + else + echo "Starting semaphore-agent service..." + systemctl start semaphore-agent + fi fi echo "Done." \ No newline at end of file From 421892131f958a255057734872ce483f3ce614e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0ar=C4=8Devi=C4=87?= Date: Mon, 3 Jan 2022 18:09:45 +0100 Subject: [PATCH 010/130] Fix grammar mistake while failing to decode file content (#139) --- pkg/executors/docker_compose_executor.go | 4 ++-- pkg/executors/shell_executor.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/executors/docker_compose_executor.go b/pkg/executors/docker_compose_executor.go index 8304f339..10579af5 100644 --- a/pkg/executors/docker_compose_executor.go +++ b/pkg/executors/docker_compose_executor.go @@ -406,7 +406,7 @@ func (e *DockerComposeExecutor) injectImagePullSecretsForGCR(envVars []api.EnvVa content, err := f.Decode() if err != nil { - e.Logger.LogCommandOutput("Failed to decode content of file.\n") + e.Logger.LogCommandOutput("Failed to decode the content of the file.\n") return 1 } @@ -628,7 +628,7 @@ func (e *DockerComposeExecutor) InjectFiles(files []api.File) int { content, err := f.Decode() if err != nil { - e.Logger.LogCommandOutput("Failed to decode content of file.\n") + e.Logger.LogCommandOutput("Failed to decode the content of the file.\n") exitCode = 1 return exitCode } diff --git a/pkg/executors/shell_executor.go b/pkg/executors/shell_executor.go index 5204ba1c..12f0eeae 100644 --- a/pkg/executors/shell_executor.go +++ b/pkg/executors/shell_executor.go @@ -151,7 +151,7 @@ func (e *ShellExecutor) InjectFiles(files []api.File) int { content, err := f.Decode() if err != nil { - e.Logger.LogCommandOutput("Failed to decode content of file.\n") + e.Logger.LogCommandOutput("Failed to decode the content of the file.\n") exitCode = 1 return exitCode } From fb9c58a05fec23a7a5d5bb9b8e4eca6f723451a7 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Wed, 12 Jan 2022 10:52:35 -0300 Subject: [PATCH 011/130] Setup security checks (#136) --- .semaphore/semaphore.yml | 18 +++ Makefile | 19 +++ go.mod | 12 +- go.sum | 168 +++++++++++++++++------ main.go | 6 +- pkg/api/job_request.go | 4 +- pkg/eventlogger/filebackend.go | 21 ++- pkg/eventlogger/logger.go | 41 ++++-- pkg/executors/authorized_keys.go | 7 +- pkg/executors/docker_compose_executor.go | 30 +++- pkg/executors/shell_executor.go | 2 + pkg/executors/ssh_jump_point.go | 1 + pkg/listener/job_processor.go | 2 + pkg/listener/listener.go | 30 ++-- pkg/server/auth_middleware.go | 13 +- pkg/server/server.go | 11 +- pkg/shell/process.go | 11 +- pkg/shell/shell.go | 31 ++++- test/support/test_logger.go | 1 + 19 files changed, 327 insertions(+), 101 deletions(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index f4a21bed..646365bb 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -31,6 +31,24 @@ blocks: - go get -u github.com/mgechev/revive - make lint + - name: "Security checks" + dependencies: [] + task: + secrets: + - name: security-toolbox-shared-read-access + prologue: + commands: + - checkout + - mv ~/.ssh/security-toolbox ~/.ssh/id_rsa + - sudo chmod 600 ~/.ssh/id_rsa + jobs: + - name: Check dependencies + commands: + - make check.deps + - name: Check code + commands: + - make check.static + - name: "Tests" dependencies: [] task: diff --git a/Makefile b/Makefile index 097393dc..198b6bf1 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,25 @@ AGENT_PORT_IN_TESTS=30000 AGENT_SSH_PORT_IN_TESTS=2222 +SECURITY_TOOLBOX_BRANCH ?= master +SECURITY_TOOLBOX_TMP_DIR ?= /tmp/security-toolbox + +check.prepare: + rm -rf $(SECURITY_TOOLBOX_TMP_DIR) + git clone git@github.com:renderedtext/security-toolbox.git $(SECURITY_TOOLBOX_TMP_DIR) && (cd $(SECURITY_TOOLBOX_TMP_DIR) && git checkout $(SECURITY_TOOLBOX_BRANCH) && cd -) + +check.static: check.prepare + docker run -it -v $$(pwd):/app \ + -v $(SECURITY_TOOLBOX_TMP_DIR):$(SECURITY_TOOLBOX_TMP_DIR) \ + registry.semaphoreci.com/ruby:2.7 \ + bash -c 'cd /app && $(SECURITY_TOOLBOX_TMP_DIR)/code --language go -d' + +check.deps: check.prepare + docker run -it -v $$(pwd):/app \ + -v $(SECURITY_TOOLBOX_TMP_DIR):$(SECURITY_TOOLBOX_TMP_DIR) \ + registry.semaphoreci.com/ruby:2.7 \ + bash -c 'cd /app && $(SECURITY_TOOLBOX_TMP_DIR)/dependencies --language go -d' + go.install: cd /tmp sudo curl -O https://dl.google.com/go/go1.11.linux-amd64.tar.gz diff --git a/go.mod b/go.mod index e3a4d0a6..49c8cc4f 100644 --- a/go.mod +++ b/go.mod @@ -2,16 +2,20 @@ module github.com/semaphoreci/agent require ( github.com/creack/pty v1.1.17 - github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/felixge/httpsnoop v1.0.2 // indirect + github.com/golang-jwt/jwt/v4 v4.1.0 github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 github.com/mitchellh/panicwrap v1.0.0 - github.com/renderedtext/go-watchman v0.0.0-20210705111746-70108070d074 + github.com/renderedtext/go-watchman v0.0.0-20210809121718-0632d0d12b0a github.com/sirupsen/logrus v1.8.1 github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.8.1 + github.com/spf13/viper v1.9.0 github.com/stretchr/testify v1.7.0 - gopkg.in/yaml.v2 v2.4.0 + golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 // indirect + golang.org/x/text v0.3.7 // indirect + gopkg.in/ini.v1 v1.64.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) go 1.16 diff --git a/go.sum b/go.sum index 4ab0216b..6cd55b44 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,11 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -26,7 +31,7 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/firestore v1.6.0/go.mod h1:afJwI0vaXwAG54kI7A//lP/lSPDkQORQuMkv56TxEPU= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -39,13 +44,15 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -53,6 +60,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= @@ -60,26 +68,30 @@ github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr 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/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= +github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.1.0 h1:XUgk2Ex5veyVFVeLm0xhusUTQybEbexJXrvPNOKkSY0= +github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -92,6 +104,7 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -109,6 +122,7 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -122,10 +136,12 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -137,103 +153,111 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= +github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= +github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo= +github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/panicwrap v1.0.0 h1:67zIyVakCIvcs69A0FGfZjBdPleaonSgGlXRSRlb6fE= github.com/mitchellh/panicwrap v1.0.0/go.mod h1:pKvZHwWrZowLUzftuFq7coarnxbBXU4aQh3N0BJOeeA= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= -github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= +github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= 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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/renderedtext/go-watchman v0.0.0-20210705111746-70108070d074 h1:6YLKgGc2PwDM3oEpAUavoiyjjpIH44HmjWSswwWTBa8= github.com/renderedtext/go-watchman v0.0.0-20210705111746-70108070d074/go.mod h1:Z+qanDzSoUGCbcrTM7G6YCA9ST2KBdte7sCz+HQAp7I= +github.com/renderedtext/go-watchman v0.0.0-20210809121718-0632d0d12b0a h1:pRX9qebwT+TMdBojMspqDtU1RFLIbH5VzI8aI9yMiyE= +github.com/renderedtext/go-watchman v0.0.0-20210809121718-0632d0d12b0a/go.mod h1:Z+qanDzSoUGCbcrTM7G6YCA9ST2KBdte7sCz+HQAp7I= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYIR88KRMEuODE= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44= -github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/spf13/viper v1.9.0 h1:yR6EXjTp0y0cLN8OZg1CRZmOBdI88UcGkhgyJhu6nZk= +github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4= 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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -259,6 +283,7 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= @@ -267,8 +292,10 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -307,7 +334,6 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -318,6 +344,7 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -340,6 +367,7 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -351,7 +379,10 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -367,6 +398,7 @@ golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -374,13 +406,17 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -404,8 +440,18 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf h1:2ucpDCmfkl8Bd/FsLtiD653Wf96cW37s+iGx93zsu4k= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI= +golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -413,8 +459,11 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -424,7 +473,6 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -432,9 +480,9 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -468,7 +516,11 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -494,7 +546,12 @@ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34q google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -542,7 +599,18 @@ google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -562,7 +630,13 @@ google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA5 google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -575,14 +649,18 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/alexcesaro/statsd.v2 v2.0.0 h1:FXkZSCZIH17vLCO5sO2UucTHsH9pc+17F6pl3JVCwMc= gopkg.in/alexcesaro/statsd.v2 v2.0.0/go.mod h1:i0ubccKGzBVNBpdGV5MocxyA/XlLUJzA7SLonnE4drU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.63.2 h1:tGK/CyBg7SMzb60vP1M03vNZ3VDu3wGQJwn7Sxi9r3c= +gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.64.0 h1:Mj2zXEXcNb5joEiSA0zc3HZpTst/iyjNiR4CN8tDzOg= +gopkg.in/ini.v1 v1.64.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go index e4d82ed7..4a02182f 100644 --- a/main.go +++ b/main.go @@ -62,6 +62,7 @@ func main() { } func OpenLogfile() io.Writer { + // #nosec f, err := os.OpenFile("/tmp/agent_log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) if err != nil { @@ -103,7 +104,10 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { loadConfigFile(*configFile) } - viper.BindPFlags(pflag.CommandLine) + err := viper.BindPFlags(pflag.CommandLine) + if err != nil { + log.Fatalf("Error binding pflags: %v", err) + } validateConfiguration() diff --git a/pkg/api/job_request.go b/pkg/api/job_request.go index 9c2f284b..eacc5edf 100644 --- a/pkg/api/job_request.go +++ b/pkg/api/job_request.go @@ -7,7 +7,7 @@ import ( "io/ioutil" "path/filepath" - yaml "gopkg.in/yaml.v2" + yaml "gopkg.in/yaml.v3" ) type Container struct { @@ -94,6 +94,8 @@ func NewRequestFromJSON(content []byte) (*JobRequest, error) { func NewRequestFromYamlFile(path string) (*JobRequest, error) { filename, _ := filepath.Abs(path) + + // #nosec yamlFile, err := ioutil.ReadFile(filename) jobRequest := &JobRequest{} diff --git a/pkg/eventlogger/filebackend.go b/pkg/eventlogger/filebackend.go index 7f07f907..10e209b1 100644 --- a/pkg/eventlogger/filebackend.go +++ b/pkg/eventlogger/filebackend.go @@ -31,10 +31,20 @@ func (l *FileBackend) Open() error { } func (l *FileBackend) Write(event interface{}) error { - jsonString, _ := json.Marshal(event) + jsonString, err := json.Marshal(event) + if err != nil { + return err + } + + _, err = l.file.Write([]byte(jsonString)) + if err != nil { + return err + } - l.file.Write([]byte(jsonString)) - l.file.Write([]byte("\n")) + _, err = l.file.Write([]byte("\n")) + if err != nil { + return err + } log.Debugf("%s", jsonString) @@ -51,8 +61,6 @@ func (l *FileBackend) Stream(startLine int, writer io.Writer) (int, error) { return startLine, err } - defer fd.Close() - reader := bufio.NewReader(fd) lineIndex := 0 @@ -60,6 +68,7 @@ func (l *FileBackend) Stream(startLine int, writer io.Writer) (int, error) { line, err := reader.ReadString('\n') if err != nil { if err != io.EOF { + _ = fd.Close() return lineIndex, err } @@ -75,5 +84,5 @@ func (l *FileBackend) Stream(startLine int, writer io.Writer) (int, error) { } } - return lineIndex, nil + return lineIndex, fd.Close() } diff --git a/pkg/eventlogger/logger.go b/pkg/eventlogger/logger.go index 19866a90..922ad314 100644 --- a/pkg/eventlogger/logger.go +++ b/pkg/eventlogger/logger.go @@ -1,6 +1,10 @@ package eventlogger -import "time" +import ( + "time" + + log "github.com/sirupsen/logrus" +) type Logger struct { Backend Backend @@ -18,46 +22,58 @@ func (l *Logger) Close() error { return l.Backend.Close() } -func (l *Logger) LogJobStarted() error { +func (l *Logger) LogJobStarted() { event := &JobStartedEvent{ Timestamp: int(time.Now().Unix()), Event: "job_started", } - return l.Backend.Write(event) + err := l.Backend.Write(event) + if err != nil { + log.Errorf("Error writing job_started log: %v", err) + } } -func (l *Logger) LogJobFinished(result string) error { +func (l *Logger) LogJobFinished(result string) { event := &JobFinishedEvent{ Timestamp: int(time.Now().Unix()), Event: "job_finished", Result: result, } - return l.Backend.Write(event) + err := l.Backend.Write(event) + if err != nil { + log.Errorf("Error writing job_finished log: %v", err) + } } -func (l *Logger) LogCommandStarted(directive string) error { +func (l *Logger) LogCommandStarted(directive string) { event := &CommandStartedEvent{ Timestamp: int(time.Now().Unix()), Event: "cmd_started", Directive: directive, } - return l.Backend.Write(event) + err := l.Backend.Write(event) + if err != nil { + log.Errorf("Error writing cmd_started log: %v", err) + } } -func (l *Logger) LogCommandOutput(output string) error { +func (l *Logger) LogCommandOutput(output string) { event := &CommandOutputEvent{ Timestamp: int(time.Now().Unix()), Event: "cmd_output", Output: output, } - return l.Backend.Write(event) + err := l.Backend.Write(event) + if err != nil { + log.Errorf("Error writing cmd_output log: %v", err) + } } -func (l *Logger) LogCommandFinished(directive string, exitCode int, startedAt int, finishedAt int) error { +func (l *Logger) LogCommandFinished(directive string, exitCode int, startedAt int, finishedAt int) { event := &CommandFinishedEvent{ Timestamp: int(time.Now().Unix()), Event: "cmd_finished", @@ -67,5 +83,8 @@ func (l *Logger) LogCommandFinished(directive string, exitCode int, startedAt in FinishedAt: finishedAt, } - return l.Backend.Write(event) + err := l.Backend.Write(event) + if err != nil { + log.Errorf("Error writing cmd_finished log: %v", err) + } } diff --git a/pkg/executors/authorized_keys.go b/pkg/executors/authorized_keys.go index 444d91e1..65d32c80 100644 --- a/pkg/executors/authorized_keys.go +++ b/pkg/executors/authorized_keys.go @@ -21,6 +21,7 @@ func InjectEntriesToAuthorizedKeys(keys []api.PublicKey) error { authorizedKeysPath := filepath.Join(sshDirectory, "authorized_keys") + // #nosec authorizedKeys, err := os.OpenFile( authorizedKeysPath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, @@ -30,19 +31,19 @@ func InjectEntriesToAuthorizedKeys(keys []api.PublicKey) error { return err } - defer authorizedKeys.Close() - for _, key := range keys { authorizedKeysEntry, err := key.Decode() if err != nil { + _ = authorizedKeys.Close() return err } _, err = authorizedKeys.WriteString(string(authorizedKeysEntry) + "\n") if err != nil { + _ = authorizedKeys.Close() return err } } - return nil + return authorizedKeys.Close() } diff --git a/pkg/executors/docker_compose_executor.go b/pkg/executors/docker_compose_executor.go index 10579af5..47a02130 100644 --- a/pkg/executors/docker_compose_executor.go +++ b/pkg/executors/docker_compose_executor.go @@ -76,7 +76,11 @@ func (e *DockerComposeExecutor) Prepare() int { log.Debug("Compose File:") log.Debug(compose) - ioutil.WriteFile(e.dockerComposeManifestPath, []byte(compose), 0644) + err = ioutil.WriteFile(e.dockerComposeManifestPath, []byte(compose), 0600) + if err != nil { + log.Errorf("Error writing docker compose manifest file: %v", err) + return 1 + } return e.setUpSSHJumpPoint() } @@ -104,6 +108,8 @@ func (e *DockerComposeExecutor) executeHostCommands() error { for _, c := range hostCommands { log.Debug("Executing Host Command:", c.Directive) + + // #nosec cmd := exec.Command("bash", "-c", c.Directive) out, err := cmd.CombinedOutput() @@ -196,6 +202,7 @@ func (e *DockerComposeExecutor) startBashSession() int { log.Debug("Starting stateful shell") + // #nosec cmd := exec.Command( "docker-compose", "--ansi", @@ -412,6 +419,7 @@ func (e *DockerComposeExecutor) injectImagePullSecretsForGCR(envVars []api.EnvVa tmpPath := fmt.Sprintf("%s/file", e.tmpDirectory) + // #nosec err = ioutil.WriteFile(tmpPath, []byte(content), 0644) if err != nil { e.Logger.LogCommandOutput(err.Error() + "\n") @@ -427,6 +435,8 @@ func (e *DockerComposeExecutor) injectImagePullSecretsForGCR(envVars []api.EnvVa } fileCmd := fmt.Sprintf("mkdir -p %s", path.Dir(destPath)) + + // #nosec cmd := exec.Command("bash", "-c", fileCmd) out, err := cmd.CombinedOutput() if err != nil { @@ -436,6 +446,8 @@ func (e *DockerComposeExecutor) injectImagePullSecretsForGCR(envVars []api.EnvVa } fileCmd = fmt.Sprintf("cp %s %s", tmpPath, destPath) + + // #nosec cmd = exec.Command("bash", "-c", fileCmd) out, err = cmd.CombinedOutput() if err != nil { @@ -445,6 +457,8 @@ func (e *DockerComposeExecutor) injectImagePullSecretsForGCR(envVars []api.EnvVa } fileCmd = fmt.Sprintf("chmod %s %s", f.Mode, destPath) + + // #nosec cmd = exec.Command("bash", "-c", fileCmd) out, err = cmd.CombinedOutput() if err != nil { @@ -511,6 +525,7 @@ func (e *DockerComposeExecutor) pullDockerImages() int { // are not present locally. // + // #nosec cmd := exec.Command( "docker-compose", "--ansi", @@ -591,7 +606,7 @@ func (e *DockerComposeExecutor) ExportEnvVars(envVars []api.EnvVar, hostEnvVars } envPath := fmt.Sprintf("%s/.env", e.tmpDirectory) - err := ioutil.WriteFile(envPath, []byte(envFile), 0644) + err := ioutil.WriteFile(envPath, []byte(envFile), 0600) if err != nil { exitCode = 255 @@ -635,6 +650,7 @@ func (e *DockerComposeExecutor) InjectFiles(files []api.File) int { tmpPath := fmt.Sprintf("%s/file", e.tmpDirectory) + // #nosec err = ioutil.WriteFile(tmpPath, []byte(content), 0644) if err != nil { e.Logger.LogCommandOutput(err.Error() + "\n") @@ -740,13 +756,19 @@ func (e *DockerComposeExecutor) SubmitDockerStats(metricName string) { func (e *DockerComposeExecutor) SubmitStats(imageName, metricName string, tags []string, value int) { if strings.Contains(e.jobRequest.Compose.Containers[0].Image, imageName) { - watchman.SubmitWithTags(metricName, tags, value) + err := watchman.SubmitWithTags(metricName, tags, value) + if err != nil { + log.Errorf("Error submiting metrics: %v", err) + } } } func (e *DockerComposeExecutor) SubmitDockerPullTime(duration int) { if strings.Contains(e.jobRequest.Compose.Containers[0].Image, "semaphoreci/android") { // only submiting android metrics. - watchman.SubmitWithTags("compose.docker.pull.duration", []string{"semaphoreci/android"}, duration) + err := watchman.SubmitWithTags("compose.docker.pull.duration", []string{"semaphoreci/android"}, duration) + if err != nil { + log.Errorf("Error submiting metrics: %v", err) + } } } diff --git a/pkg/executors/shell_executor.go b/pkg/executors/shell_executor.go index 12f0eeae..fd558b1f 100644 --- a/pkg/executors/shell_executor.go +++ b/pkg/executors/shell_executor.go @@ -117,6 +117,7 @@ func (e *ShellExecutor) ExportEnvVars(envVars []api.EnvVar, hostEnvVars []config envFile += fmt.Sprintf("export %s=%s\n", env.Name, ShellQuote(env.Value)) } + // #nosec err := ioutil.WriteFile("/tmp/.env", []byte(envFile), 0644) if err != nil { @@ -158,6 +159,7 @@ func (e *ShellExecutor) InjectFiles(files []api.File) int { tmpPath := fmt.Sprintf("%s/file", e.tmpDirectory) + // #nosec err = ioutil.WriteFile(tmpPath, []byte(content), 0644) if err != nil { e.Logger.LogCommandOutput(err.Error() + "\n") diff --git a/pkg/executors/ssh_jump_point.go b/pkg/executors/ssh_jump_point.go index 9baad36e..12242de3 100644 --- a/pkg/executors/ssh_jump_point.go +++ b/pkg/executors/ssh_jump_point.go @@ -5,6 +5,7 @@ import "os" const SSHJumpPointPath = "/tmp/ssh_jump_point" func SetUpSSHJumpPoint(script string) error { + // #nosec f, err := os.OpenFile(SSHJumpPointPath, os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return err diff --git a/pkg/listener/job_processor.go b/pkg/listener/job_processor.go index 83b8ae13..30e19b1d 100644 --- a/pkg/listener/job_processor.go +++ b/pkg/listener/job_processor.go @@ -269,6 +269,8 @@ func (p *JobProcessor) Shutdown(reason ShutdownReason, code int) { func (p *JobProcessor) executeShutdownHook(reason ShutdownReason) { if p.ShutdownHookPath != "" { log.Infof("Executing shutdown hook from %s", p.ShutdownHookPath) + + // #nosec cmd := exec.Command("bash", p.ShutdownHookPath) cmd.Env = os.Environ() cmd.Env = append(cmd.Env, fmt.Sprintf("SEMAPHORE_AGENT_SHUTDOWN_REASON=%s", reason)) diff --git a/pkg/listener/listener.go b/pkg/listener/listener.go index ec560403..a96349a9 100644 --- a/pkg/listener/listener.go +++ b/pkg/listener/listener.go @@ -1,9 +1,10 @@ package listener import ( + "crypto/rand" + "encoding/base64" "fmt" "io" - "math/rand" "net/http" "os" "time" @@ -77,28 +78,37 @@ func (l *Listener) DisplayHelloMessage() { fmt.Println(" ") } -const nameLetters = "abcdefghijklmnopqrstuvwxyz123456789" -const nameLength = 20 +// base64 gives you 4 chars every 3 bytes, we want 20 chars, so 15 bytes +const nameLength = 15 -func (l *Listener) Name() string { - b := make([]byte, nameLength) - for i := range b { - b[i] = nameLetters[rand.Intn(len(nameLetters))] +func (l *Listener) Name() (string, error) { + buffer := make([]byte, nameLength) + _, err := rand.Read(buffer) + + if err != nil { + return "", err } - return string(b) + + return base64.URLEncoding.EncodeToString(buffer), nil } func (l *Listener) Register() error { + name, err := l.Name() + if err != nil { + log.Errorf("Error generating name for agent: %v", err) + return err + } + req := &selfhostedapi.RegisterRequest{ Version: l.Config.AgentVersion, - Name: l.Name(), + Name: name, PID: os.Getpid(), OS: osinfo.Name(), Arch: osinfo.Arch(), Hostname: osinfo.Hostname(), } - err := retry.RetryWithConstantWait("Register", l.Config.RegisterRetryLimit, time.Second, func() error { + err = retry.RetryWithConstantWait("Register", l.Config.RegisterRetryLimit, time.Second, func() error { resp, err := l.Client.Register(req) if err != nil { return err diff --git a/pkg/server/auth_middleware.go b/pkg/server/auth_middleware.go index 4f4c097b..768b1fc0 100644 --- a/pkg/server/auth_middleware.go +++ b/pkg/server/auth_middleware.go @@ -6,7 +6,7 @@ import ( "net/http" "strings" - jwt "github.com/dgrijalva/jwt-go" + jwt "github.com/golang-jwt/jwt/v4" ) // @@ -24,7 +24,7 @@ func CreateJwtMiddleware(jwtSecret []byte) func(http.HandlerFunc) http.HandlerFu if authorizationHeader == "" { w.WriteHeader(401) - json.NewEncoder(w).Encode("An authorization header is required") + _ = json.NewEncoder(w).Encode("An authorization header is required") return } @@ -32,13 +32,13 @@ func CreateJwtMiddleware(jwtSecret []byte) func(http.HandlerFunc) http.HandlerFu if len(bearerToken) != 2 { w.WriteHeader(401) - json.NewEncoder(w).Encode("Invalid authorization token") + _ = json.NewEncoder(w).Encode("Invalid authorization token") return } token, err := jwt.Parse(bearerToken[1], func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("Invalid authorization token") + return nil, fmt.Errorf("invalid authorization token") } return jwtSecret, nil @@ -46,13 +46,14 @@ func CreateJwtMiddleware(jwtSecret []byte) func(http.HandlerFunc) http.HandlerFu if err != nil { w.WriteHeader(401) - json.NewEncoder(w).Encode(err.Error()) + _ = json.NewEncoder(w).Encode(err.Error()) return } if !token.Valid { w.WriteHeader(401) - json.NewEncoder(w).Encode("Invalid authorization token") + _ = json.NewEncoder(w).Encode("Invalid authorization token") + return } next(w, req) diff --git a/pkg/server/server.go b/pkg/server/server.go index 8931d690..3c379db2 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -150,9 +150,16 @@ func (s *Server) AgentLogs(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) return } - defer logfile.Close() - io.Copy(w, logfile) + _, err = io.Copy(w, logfile) + if err != nil { + log.Errorf("Error writing agent logs: %v", err) + } + + err = logfile.Close() + if err != nil { + log.Errorf("Error closing agent logs file: %v", err) + } } func (s *Server) Run(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/shell/process.go b/pkg/shell/process.go index b7abe0ff..a1fbe167 100644 --- a/pkg/shell/process.go +++ b/pkg/shell/process.go @@ -117,8 +117,13 @@ func (p *Process) Run() { return } - p.Shell.Write(instruction) - p.scan() + _, err = p.Shell.Write(instruction) + if err != nil { + log.Errorf("Error writing instruction: %v", err) + return + } + + _ = p.scan() } func (p *Process) constructShellInstruction() string { @@ -143,6 +148,7 @@ func (p *Process) loadCommand() error { // scheme. To circumvent this, we are storing the command in a file // + // #nosec err := ioutil.WriteFile(p.cmdFilePath, []byte(p.Command), 0644) if err != nil { // TODO: log something @@ -169,6 +175,7 @@ func (p *Process) readBufferSize() int { min := 1 max := 20 + // #nosec return rand.Intn(max-min) + min } diff --git a/pkg/shell/shell.go b/pkg/shell/shell.go index 6caf0bff..42b2cab1 100644 --- a/pkg/shell/shell.go +++ b/pkg/shell/shell.go @@ -69,7 +69,7 @@ func (s *Shell) handleAbruptShellCloses() { } log.Debugf("Shell closed with %s. Closing associated TTY", msg) - s.TTY.Close() + _ = s.TTY.Close() log.Debugf("Publishing an exit signal: %s", msg) s.ExitSignal <- msg @@ -119,11 +119,30 @@ func (s *Shell) Write(instruction string) (int, error) { func (s *Shell) silencePromptAndDisablePS1() error { everythingIsReadyMark := "87d140552e404df69f6472729d2b2c3" - s.TTY.Write([]byte("export PS1=''\n")) - s.TTY.Write([]byte("stty -echo\n")) - s.TTY.Write([]byte("echo stty `stty -g` > /tmp/restore-tty\n")) - s.TTY.Write([]byte("cd ~\n")) - s.TTY.Write([]byte("echo '" + everythingIsReadyMark + "'\n")) + _, err := s.TTY.Write([]byte("export PS1=''\n")) + if err != nil { + return err + } + + _, err = s.TTY.Write([]byte("stty -echo\n")) + if err != nil { + return err + } + + _, err = s.TTY.Write([]byte("echo stty `stty -g` > /tmp/restore-tty\n")) + if err != nil { + return err + } + + _, err = s.TTY.Write([]byte("cd ~\n")) + if err != nil { + return err + } + + _, err = s.TTY.Write([]byte("echo '" + everythingIsReadyMark + "'\n")) + if err != nil { + return err + } stdoutScanner := bufio.NewScanner(s.TTY) diff --git a/test/support/test_logger.go b/test/support/test_logger.go index 7e8dba1e..b48adc40 100644 --- a/test/support/test_logger.go +++ b/test/support/test_logger.go @@ -8,6 +8,7 @@ import ( ) func SetupTestLogs() { + // #nosec f, err := os.OpenFile("/tmp/test.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0777) if err != nil { fmt.Printf("error opening file: %v", err) From 0312e2d7d9a26797586ae08e35a10367756f1d90 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Wed, 23 Feb 2022 08:04:39 -0300 Subject: [PATCH 012/130] Expose SEMAPHORE_AGENT_LOG_FILE_PATH (#143) --- .semaphore/semaphore.yml | 2 +- main.go | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 646365bb..326ed831 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -173,7 +173,7 @@ blocks: - container_options - dockerhub_private_image - docker_registry_private_image - - docker_private_image_ecr + # - docker_private_image_ecr - docker_private_image_gcr - dockerhub_private_image_bad_creds - docker_registry_private_image_bad_creds diff --git a/main.go b/main.go index 4a02182f..f34405ad 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "math/rand" "net/http" "os" + "path" "strings" "time" @@ -63,7 +64,7 @@ func main() { func OpenLogfile() io.Writer { // #nosec - f, err := os.OpenFile("/tmp/agent_log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) + f, err := os.OpenFile(getLogFilePath(), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) if err != nil { log.Fatal(err) @@ -86,6 +87,21 @@ func getLogLevel() log.Level { return level } +func getLogFilePath() string { + logFilePath := os.Getenv("SEMAPHORE_AGENT_LOG_FILE_PATH") + if logFilePath == "" { + return "/tmp/agent_log" + } + + parentDirectory := path.Dir(logFilePath) + err := os.MkdirAll(parentDirectory, 0644) + if err != nil { + log.Panicf("Could not create directories to place log file in '%s': %v", logFilePath, err) + } + + return logFilePath +} + func RunListener(httpClient *http.Client, logfile io.Writer) { configFile := pflag.String(config.ConfigFile, "", "Config file") _ = pflag.String(config.Endpoint, "", "Endpoint where agents are registered") From 444c6be958c330e7ac52171b970aeaf1c7557e2b Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Wed, 9 Mar 2022 16:08:27 -0300 Subject: [PATCH 013/130] Use 0644 when exposing env vars on docker-compose executor (#147) --- pkg/executors/docker_compose_executor.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/executors/docker_compose_executor.go b/pkg/executors/docker_compose_executor.go index 47a02130..c2fdf526 100644 --- a/pkg/executors/docker_compose_executor.go +++ b/pkg/executors/docker_compose_executor.go @@ -76,7 +76,8 @@ func (e *DockerComposeExecutor) Prepare() int { log.Debug("Compose File:") log.Debug(compose) - err = ioutil.WriteFile(e.dockerComposeManifestPath, []byte(compose), 0600) + // #nosec + err = ioutil.WriteFile(e.dockerComposeManifestPath, []byte(compose), 0644) if err != nil { log.Errorf("Error writing docker compose manifest file: %v", err) return 1 @@ -606,7 +607,9 @@ func (e *DockerComposeExecutor) ExportEnvVars(envVars []api.EnvVar, hostEnvVars } envPath := fmt.Sprintf("%s/.env", e.tmpDirectory) - err := ioutil.WriteFile(envPath, []byte(envFile), 0600) + + // #nosec + err := ioutil.WriteFile(envPath, []byte(envFile), 0644) if err != nil { exitCode = 255 From fc7e17f692b339ce228f7496ed38a6377bcd2f91 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Thu, 10 Mar 2022 08:30:02 -0300 Subject: [PATCH 014/130] Initial Windows support (#144) --- .gitignore | 1 + .semaphore/semaphore.yml | 1 - Makefile | 6 +- Vagrantfile | 8 + go.mod | 2 +- go.sum | 6 - main.go | 3 +- pkg/api/job_request.go | 24 ++ pkg/eventlogger/default.go | 5 +- pkg/eventlogger/httpbackend.go | 5 +- pkg/executors/authorized_keys.go | 8 +- pkg/executors/docker_compose_executor.go | 53 ++-- pkg/executors/docker_compose_executor_test.go | 13 +- pkg/executors/shell_executor.go | 170 ++++++----- pkg/executors/shell_executor_test.go | 11 +- pkg/executors/ssh_jump_point.go | 11 +- pkg/executors/utils.go | 32 --- pkg/jobs/job.go | 20 +- pkg/listener/job_processor.go | 36 ++- pkg/osinfo/name.go | 17 ++ pkg/osinfo/name_windows.go | 16 ++ pkg/osinfo/osinfo.go | 12 - pkg/server/server.go | 9 +- pkg/shell/env.go | 145 ++++++++++ pkg/shell/output_buffer.go | 2 +- pkg/shell/output_buffer_test.go | 4 +- pkg/shell/process.go | 268 +++++++++++++++--- pkg/shell/process_setup.go | 17 ++ pkg/shell/process_setup_windows.go | 30 ++ pkg/shell/pty.go | 14 + pkg/shell/pty_windows.go | 13 + pkg/shell/shell.go | 77 ++++- pkg/shell/shell_setup.go | 17 ++ pkg/shell/shell_setup_windows.go | 44 +++ pkg/shell/shell_test.go | 5 +- scripts/provision-windows-box.ps1 | 43 +++ test/e2e/docker/broken_unicode.rb | 5 +- test/e2e/docker/check_dev_kvm.rb | 12 +- test/e2e/docker/command_aliases.rb | 5 +- test/e2e/docker/container_custom_name.rb | 6 +- test/e2e/docker/container_env_vars.rb | 6 +- test/e2e/docker/container_options.rb | 6 +- test/e2e/docker/docker_in_docker.rb | 6 +- test/e2e/docker/docker_private_image_gcr.rb | 6 +- .../docker/docker_registry_private_image.rb | 6 +- test/e2e/docker/dockerhub_private_image.rb | 6 +- test/e2e/docker/env_vars.rb | 6 +- test/e2e/docker/epilogue_on_fail.rb | 23 +- test/e2e/docker/epilogue_on_pass.rb | 23 +- test/e2e/docker/failed_job.rb | 6 +- test/e2e/docker/file_injection.rb | 12 +- .../docker/file_injection_broken_file_mode.rb | 6 +- test/e2e/docker/hello_world.rb | 6 +- test/e2e/docker/host_setup_commands.rb | 6 +- test/e2e/docker/job_stopping_on_epilogue.rb | 6 +- test/e2e/docker/multiple_containers.rb | 6 +- test/e2e/docker/stty_restoration.rb | 5 +- test/e2e/docker/unicode.rb | 6 +- test/e2e/docker/unknown_command.rb | 6 +- .../docker_compose_host_env_vars.rb | 6 +- .../self-hosted/docker_compose_host_files.rb | 6 +- .../docker_compose_missing_host_files.rb | 6 +- test/e2e/self-hosted/no_ssh_jump_points.rb | 6 +- test/e2e/self-hosted/shell_host_env_vars.rb | 6 +- test/e2e/shell/broken_unicode.rb | 5 +- test/e2e/shell/command_aliases.rb | 5 +- test/e2e/shell/env_vars.rb | 5 +- test/e2e/shell/epilogue_on_fail.rb | 5 +- test/e2e/shell/epilogue_on_pass.rb | 5 +- test/e2e/shell/failed_job.rb | 5 +- test/e2e/shell/file_injection.rb | 12 +- .../shell/file_injection_broken_file_mode.rb | 7 +- test/e2e/shell/hello_world.rb | 5 +- test/e2e/shell/job_stopping_on_epilogue.rb | 5 +- test/e2e/shell/killing_root_bash.rb | 5 +- test/e2e/shell/set_e.rb | 5 +- test/e2e/shell/set_pipefail.rb | 5 +- test/e2e/shell/stty_restoration.rb | 5 +- test/e2e/shell/unicode.rb | 5 +- test/e2e/shell/unknown_command.rb | 5 +- test/e2e_support/docker-compose-listen.yml | 2 +- 81 files changed, 1092 insertions(+), 358 deletions(-) create mode 100644 Vagrantfile delete mode 100644 pkg/executors/utils.go create mode 100644 pkg/osinfo/name.go create mode 100644 pkg/osinfo/name_windows.go create mode 100644 pkg/shell/env.go create mode 100644 pkg/shell/process_setup.go create mode 100644 pkg/shell/process_setup_windows.go create mode 100644 pkg/shell/pty.go create mode 100644 pkg/shell/pty_windows.go create mode 100644 pkg/shell/shell_setup.go create mode 100644 pkg/shell/shell_setup_windows.go create mode 100644 scripts/provision-windows-box.ps1 diff --git a/.gitignore b/.gitignore index 570a6ba3..7fe7de22 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /build /log agent +.vagrant/ diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 326ed831..a26a412d 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -111,7 +111,6 @@ blocks: - stty_restoration - epilogue_on_pass - epilogue_on_fail - - ssh_jump_points - unicode - unknown_command - broken_unicode diff --git a/Makefile b/Makefile index 198b6bf1..08500785 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,7 @@ serve: .PHONY: serve test: - go test -short -v ./... + go test -p 1 -short -v ./... .PHONY: test build: @@ -48,6 +48,10 @@ build: env GOOS=linux GOARCH=386 go build -o build/agent main.go .PHONY: build +build.windows: + rm -rf build + env GOOS=windows GOARCH=amd64 go build -o build/agent.exe main.go + e2e: build ruby test/e2e/$(TEST).rb diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 00000000..b3f3824f --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,8 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +Vagrant.configure("2") do |config| + # https://github.com/gusztavvargadr/packer/ + config.vm.box = "gusztavvargadr/windows-server-2019-standard" + config.vm.provision "shell", path: "scripts/provision-windows-box.ps1" +end diff --git a/go.mod b/go.mod index 49c8cc4f..5dfb2325 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.9.0 github.com/stretchr/testify v1.7.0 - golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 // indirect + golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 golang.org/x/text v0.3.7 // indirect gopkg.in/ini.v1 v1.64.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b diff --git a/go.sum b/go.sum index 6cd55b44..e920757f 100644 --- a/go.sum +++ b/go.sum @@ -78,7 +78,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= @@ -236,8 +235,6 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/renderedtext/go-watchman v0.0.0-20210705111746-70108070d074 h1:6YLKgGc2PwDM3oEpAUavoiyjjpIH44HmjWSswwWTBa8= -github.com/renderedtext/go-watchman v0.0.0-20210705111746-70108070d074/go.mod h1:Z+qanDzSoUGCbcrTM7G6YCA9ST2KBdte7sCz+HQAp7I= github.com/renderedtext/go-watchman v0.0.0-20210809121718-0632d0d12b0a h1:pRX9qebwT+TMdBojMspqDtU1RFLIbH5VzI8aI9yMiyE= github.com/renderedtext/go-watchman v0.0.0-20210809121718-0632d0d12b0a/go.mod h1:Z+qanDzSoUGCbcrTM7G6YCA9ST2KBdte7sCz+HQAp7I= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= @@ -448,7 +445,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf h1:2ucpDCmfkl8Bd/FsLtiD653Wf96cW37s+iGx93zsu4k= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI= golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -460,7 +456,6 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -657,7 +652,6 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.63.2 h1:tGK/CyBg7SMzb60vP1M03vNZ3VDu3wGQJwn7Sxi9r3c= gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.64.0 h1:Mj2zXEXcNb5joEiSA0zc3HZpTst/iyjNiR4CN8tDzOg= gopkg.in/ini.v1 v1.64.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= diff --git a/main.go b/main.go index f34405ad..409754ae 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "path" + "path/filepath" "strings" "time" @@ -90,7 +91,7 @@ func getLogLevel() log.Level { func getLogFilePath() string { logFilePath := os.Getenv("SEMAPHORE_AGENT_LOG_FILE_PATH") if logFilePath == "" { - return "/tmp/agent_log" + return filepath.Join(os.TempDir(), "agent_log") } parentDirectory := path.Dir(logFilePath) diff --git a/pkg/api/job_request.go b/pkg/api/job_request.go index eacc5edf..8b606767 100644 --- a/pkg/api/job_request.go +++ b/pkg/api/job_request.go @@ -4,8 +4,11 @@ import ( "encoding/base64" "encoding/json" "fmt" + "io/fs" "io/ioutil" "path/filepath" + "strconv" + "strings" yaml "gopkg.in/yaml.v3" ) @@ -46,6 +49,27 @@ type File struct { Mode string `json:"mode" yaml:"mode"` } +func (f *File) NormalizePath(homeDir string) string { + if filepath.IsAbs(f.Path) { + return filepath.FromSlash(f.Path) + } + + if strings.HasPrefix(f.Path, "~") { + return filepath.FromSlash(strings.ReplaceAll(f.Path, "~", homeDir)) + } + + return filepath.FromSlash(filepath.Join(homeDir, f.Path)) +} + +func (f *File) ParseMode() (fs.FileMode, error) { + fileMode, err := strconv.ParseUint(f.Mode, 8, 32) + if err != nil { + return 0, fmt.Errorf("bad file permission '%s'", f.Mode) + } + + return fs.FileMode(fileMode), nil +} + type Callbacks struct { Finished string `json:"finished" yaml:"finished"` TeardownFinished string `json:"teardown_finished" yaml:"teardown_finished"` diff --git a/pkg/eventlogger/default.go b/pkg/eventlogger/default.go index 979f607c..5a5888ac 100644 --- a/pkg/eventlogger/default.go +++ b/pkg/eventlogger/default.go @@ -3,6 +3,8 @@ package eventlogger import ( "errors" "fmt" + "os" + "path/filepath" "github.com/semaphoreci/agent/pkg/api" ) @@ -22,7 +24,8 @@ func CreateLogger(request *api.JobRequest) (*Logger, error) { } func Default() (*Logger, error) { - backend, err := NewFileBackend("/tmp/job_log.json") + path := filepath.Join(os.TempDir(), "job_log.json") + backend, err := NewFileBackend(path) if err != nil { return nil, err } diff --git a/pkg/eventlogger/httpbackend.go b/pkg/eventlogger/httpbackend.go index 369167d5..16d8bcbb 100644 --- a/pkg/eventlogger/httpbackend.go +++ b/pkg/eventlogger/httpbackend.go @@ -4,6 +4,8 @@ import ( "bytes" "fmt" "net/http" + "os" + "path/filepath" "sync" "time" @@ -22,7 +24,8 @@ type HTTPBackend struct { } func NewHTTPBackend(url, token string) (*HTTPBackend, error) { - fileBackend, err := NewFileBackend("/tmp/job_log.json") + path := filepath.Join(os.TempDir(), "job_log.json") + fileBackend, err := NewFileBackend(path) if err != nil { return nil, err } diff --git a/pkg/executors/authorized_keys.go b/pkg/executors/authorized_keys.go index 65d32c80..22c679c2 100644 --- a/pkg/executors/authorized_keys.go +++ b/pkg/executors/authorized_keys.go @@ -12,9 +12,13 @@ func InjectEntriesToAuthorizedKeys(keys []api.PublicKey) error { return nil } - sshDirectory := filepath.Join(UserHomeDir(), ".ssh") + homeDir, err := os.UserHomeDir() + if err != nil { + return err + } - err := os.MkdirAll(sshDirectory, os.ModePerm) + sshDirectory := filepath.Join(homeDir, ".ssh") + err = os.MkdirAll(sshDirectory, os.ModePerm) if err != nil { return err } diff --git a/pkg/executors/docker_compose_executor.go b/pkg/executors/docker_compose_executor.go index c2fdf526..aaa63785 100644 --- a/pkg/executors/docker_compose_executor.go +++ b/pkg/executors/docker_compose_executor.go @@ -7,10 +7,11 @@ import ( "os" "os/exec" "path" + "path/filepath" + "runtime" "strings" "time" - pty "github.com/creack/pty" watchman "github.com/renderedtext/go-watchman" api "github.com/semaphoreci/agent/pkg/api" "github.com/semaphoreci/agent/pkg/config" @@ -56,6 +57,11 @@ func NewDockerComposeExecutor(request *api.JobRequest, logger *eventlogger.Logge } func (e *DockerComposeExecutor) Prepare() int { + if runtime.GOOS == "windows" { + log.Error("docker-compose executor is not supported in Windows") + return 1 + } + err := os.MkdirAll(e.tmpDirectory, os.ModePerm) if err != nil { return 1 @@ -204,8 +210,8 @@ func (e *DockerComposeExecutor) startBashSession() int { log.Debug("Starting stateful shell") // #nosec - cmd := exec.Command( - "docker-compose", + executable := "docker-compose" + args := []string{ "--ansi", "never", "-f", @@ -220,9 +226,9 @@ func (e *DockerComposeExecutor) startBashSession() int { fmt.Sprintf("%s:%s:ro", e.tmpDirectory, e.tmpDirectory), e.mainContainerName, "bash", - ) + } - shell, err := shell.NewShell(cmd, e.tmpDirectory) + shell, err := shell.NewShellFromExecAndArgs(executable, args, e.tmpDirectory) if err != nil { log.Errorf("Failed to start stateful shell err: %+v", err) @@ -538,7 +544,7 @@ func (e *DockerComposeExecutor) pullDockerImages() int { e.mainContainerName, "true") - tty, err := pty.Start(cmd) + tty, err := shell.StartPTY(cmd) if err != nil { log.Errorf("Failed to initialize docker pull, err: %+v", err) return 1 @@ -586,43 +592,30 @@ func (e *DockerComposeExecutor) ExportEnvVars(envVars []api.EnvVar, hostEnvVars e.Logger.LogCommandFinished(directive, exitCode, commandStartedAt, commandFinishedAt) }() - envFile := "" - - for _, env := range envVars { - e.Logger.LogCommandOutput(fmt.Sprintf("Exporting %s\n", env.Name)) - - value, err := env.Decode() - - if err != nil { - exitCode = 1 - return exitCode - } - - envFile += fmt.Sprintf("export %s=%s\n", env.Name, ShellQuote(string(value))) - } - - for _, env := range hostEnvVars { - e.Logger.LogCommandOutput(fmt.Sprintf("Exporting %s\n", env.Name)) - envFile += fmt.Sprintf("export %s=%s\n", env.Name, ShellQuote(env.Value)) + environment, err := shell.CreateEnvironment(envVars, hostEnvVars) + if err != nil { + log.Errorf("Error creating environment: %v", err) + exitCode = 1 + return exitCode } - envPath := fmt.Sprintf("%s/.env", e.tmpDirectory) - - // #nosec - err := ioutil.WriteFile(envPath, []byte(envFile), 0644) + envFileName := filepath.Join(e.tmpDirectory, ".env") + err = environment.ToFile(envFileName, func(name string) { + e.Logger.LogCommandOutput(fmt.Sprintf("Exporting %s\n", name)) + }) if err != nil { exitCode = 255 return exitCode } - cmd := fmt.Sprintf("source %s", envPath) + cmd := fmt.Sprintf("source %s", envFileName) exitCode = e.RunCommand(cmd, true, "") if exitCode != 0 { return exitCode } - cmd = fmt.Sprintf("echo 'source %s' >> ~/.bash_profile", envPath) + cmd = fmt.Sprintf("echo 'source %s' >> ~/.bash_profile", envFileName) exitCode = e.RunCommand(cmd, true, "") if exitCode != 0 { return exitCode diff --git a/pkg/executors/docker_compose_executor_test.go b/pkg/executors/docker_compose_executor_test.go index f3482c40..100354d8 100644 --- a/pkg/executors/docker_compose_executor_test.go +++ b/pkg/executors/docker_compose_executor_test.go @@ -3,6 +3,7 @@ package executors import ( "encoding/base64" "os/exec" + "runtime" "testing" "time" @@ -66,6 +67,10 @@ func startComposeExecutor() (*DockerComposeExecutor, *eventlogger.Logger, *event } func Test__DockerComposeExecutor(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("docker-compose executor is not yet support in Windows") + } + e, _, testLoggerBackend := startComposeExecutor() e.RunCommand("echo 'here'", false, "") @@ -78,14 +83,14 @@ func Test__DockerComposeExecutor(t *testing.T) { e.RunCommand(multilineCmd, false, "") envVars := []api.EnvVar{ - api.EnvVar{Name: "A", Value: "Zm9vCg=="}, + {Name: "A", Value: "Zm9vCg=="}, } e.ExportEnvVars(envVars, []config.HostEnvVar{}) e.RunCommand("echo $A", false, "") files := []api.File{ - api.File{ + { Path: "/tmp/random-file.txt", Content: "YWFhYmJiCgo=", Mode: "0600", @@ -139,6 +144,10 @@ func Test__DockerComposeExecutor(t *testing.T) { } func Test__DockerComposeExecutor__StopingRunningJob(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("docker-compose executor is not yet support in Windows") + } + e, _, testLoggerBackend := startComposeExecutor() go func() { diff --git a/pkg/executors/shell_executor.go b/pkg/executors/shell_executor.go index fd558b1f..3ba9e99e 100644 --- a/pkg/executors/shell_executor.go +++ b/pkg/executors/shell_executor.go @@ -2,9 +2,10 @@ package executors import ( "fmt" - "io/ioutil" - "os/exec" + "os" "path" + "path/filepath" + "runtime" "strings" "time" @@ -17,23 +18,31 @@ import ( type ShellExecutor struct { Executor - - Logger *eventlogger.Logger - Shell *shell.Shell - jobRequest *api.JobRequest - - tmpDirectory string + Logger *eventlogger.Logger + Shell *shell.Shell + jobRequest *api.JobRequest + tmpDirectory string + hasSSHJumpPoint bool + shouldUpdateBashProfile bool + cleanupAfterClose []string } -func NewShellExecutor(request *api.JobRequest, logger *eventlogger.Logger) *ShellExecutor { +func NewShellExecutor(request *api.JobRequest, logger *eventlogger.Logger, selfHosted bool) *ShellExecutor { return &ShellExecutor{ - Logger: logger, - jobRequest: request, - tmpDirectory: "/tmp", + Logger: logger, + jobRequest: request, + tmpDirectory: os.TempDir(), + hasSSHJumpPoint: !selfHosted, + shouldUpdateBashProfile: !selfHosted, + cleanupAfterClose: []string{}, } } func (e *ShellExecutor) Prepare() int { + if !e.hasSSHJumpPoint { + return 0 + } + return e.setUpSSHJumpPoint() } @@ -65,15 +74,13 @@ func (e *ShellExecutor) setUpSSHJumpPoint() int { } func (e *ShellExecutor) Start() int { - cmd := exec.Command("bash", "--login") - - shell, err := shell.NewShell(cmd, e.tmpDirectory) + sh, err := shell.NewShell(e.tmpDirectory) if err != nil { - log.Debug(shell) + log.Debug(sh) return 1 } - e.Shell = shell + e.Shell = sh err = e.Shell.Start() if err != nil { @@ -97,42 +104,53 @@ func (e *ShellExecutor) ExportEnvVars(envVars []api.EnvVar, hostEnvVars []config e.Logger.LogCommandFinished(directive, exitCode, commandStartedAt, commandFinishedAt) }() - envFile := "" - - for _, env := range envVars { - e.Logger.LogCommandOutput(fmt.Sprintf("Exporting %s\n", env.Name)) - - value, err := env.Decode() - - if err != nil { - exitCode = 1 - return exitCode - } - - envFile += fmt.Sprintf("export %s=%s\n", env.Name, ShellQuote(string(value))) + environment, err := shell.CreateEnvironment(envVars, hostEnvVars) + if err != nil { + exitCode = 1 + return exitCode } - for _, env := range hostEnvVars { - e.Logger.LogCommandOutput(fmt.Sprintf("Exporting %s\n", env.Name)) - envFile += fmt.Sprintf("export %s=%s\n", env.Name, ShellQuote(env.Value)) + /* + * In windows, no PTY is used, so the environment state + * is tracked in the shell itself. + */ + if runtime.GOOS == "windows" { + e.Shell.Env.Append(environment, func(name, value string) { + e.Logger.LogCommandOutput(fmt.Sprintf("Exporting %s\n", name)) + }) + + exitCode = 0 + return exitCode } - // #nosec - err := ioutil.WriteFile("/tmp/.env", []byte(envFile), 0644) + /* + * If not windows, we use a PTY, so there's no need to track + * the environment state here. + */ + envFileName := filepath.Join(e.tmpDirectory, fmt.Sprintf(".env-%d", time.Now().UnixNano())) + err = environment.ToFile(envFileName, func(name string) { + e.Logger.LogCommandOutput(fmt.Sprintf("Exporting %s\n", name)) + }) if err != nil { exitCode = 1 return exitCode } - exitCode = e.RunCommand("source /tmp/.env", true, "") + e.cleanupAfterClose = append(e.cleanupAfterClose, envFileName) + + cmd := fmt.Sprintf("source %s", envFileName) + exitCode = e.RunCommand(cmd, true, "") if exitCode != 0 { return exitCode } - exitCode = e.RunCommand("echo 'source /tmp/.env' >> ~/.bash_profile", true, "") - if exitCode != 0 { - return exitCode + if e.shouldUpdateBashProfile { + cmd = fmt.Sprintf("echo 'source %s' >> ~/.bash_profile", envFileName) + exitCode = e.RunCommand(cmd, true, "") + if exitCode != 0 { + return exitCode + } } return exitCode @@ -145,6 +163,12 @@ func (e *ShellExecutor) InjectFiles(files []api.File) int { e.Logger.LogCommandStarted(directive) + homeDir, err := os.UserHomeDir() + if err != nil { + log.Errorf("Error finding home directory: %v\n", err) + return 1 + } + for _, f := range files { output := fmt.Sprintf("Injecting %s with file mode %s\n", f.Path, f.Mode) @@ -157,45 +181,40 @@ func (e *ShellExecutor) InjectFiles(files []api.File) int { return exitCode } - tmpPath := fmt.Sprintf("%s/file", e.tmpDirectory) - - // #nosec - err = ioutil.WriteFile(tmpPath, []byte(content), 0644) + destPath := f.NormalizePath(homeDir) + err = os.MkdirAll(path.Dir(destPath), 0644) if err != nil { - e.Logger.LogCommandOutput(err.Error() + "\n") - exitCode = 255 + e.Logger.LogCommandOutput(fmt.Sprintf("Failed to create directories for '%s': %v\n", destPath, err)) + exitCode = 1 break } - destPath := "" - - if f.Path[0] == '/' || f.Path[0] == '~' { - destPath = f.Path - } else { - destPath = "~/" + f.Path + // #nosec + destFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644) + if err != nil { + e.Logger.LogCommandOutput(fmt.Sprintf("Failed to create destination path '%s': %v\n", destPath, err)) + exitCode = 1 + break } - cmd := fmt.Sprintf("mkdir -p %s", path.Dir(destPath)) - exitCode = e.RunCommand(cmd, true, "") - if exitCode != 0 { - output := fmt.Sprintf("Failed to create destination path %s", destPath) - e.Logger.LogCommandOutput(output + "\n") + _, err = destFile.Write(content) + if err != nil { + e.Logger.LogCommandOutput(err.Error() + "\n") + exitCode = 255 break } - cmd = fmt.Sprintf("cp %s %s", tmpPath, destPath) - exitCode = e.RunCommand(cmd, true, "") - if exitCode != 0 { - output := fmt.Sprintf("Failed to move to destination path %s %s", tmpPath, destPath) - e.Logger.LogCommandOutput(output + "\n") + fileMode, err := f.ParseMode() + if err != nil { + e.Logger.LogCommandOutput(err.Error() + "\n") + exitCode = 1 break } - cmd = fmt.Sprintf("chmod %s %s", f.Mode, destPath) - exitCode = e.RunCommand(cmd, true, "") - if exitCode != 0 { - output := fmt.Sprintf("Failed to set file mode to %s", f.Mode) - e.Logger.LogCommandOutput(output + "\n") + err = os.Chmod(destPath, fileMode) + if err != nil { + e.Logger.LogCommandOutput(fmt.Sprintf("Failed to set file mode '%s' for '%s': %v\n", f.Mode, destPath, err)) + exitCode = 1 break } } @@ -246,11 +265,28 @@ func (e *ShellExecutor) Stop() int { log.Error(err) } - log.Debug("Process killing finished without errors") + err = e.Shell.Terminate() + if err != nil { + log.Errorf("Error terminating shell: %v", err) + return 1 + } + exitCode := e.Cleanup() + if exitCode != 0 { + log.Errorf("Error cleaning up executor resources: %v", err) + return exitCode + } + + log.Debug("Process killing finished without errors") return 0 } func (e *ShellExecutor) Cleanup() int { + for _, resource := range e.cleanupAfterClose { + if err := os.Remove(resource); err != nil { + log.Errorf("Error removing %s: %v\n", resource, err) + } + } + return 0 } diff --git a/pkg/executors/shell_executor_test.go b/pkg/executors/shell_executor_test.go index aca1ec82..f7c9da61 100644 --- a/pkg/executors/shell_executor_test.go +++ b/pkg/executors/shell_executor_test.go @@ -23,7 +23,7 @@ func Test__ShellExecutor(t *testing.T) { }, } - e := NewShellExecutor(request, testLogger) + e := NewShellExecutor(request, testLogger, true) e.Prepare() e.Start() @@ -35,17 +35,18 @@ func Test__ShellExecutor(t *testing.T) { echo 'etc exists, multiline huzzahh!' fi ` + e.RunCommand(multilineCmd, false, "") envVars := []api.EnvVar{ - api.EnvVar{Name: "A", Value: "Zm9vCg=="}, + {Name: "A", Value: "Zm9vCg=="}, } e.ExportEnvVars(envVars, []config.HostEnvVar{}) e.RunCommand("echo $A", false, "") files := []api.File{ - api.File{ + { Path: "/tmp/random-file.txt", Content: "YWFhYmJiCgo=", Mode: "0600", @@ -102,7 +103,7 @@ func Test__ShellExecutor__StopingRunningJob(t *testing.T) { }, } - e := NewShellExecutor(request, testLogger) + e := NewShellExecutor(request, testLogger, true) e.Prepare() e.Start() @@ -139,7 +140,7 @@ func Test__ShellExecutor__LargeCommandOutput(t *testing.T) { }, } - e := NewShellExecutor(request, testLogger) + e := NewShellExecutor(request, testLogger, true) e.Prepare() e.Start() diff --git a/pkg/executors/ssh_jump_point.go b/pkg/executors/ssh_jump_point.go index 12242de3..df59a9a2 100644 --- a/pkg/executors/ssh_jump_point.go +++ b/pkg/executors/ssh_jump_point.go @@ -1,12 +1,15 @@ package executors -import "os" - -const SSHJumpPointPath = "/tmp/ssh_jump_point" +import ( + "os" + "path/filepath" +) func SetUpSSHJumpPoint(script string) error { + path := filepath.Join(os.TempDir(), "ssh_jump_point") + // #nosec - f, err := os.OpenFile(SSHJumpPointPath, os.O_WRONLY|os.O_CREATE, 0644) + f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return err } diff --git a/pkg/executors/utils.go b/pkg/executors/utils.go deleted file mode 100644 index bb584e69..00000000 --- a/pkg/executors/utils.go +++ /dev/null @@ -1,32 +0,0 @@ -package executors - -import ( - "os" - "regexp" - "runtime" - "strings" -) - -func UserHomeDir() string { - if runtime.GOOS == "windows" { - home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") - if home == "" { - home = os.Getenv("USERPROFILE") - } - return home - } - return os.Getenv("HOME") -} - -func ShellQuote(s string) string { - pattern := regexp.MustCompile(`[^\w@%+=:,./-]`) - - if len(s) == 0 { - return "''" - } - if pattern.MatchString(s) { - return "'" + strings.Replace(s, "'", "'\"'\"'", -1) + "'" - } - - return s -} diff --git a/pkg/jobs/job.go b/pkg/jobs/job.go index 7af5188e..d622a53a 100644 --- a/pkg/jobs/job.go +++ b/pkg/jobs/job.go @@ -2,6 +2,7 @@ package jobs import ( "bytes" + "encoding/base64" "fmt" "net/http" "time" @@ -37,6 +38,7 @@ type JobOptions struct { ExposeKvmDevice bool FileInjections []config.FileInjection FailOnMissingFiles bool + SelfHosted bool } func NewJob(request *api.JobRequest, client *http.Client) (*Job, error) { @@ -46,6 +48,7 @@ func NewJob(request *api.JobRequest, client *http.Client) (*Job, error) { ExposeKvmDevice: true, FileInjections: []config.FileInjection{}, FailOnMissingFiles: false, + SelfHosted: false, }) } @@ -85,7 +88,7 @@ func NewJobWithOptions(options *JobOptions) (*Job, error) { func CreateExecutor(request *api.JobRequest, logger *eventlogger.Logger, jobOptions JobOptions) (executors.Executor, error) { switch request.Executor { case executors.ExecutorTypeShell: - return executors.NewShellExecutor(request, logger), nil + return executors.NewShellExecutor(request, logger, jobOptions.SelfHosted), nil case executors.ExecutorTypeDockerCompose: executorOptions := executors.DockerComposeExecutorOptions{ ExposeKvmDevice: jobOptions.ExposeKvmDevice, @@ -132,13 +135,9 @@ func (job *Job) RunWithOptions(options RunOptions) { if executorRunning { result = job.RunRegularCommands(options.EnvVars) log.Debug("Exporting job result") - job.RunCommandsUntilFirstFailure([]api.Command{ - { - Directive: fmt.Sprintf("export SEMAPHORE_JOB_RESULT=%s", result), - }, - }) if result != JobStopped { + log.Debug("Handling epilogues") job.handleEpilogues(result) } } @@ -204,6 +203,15 @@ func (job *Job) RunRegularCommands(hostEnvVars []config.HostEnvVar) string { } func (job *Job) handleEpilogues(result string) { + envVars := []api.EnvVar{ + {Name: "SEMAPHORE_JOB_RESULT", Value: base64.RawStdEncoding.EncodeToString([]byte(result))}, + } + + exitCode := job.Executor.ExportEnvVars(envVars, []config.HostEnvVar{}) + if exitCode != 0 { + log.Errorf("Error setting SEMAPHORE_JOB_RESULT: exit code %d", exitCode) + } + job.executeIfNotStopped(func() { log.Info("Starting epilogue always commands") job.RunCommandsUntilFirstFailure(job.Request.EpilogueAlwaysCommands) diff --git a/pkg/listener/job_processor.go b/pkg/listener/job_processor.go index 30e19b1d..83e3f33b 100644 --- a/pkg/listener/job_processor.go +++ b/pkg/listener/job_processor.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "os/signal" + "runtime" "syscall" "time" @@ -14,6 +15,7 @@ import ( jobs "github.com/semaphoreci/agent/pkg/jobs" selfhostedapi "github.com/semaphoreci/agent/pkg/listener/selfhostedapi" "github.com/semaphoreci/agent/pkg/retry" + "github.com/semaphoreci/agent/pkg/shell" log "github.com/sirupsen/logrus" ) @@ -172,6 +174,7 @@ func (p *JobProcessor) RunJob(jobID string) { ExposeKvmDevice: false, FileInjections: p.FileInjections, FailOnMissingFiles: p.FailOnMissingFiles, + SelfHosted: true, }) if err != nil { @@ -267,20 +270,27 @@ func (p *JobProcessor) Shutdown(reason ShutdownReason, code int) { } func (p *JobProcessor) executeShutdownHook(reason ShutdownReason) { - if p.ShutdownHookPath != "" { - log.Infof("Executing shutdown hook from %s", p.ShutdownHookPath) + if p.ShutdownHookPath == "" { + return + } - // #nosec - cmd := exec.Command("bash", p.ShutdownHookPath) - cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, fmt.Sprintf("SEMAPHORE_AGENT_SHUTDOWN_REASON=%s", reason)) + var cmd *exec.Cmd + log.Infof("Executing shutdown hook from %s", p.ShutdownHookPath) - output, err := cmd.Output() - if err != nil { - log.Errorf("Error executing shutdown hook: %v", err) - log.Errorf("Output: %s", string(output)) - } else { - log.Infof("Output: %s", string(output)) - } + // #nosec + if runtime.GOOS == "windows" { + args := append(shell.Args(), p.ShutdownHookPath) + cmd = exec.Command(shell.Executable(), args...) + } else { + cmd = exec.Command("bash", p.ShutdownHookPath) + } + + cmd.Env = append(os.Environ(), fmt.Sprintf("SEMAPHORE_AGENT_SHUTDOWN_REASON=%s", reason)) + output, err := cmd.Output() + if err != nil { + log.Errorf("Error executing shutdown hook: %v", err) + log.Errorf("Output: %s", string(output)) + } else { + log.Infof("Output: %s", string(output)) } } diff --git a/pkg/osinfo/name.go b/pkg/osinfo/name.go new file mode 100644 index 00000000..e16e86ec --- /dev/null +++ b/pkg/osinfo/name.go @@ -0,0 +1,17 @@ +// +build !windows + +package osinfo + +import "runtime" + +func Name() string { + switch runtime.GOOS { + case "linux": + return namelinux() + case "darwin": + return namemac() + default: + // TODO handle other OSes + return "" + } +} diff --git a/pkg/osinfo/name_windows.go b/pkg/osinfo/name_windows.go new file mode 100644 index 00000000..6cf0a258 --- /dev/null +++ b/pkg/osinfo/name_windows.go @@ -0,0 +1,16 @@ +package osinfo + +import ( + "fmt" + "golang.org/x/sys/windows" +) + +func Name() string { + info := windows.RtlGetVersion() + return fmt.Sprintf( + "Windows %d.%d - Build %d", + info.MajorVersion, + info.MinorVersion, + info.BuildNumber, + ) +} diff --git a/pkg/osinfo/osinfo.go b/pkg/osinfo/osinfo.go index 7a207e8a..143c8b27 100644 --- a/pkg/osinfo/osinfo.go +++ b/pkg/osinfo/osinfo.go @@ -8,18 +8,6 @@ import ( "strings" ) -func Name() string { - switch runtime.GOOS { - case "linux": - return namelinux() - case "darwin": - return namemac() - default: - // TODO handle other OSes - return "" - } -} - func Hostname() string { hostname, err := os.Hostname() if err != nil { diff --git a/pkg/server/server.go b/pkg/server/server.go index 3c379db2..1007c91c 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "net/http" "os" + "path/filepath" "strconv" handlers "github.com/gorilla/handlers" @@ -103,7 +104,7 @@ func (s *Server) Status(w http.ResponseWriter, r *http.Request) { jsonString, _ := json.Marshal(m) - fmt.Fprintf(w, string(jsonString)) + fmt.Fprint(w, string(jsonString)) } func (s *Server) isAlive(w http.ResponseWriter, r *http.Request) { @@ -144,7 +145,10 @@ func (s *Server) JobLogs(w http.ResponseWriter, r *http.Request) { func (s *Server) AgentLogs(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Type", "text/plain") - logfile, err := os.Open("/tmp/agent_log") + logsPath := filepath.Join(os.TempDir(), "agent_log") + + // #nosec + logfile, err := os.Open(logsPath) if err != nil { w.WriteHeader(404) @@ -206,6 +210,7 @@ func (s *Server) Run(w http.ResponseWriter, r *http.Request) { Client: s.HTTPClient, ExposeKvmDevice: true, FileInjections: []config.FileInjection{}, + SelfHosted: false, }) if err != nil { diff --git a/pkg/shell/env.go b/pkg/shell/env.go new file mode 100644 index 00000000..693f7a02 --- /dev/null +++ b/pkg/shell/env.go @@ -0,0 +1,145 @@ +package shell + +import ( + "fmt" + "io/ioutil" + "regexp" + "sort" + "strings" + + "github.com/semaphoreci/agent/pkg/api" + "github.com/semaphoreci/agent/pkg/config" +) + +type Environment struct { + env map[string]string +} + +func CreateEnvironment(envVars []api.EnvVar, HostEnvVars []config.HostEnvVar) (*Environment, error) { + newEnv := Environment{} + for _, envVar := range envVars { + value, err := envVar.Decode() + if err != nil { + return nil, err + } + + newEnv.Set(envVar.Name, string(value)) + } + + for _, envVar := range HostEnvVars { + newEnv.Set(envVar.Name, envVar.Value) + } + + return &newEnv, nil +} + +/* + * Create an environment by reading a file created with + * an environment dump in Windows with the 'SET > ' command. + */ +func CreateEnvironmentFromFile(fileName string) (*Environment, error) { + // #nosec + bytes, err := ioutil.ReadFile(fileName) + if err != nil { + return nil, err + } + + contents := string(bytes) + contents = strings.TrimSpace(contents) + contents = strings.Replace(contents, "\r\n", "\n", -1) + + lines := strings.Split(contents, "\n") + environment := Environment{env: map[string]string{}} + + for _, line := range lines { + nameAndValue := strings.SplitN(line, "=", 2) + if len(nameAndValue) == 2 { + environment.Set(nameAndValue[0], nameAndValue[1]) + } + } + + return &environment, nil +} + +func (e *Environment) IsEmpty() bool { + return e.env != nil || len(e.env) == 0 +} + +func (e *Environment) Set(name, value string) { + if e.env == nil { + e.env = map[string]string{} + } + + e.env[name] = value +} + +func (e *Environment) Get(key string) (string, bool) { + v, ok := e.env[key] + return v, ok +} + +func (e *Environment) Remove(key string) { + _, ok := e.Get(key) + if ok { + delete(e.env, key) + } +} + +func (e *Environment) Keys() []string { + var keys []string + for k := range e.env { + keys = append(keys, k) + } + + sort.Strings(keys) + return keys +} + +func (e *Environment) Append(otherEnv *Environment, callback func(name, value string)) { + for _, name := range otherEnv.Keys() { + value, _ := otherEnv.Get(name) + e.Set(name, value) + if callback != nil { + callback(name, value) + } + } +} + +func (e *Environment) ToSlice() []string { + arr := []string{} + for name, value := range e.env { + arr = append(arr, fmt.Sprintf("%s=%s", name, value)) + } + + return arr +} + +func (e *Environment) ToFile(fileName string, callback func(name string)) error { + fileContent := "" + for _, name := range e.Keys() { + value, _ := e.Get(name) + fileContent += fmt.Sprintf("export %s=%s\n", name, shellQuote(value)) + callback(name) + } + + // #nosec + err := ioutil.WriteFile(fileName, []byte(fileContent), 0644) + if err != nil { + return err + } + + return nil +} + +func shellQuote(s string) string { + pattern := regexp.MustCompile(`[^\w@%+=:,./-]`) + + if len(s) == 0 { + return "''" + } + if pattern.MatchString(s) { + return "'" + strings.Replace(s, "'", "'\"'\"'", -1) + "'" + } + + return s +} diff --git a/pkg/shell/output_buffer.go b/pkg/shell/output_buffer.go index 7d297426..b4ab9c7a 100644 --- a/pkg/shell/output_buffer.go +++ b/pkg/shell/output_buffer.go @@ -58,7 +58,7 @@ func (b *OutputBuffer) Flush() (string, bool) { timeSinceLastAppend := 1 * time.Millisecond if b.lastAppend != nil { - timeSinceLastAppend = time.Now().Sub(*b.lastAppend) + timeSinceLastAppend = time.Since(*b.lastAppend) } log.Debugf("Flushing. %d bytes in the buffer", len(b.bytes)) diff --git a/pkg/shell/output_buffer_test.go b/pkg/shell/output_buffer_test.go index a87978d5..04d07689 100644 --- a/pkg/shell/output_buffer_test.go +++ b/pkg/shell/output_buffer_test.go @@ -31,14 +31,14 @@ func Test__OutputBuffer__SimpleAscii__ShorterThanMinimalCutLength(t *testing.T) input := []byte("aaa") buffer.Append(input) - flushed, ok := buffer.Flush() + _, ok := buffer.Flush() // We need to wait a bit before flushing, the buffer is still too short assert.Equal(t, ok, false) time.Sleep(OutputBufferMaxTimeSinceLastAppend) - flushed, ok = buffer.Flush() + flushed, ok := buffer.Flush() assert.Equal(t, ok, true) assert.Equal(t, flushed, string(input)) } diff --git a/pkg/shell/process.go b/pkg/shell/process.go index a1fbe167..17839094 100644 --- a/pkg/shell/process.go +++ b/pkg/shell/process.go @@ -3,64 +3,91 @@ package shell import ( "flag" "fmt" + "io" "io/ioutil" "math/rand" + "os" + "os/exec" + "path/filepath" "regexp" + "runtime" "strconv" "strings" + "syscall" "time" log "github.com/sirupsen/logrus" ) -type Process struct { - Command string - Shell *Shell - - StartedAt int - FinishedAt int - ExitCode int +/* + * Windows does not support a PTY yet. To allow changing directories, + * and setting/unsetting environment variables, we need to keep track + * of the environment on every command executed. We do that by + * getting the whole environment after a command is executed and + * updating our shell with it. + */ +const WindowsPwshScript = ` +$ErrorActionPreference = "STOP" +%s +if ($LASTEXITCODE -eq $null) {$Env:SEMAPHORE_AGENT_CURRENT_CMD_EXIT_STATUS = 0} else {$Env:SEMAPHORE_AGENT_CURRENT_CMD_EXIT_STATUS = $LASTEXITCODE} +$Env:SEMAPHORE_AGENT_CURRENT_DIR = $PWD | Select-Object -ExpandProperty Path +Get-ChildItem Env: | Foreach-Object {"$($_.Name)=$($_.Value)"} | Set-Content "%s.env.after" +exit $Env:SEMAPHORE_AGENT_CURRENT_CMD_EXIT_STATUS +` + +type Config struct { + Shell *Shell + StoragePath string + Command string +} +type Process struct { + Command string + Shell *Shell + StoragePath string + StartedAt int + FinishedAt int + ExitCode int OnStdoutCallback func(string) - - startMark string - endMark string - commandEndRegex *regexp.Regexp - - tempStoragePath string - cmdFilePath string - - inputBuffer []byte - outputBuffer *OutputBuffer + Pid int + startMark string + endMark string + commandEndRegex *regexp.Regexp + inputBuffer []byte + outputBuffer *OutputBuffer + SysProcAttr *syscall.SysProcAttr } func randomMagicMark() string { return fmt.Sprintf("949556c7-%d", time.Now().Unix()) } -func NewProcess(cmd string, tempStoragePath string, shell *Shell) *Process { +func NewProcess(config Config) *Process { startMark := randomMagicMark() + "-start" endMark := randomMagicMark() + "-end" commandEndRegex := regexp.MustCompile(endMark + " " + `(\d+)` + "[\r\n]+") return &Process{ - Command: cmd, - ExitCode: 1, - - Shell: shell, - - startMark: startMark, - endMark: endMark, - + Shell: config.Shell, + StoragePath: config.StoragePath, + Command: config.Command, + ExitCode: 1, + startMark: startMark, + endMark: endMark, commandEndRegex: commandEndRegex, - tempStoragePath: tempStoragePath, - cmdFilePath: tempStoragePath + "/current-agent-cmd", - - outputBuffer: NewOutputBuffer(), + outputBuffer: NewOutputBuffer(), } } +func (p *Process) CmdFilePath() string { + return filepath.Join(p.StoragePath, "current-agent-cmd") +} + +func (p *Process) EnvironmentFilePath() string { + return fmt.Sprintf("%s.env.after", p.CmdFilePath()) +} + func (p *Process) OnStdout(callback func(string)) { p.OnStdoutCallback = callback } @@ -105,7 +132,6 @@ func (p *Process) flushInputBufferTill(index int) { func (p *Process) Run() { instruction := p.constructShellInstruction() - p.StartedAt = int(time.Now().Unix()) defer func() { p.FinishedAt = int(time.Now().Unix()) @@ -117,7 +143,132 @@ func (p *Process) Run() { return } - _, err = p.Shell.Write(instruction) + /* + * If the agent is running in an non-windows environment, + * we use a PTY session to run commands. + */ + if runtime.GOOS != "windows" { + p.runWithPTY(instruction) + return + } + + // In windows, so no PTY support. + p.setup() + p.runWithoutPTY(instruction) + + /* + * If we are not using a PTY, we need to keep track of shell "state" ourselves. + * We use a file with all the environment variables available after the command + * is executed. From that file, we can update our shell "state". + */ + after, _ := CreateEnvironmentFromFile(p.EnvironmentFilePath()) + + /* + * CMD.exe does not have an environment variable such as $PWD, + * so we use a custom one to get the current working directory + * after a command is executed. + */ + newCwd, _ := after.Get("SEMAPHORE_AGENT_CURRENT_DIR") + p.Shell.Chdir(newCwd) + + /* + * We use two custom environment variables to track + * things we need, but we don't want to mess the environment + * so we remove them before updating our shell state. + */ + after.Remove("SEMAPHORE_AGENT_CURRENT_DIR") + after.Remove("SEMAPHORE_AGENT_CURRENT_CMD_EXIT_STATUS") + p.Shell.UpdateEnvironment(after) +} + +func (p *Process) runWithoutPTY(instruction string) { + cmd, reader, writer := p.buildNonPTYCommand(instruction) + err := cmd.Start() + if err != nil { + log.Errorf("Error starting command: %v\n", err) + p.ExitCode = 1 + return + } + + /* + * In Windows, we need to assign the process created by the command + * with the job object handle we created for it earlier, + * for process termination purposes. + */ + p.Pid = cmd.Process.Pid + err = p.afterCreation(p.Shell.windowsJobObject) + if err != nil { + log.Errorf("Process after creation procedure failed: %v", err) + } + + /* + * Start reading the command's output and wait until it finishes. + */ + done := make(chan bool, 1) + go p.readNonPTY(reader, done) + waitResult := cmd.Wait() + + /* + * Command is done, We close our output writer + * so our output reader knows the command is over. + */ + err = writer.Close() + if err != nil { + log.Errorf("Error closing writer: %v", err) + } + + /* + * Let's wait for the reader to finish, just to make sure + * we don't leave any goroutines hanging around. + */ + log.Debug("Waiting for reading to finish") + <-done + + /* + * The command was successful, so we just return. + */ + if waitResult == nil { + p.ExitCode = 0 + return + } + + /* + * The command returned an error, so we need to figure out the exit code from it. + * If we can't figure it out, we just use 1 and carry on. + */ + if err, ok := waitResult.(*exec.ExitError); ok { + if s, ok := err.Sys().(syscall.WaitStatus); ok { + p.ExitCode = s.ExitStatus() + } else { + log.Errorf("Could not cast *exec.ExitError to syscall.WaitStatus: %v\n", err) + p.ExitCode = 1 + } + } else { + log.Errorf("Unexpected %T returned after Wait(): %v", waitResult, waitResult) + p.ExitCode = 1 + } +} + +func (p *Process) buildNonPTYCommand(instruction string) (*exec.Cmd, *io.PipeReader, *io.PipeWriter) { + args := append(p.Shell.Args, instruction) + + // #nosec + cmd := exec.Command(p.Shell.Executable, args...) + cmd.Dir = p.Shell.Cwd + cmd.SysProcAttr = p.SysProcAttr + + if p.Shell.Env != nil { + cmd.Env = append(os.Environ(), p.Shell.Env.ToSlice()...) + } + + reader, writer := io.Pipe() + cmd.Stdout = writer + cmd.Stderr = writer + return cmd, reader, writer +} + +func (p *Process) runWithPTY(instruction string) { + _, err := p.Shell.Write(instruction) if err != nil { log.Errorf("Error writing instruction: %v", err) return @@ -127,6 +278,10 @@ func (p *Process) Run() { } func (p *Process) constructShellInstruction() string { + if runtime.GOOS == "windows" { + return fmt.Sprintf(`%s.ps1`, p.CmdFilePath()) + } + // // A process is sending a complex instruction to the shell. The instruction // does the following: @@ -139,22 +294,27 @@ func (p *Process) constructShellInstruction() string { // template := `echo -e "\001 %s"; source %s; AGENT_CMD_RESULT=$?; echo -e "\001 %s $AGENT_CMD_RESULT"; echo "exit $AGENT_CMD_RESULT" | sh` - return fmt.Sprintf(template, p.startMark, p.cmdFilePath, p.endMark) + return fmt.Sprintf(template, p.startMark, p.CmdFilePath(), p.endMark) } +/* + * Multiline commands don't work very well with the start/finish marker. + * scheme. To circumvent this, we are storing the command in a file. + */ func (p *Process) loadCommand() error { - // - // Multiline commands don't work very well with the start/finish marker - // scheme. To circumvent this, we are storing the command in a file - // + if runtime.GOOS != "windows" { + return p.writeCommandToFile(p.CmdFilePath(), p.Command) + } + + cmdFilePath := fmt.Sprintf("%s.ps1", p.CmdFilePath()) + command := fmt.Sprintf(WindowsPwshScript, p.Command, p.CmdFilePath()) + return p.writeCommandToFile(cmdFilePath, command) +} +func (p *Process) writeCommandToFile(cmdFilePath, command string) error { // #nosec - err := ioutil.WriteFile(p.cmdFilePath, []byte(p.Command), 0644) + err := ioutil.WriteFile(cmdFilePath, []byte(command), 0644) if err != nil { - // TODO: log something - // e.Logger.LogCommandStarted(command) - // e.Logger.LogCommandOutput(fmt.Sprintf("Failed to run command: %+v\n", err)) - return err } @@ -179,6 +339,30 @@ func (p *Process) readBufferSize() int { return rand.Intn(max-min) + min } +func (p *Process) readNonPTY(reader *io.PipeReader, done chan bool) { + for { + log.Debug("Reading started") + buffer := make([]byte, p.readBufferSize()) + n, err := reader.Read(buffer) + if err != nil && err != io.EOF { + log.Errorf("Error while reading. Error: %v", err) + } + + p.inputBuffer = append(p.inputBuffer, buffer[0:n]...) + log.Debugf("reading data from command. Input buffer: %#v", string(p.inputBuffer)) + p.flushInputAll() + p.StreamToStdout() + + if err == io.EOF { + log.Debug("Finished reading") + p.flushOutputBuffer() + break + } + } + + done <- true +} + // // Read state from shell into the inputBuffer // diff --git a/pkg/shell/process_setup.go b/pkg/shell/process_setup.go new file mode 100644 index 00000000..1cdbfe83 --- /dev/null +++ b/pkg/shell/process_setup.go @@ -0,0 +1,17 @@ +// +build !windows + +package shell + +/* + * For non-windows agents, we handle job termination + * by closing the TTY associated with the job. + * Therefore, no special handling here is necessary. + */ + +func (p *Process) setup() { + +} + +func (p *Process) afterCreation(jobObject uintptr) error { + return nil +} diff --git a/pkg/shell/process_setup_windows.go b/pkg/shell/process_setup_windows.go new file mode 100644 index 00000000..553a2c22 --- /dev/null +++ b/pkg/shell/process_setup_windows.go @@ -0,0 +1,30 @@ +// +build windows + +package shell + +import ( + "golang.org/x/sys/windows" +) + +func (p *Process) setup() { + p.SysProcAttr = &windows.SysProcAttr{ + CreationFlags: windows.CREATE_UNICODE_ENVIRONMENT | windows.CREATE_NEW_PROCESS_GROUP, + } +} + +func (p *Process) afterCreation(jobObject uintptr) error { + permissions := uint32(windows.PROCESS_QUERY_LIMITED_INFORMATION | windows.PROCESS_SET_QUOTA | windows.PROCESS_TERMINATE) + processHandle, err := windows.OpenProcess(permissions, false, uint32(p.Pid)) + if err != nil { + return err + } + + defer windows.CloseHandle(processHandle) + + err = windows.AssignProcessToJobObject(windows.Handle(jobObject), processHandle) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/shell/pty.go b/pkg/shell/pty.go new file mode 100644 index 00000000..4538a9cd --- /dev/null +++ b/pkg/shell/pty.go @@ -0,0 +1,14 @@ +// +build !windows + +package shell + +import ( + "os" + "os/exec" + + pty "github.com/creack/pty" +) + +func StartPTY(command *exec.Cmd) (*os.File, error) { + return pty.Start(command) +} diff --git a/pkg/shell/pty_windows.go b/pkg/shell/pty_windows.go new file mode 100644 index 00000000..c7b8fa1c --- /dev/null +++ b/pkg/shell/pty_windows.go @@ -0,0 +1,13 @@ +// +build windows + +package shell + +import ( + "errors" + "os" + "os/exec" +) + +func StartPTY(c *exec.Cmd) (*os.File, error) { + return nil, errors.New("PTY is not supported on Windows") +} diff --git a/pkg/shell/shell.go b/pkg/shell/shell.go index 42b2cab1..ded0767e 100644 --- a/pkg/shell/shell.go +++ b/pkg/shell/shell.go @@ -6,34 +6,68 @@ import ( "fmt" "os" "os/exec" + "runtime" "strings" "time" - pty "github.com/creack/pty" log "github.com/sirupsen/logrus" ) type Shell struct { + Executable string + Args []string BootCommand *exec.Cmd StoragePath string TTY *os.File ExitSignal chan string + Env *Environment + Cwd string + + /* + * A job object handle used to interrupt the command + * process in case of a stop request. + */ + windowsJobObject uintptr } -func NewShell(bootCommand *exec.Cmd, storagePath string) (*Shell, error) { +func NewShell(storagePath string) (*Shell, error) { + return NewShellFromExecAndArgs(Executable(), Args(), storagePath) +} + +func NewShellFromExecAndArgs(executable string, args []string, storagePath string) (*Shell, error) { exitChannel := make(chan string, 1) + cwd, err := os.Getwd() + if err != nil { + return nil, fmt.Errorf("error finding current working directory: %v", err) + } + return &Shell{ - BootCommand: bootCommand, + Executable: executable, + Args: args, StoragePath: storagePath, ExitSignal: exitChannel, + Env: &Environment{}, + Cwd: cwd, }, nil } func (s *Shell) Start() error { + + /* + * In windows, we don't use a PTY, so we need to create a job object + * to assign the processes started by the user. + */ + if runtime.GOOS == "windows" { + s.Setup() + return nil + } + log.Debug("Starting stateful shell") - tty, err := pty.Start(s.BootCommand) + // #nosec + s.BootCommand = exec.Command(s.Executable, s.Args...) + tty, err := StartPTY(s.BootCommand) if err != nil { log.Errorf("Failed to start stateful shell: %v", err) return err @@ -182,7 +216,12 @@ func (s *Shell) silencePromptAndDisablePS1() error { } func (s *Shell) NewProcess(command string) *Process { - return NewProcess(command, s.StoragePath, s) + return NewProcess( + Config{ + Command: command, + Shell: s, + StoragePath: s.StoragePath, + }) } func (s *Shell) Close() error { @@ -194,7 +233,7 @@ func (s *Shell) Close() error { } } - if s.BootCommand.Process != nil { + if s.BootCommand != nil && s.BootCommand.Process != nil { err := s.BootCommand.Process.Kill() if err != nil && !errors.Is(err, os.ErrProcessDone) { log.Errorf("Process killing procedure returned an error %+v", err) @@ -204,3 +243,29 @@ func (s *Shell) Close() error { return nil } + +func (s *Shell) Chdir(newCwd string) { + if newCwd != s.Cwd { + s.Cwd = newCwd + } +} + +func (s *Shell) UpdateEnvironment(newEnvironment *Environment) { + s.Env = newEnvironment +} + +func Executable() string { + if runtime.GOOS == "windows" { + return "powershell" + } + + return "bash" +} + +func Args() []string { + if runtime.GOOS == "windows" { + return []string{"-NoProfile", "-NonInteractive"} + } + + return []string{"--login"} +} diff --git a/pkg/shell/shell_setup.go b/pkg/shell/shell_setup.go new file mode 100644 index 00000000..d6f67c23 --- /dev/null +++ b/pkg/shell/shell_setup.go @@ -0,0 +1,17 @@ +// +build !windows + +package shell + +/* + * For non-windows agents, we handle job termination + * by closing the TTY associated with the job. + * Therefore, no special handling here is necessary. + */ + +func (s *Shell) Setup() { + +} + +func (s *Shell) Terminate() error { + return nil +} diff --git a/pkg/shell/shell_setup_windows.go b/pkg/shell/shell_setup_windows.go new file mode 100644 index 00000000..6f04edcf --- /dev/null +++ b/pkg/shell/shell_setup_windows.go @@ -0,0 +1,44 @@ +// +build windows + +package shell + +import ( + "unsafe" + + log "github.com/sirupsen/logrus" + "golang.org/x/sys/windows" +) + +func (s *Shell) Setup() { + jobObject, err := windows.CreateJobObject(nil, nil) + if err != nil { + log.Errorf("Error creating job object: %v", err) + return + } + + info := windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION{ + BasicLimitInformation: windows.JOBOBJECT_BASIC_LIMIT_INFORMATION{ + LimitFlags: windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, + }, + } + + _, err = windows.SetInformationJobObject( + jobObject, + windows.JobObjectExtendedLimitInformation, + uintptr(unsafe.Pointer(&info)), + uint32(unsafe.Sizeof(info)), + ) + + if err != nil { + log.Errorf("Error setting job object information: %v", err) + return + } + + log.Debugf("Successfully created job object: %v", jobObject) + s.windowsJobObject = uintptr(jobObject) +} + +func (s *Shell) Terminate() error { + log.Debugf("Terminating all processes assigned to job object %v", s.windowsJobObject) + return windows.CloseHandle(windows.Handle(s.windowsJobObject)) +} diff --git a/pkg/shell/shell_test.go b/pkg/shell/shell_test.go index f63ace0c..91d612cf 100644 --- a/pkg/shell/shell_test.go +++ b/pkg/shell/shell_test.go @@ -4,7 +4,6 @@ import ( "bytes" "io/ioutil" "log" - "os/exec" "testing" assert "github.com/stretchr/testify/assert" @@ -80,9 +79,7 @@ func tempStorageFolder() string { func bashShell() *Shell { dir := tempStorageFolder() - cmd := exec.Command("bash", "--login") - - shell, _ := NewShell(cmd, dir) + shell, _ := NewShell(dir) shell.Start() return shell diff --git a/scripts/provision-windows-box.ps1 b/scripts/provision-windows-box.ps1 new file mode 100644 index 00000000..9216465c --- /dev/null +++ b/scripts/provision-windows-box.ps1 @@ -0,0 +1,43 @@ +$ErrorActionPreference = "Stop" + +function Add-To-Path { + param ( + $Path + ) + + $Content = "if (-not (`$env:Path.Split(';').Contains('$Path'))) { `$env:Path = '$Path;' + `$env:Path }" + $ProfilePath = $profile.AllUsersAllHosts + Write-Output "Adding $Path to `$env:Path, using profile $ProfilePath..." + + if (-not (Test-Path $profilePath)) { + Write-Output "$ProfilePath does not exist. Creating it..." + New-Item $ProfilePath > $null + Set-Content $ProfilePath $Content + } else { + Add-Content -Path $ProfilePath -Value $Content + } +} + +Write-Output "Installing golang..." +choco install -y golang +If ($lastexitcode -ne 0) { Exit $lastexitcode } + +Write-Output "Installing Git for Windows" +choco install -y git --version 2.31.0 +If ($lastexitcode -ne 0) { Exit $lastexitcode } + +Add-To-Path -Path "C:\Program Files\Go\bin" +Add-To-Path -Path "C:\Program Files\Git\bin" + +Write-Output "Importing the choco profile module..." +$ChocolateyInstall = Convert-Path "$((Get-Command choco).path)\..\.." +Import-Module "$ChocolateyInstall\helpers\chocolateyProfile.psm1" +Write-Output "Refreshing the current session environment..." +Update-SessionEnvironment + +go version +If ($lastexitcode -ne 0) { Exit $lastexitcode } + +# no mismatched line endings +git config --system core.autocrlf false +If ($lastexitcode -ne 0) { Exit $lastexitcode } diff --git a/test/e2e/docker/broken_unicode.rb b/test/e2e/docker/broken_unicode.rb index 2f18c62b..de01ac9b 100644 --- a/test/e2e/docker/broken_unicode.rb +++ b/test/e2e/docker/broken_unicode.rb @@ -60,8 +60,9 @@ {"event":"cmd_output", "timestamp":"*", "output":"\ufffd\ufffd\ufffd\ufffd\ufffd"} {"event":"cmd_finished", "timestamp":"*", "directive": "echo | awk '{ printf(\\\"%c%c%c%c%c\\\", 150, 150, 150, 150, 150) }'","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/docker/check_dev_kvm.rb b/test/e2e/docker/check_dev_kvm.rb index 14384bf8..4396ec15 100644 --- a/test/e2e/docker/check_dev_kvm.rb +++ b/test/e2e/docker/check_dev_kvm.rb @@ -59,8 +59,10 @@ {"event":"cmd_output", "timestamp":"*", "output":"kvm\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"ls /dev | grep kvm","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG when "listen" then @@ -82,8 +84,10 @@ {"event":"cmd_started", "timestamp":"*", "directive":"ls /dev | grep kvm"} {"event":"cmd_finished", "timestamp":"*", "directive":"ls /dev | grep kvm","event":"cmd_finished","exit_code":1,"finished_at":"*","started_at":"*","timestamp":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"failed"} LOG diff --git a/test/e2e/docker/command_aliases.rb b/test/e2e/docker/command_aliases.rb index acc109da..d57de016 100644 --- a/test/e2e/docker/command_aliases.rb +++ b/test/e2e/docker/command_aliases.rb @@ -56,7 +56,8 @@ {"event":"cmd_output", "timestamp":"*", "output":"Running: echo Hello World\\n"} {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"Display Hello World","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/docker/container_custom_name.rb b/test/e2e/docker/container_custom_name.rb index 342e3eff..977bb276 100644 --- a/test/e2e/docker/container_custom_name.rb +++ b/test/e2e/docker/container_custom_name.rb @@ -83,7 +83,9 @@ {"event":"cmd_output", "timestamp":"*", "output":"$PATH:/etc/a\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"echo $D","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/docker/container_env_vars.rb b/test/e2e/docker/container_env_vars.rb index 9967c7e4..230e644b 100644 --- a/test/e2e/docker/container_env_vars.rb +++ b/test/e2e/docker/container_env_vars.rb @@ -60,7 +60,9 @@ {"event":"cmd_output", "timestamp":"*", "output":"bar\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"echo $FOO","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/docker/container_options.rb b/test/e2e/docker/container_options.rb index 8a3bbc60..2f807fa2 100644 --- a/test/e2e/docker/container_options.rb +++ b/test/e2e/docker/container_options.rb @@ -61,7 +61,9 @@ {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello World"} {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello World","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/docker/docker_in_docker.rb b/test/e2e/docker/docker_in_docker.rb index 274047fc..7018e5d8 100644 --- a/test/e2e/docker/docker_in_docker.rb +++ b/test/e2e/docker/docker_in_docker.rb @@ -55,7 +55,9 @@ {"event":"cmd_started", "timestamp":"*", "directive":"docker run alpine uname 2>/dev/null | grep Linux"} {"event":"cmd_output", "timestamp":"*", "output":"Linux\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"docker run alpine uname 2>/dev/null | grep Linux","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/docker/docker_private_image_gcr.rb b/test/e2e/docker/docker_private_image_gcr.rb index fac6ab3a..0454d210 100644 --- a/test/e2e/docker/docker_private_image_gcr.rb +++ b/test/e2e/docker/docker_private_image_gcr.rb @@ -77,7 +77,9 @@ {"directive":"echo Hello World","event":"cmd_started","timestamp":"*"} {"event":"cmd_output","output":"Hello World\\n","timestamp":"*"} {"directive":"echo Hello World","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} - {"directive":"export SEMAPHORE_JOB_RESULT=passed","event":"cmd_started","timestamp":"*"} - {"directive":"export SEMAPHORE_JOB_RESULT=passed","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished","result":"passed","timestamp":"*"} LOG diff --git a/test/e2e/docker/docker_registry_private_image.rb b/test/e2e/docker/docker_registry_private_image.rb index 93997489..062559bf 100644 --- a/test/e2e/docker/docker_registry_private_image.rb +++ b/test/e2e/docker/docker_registry_private_image.rb @@ -80,7 +80,9 @@ {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello World"} {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello World","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/docker/dockerhub_private_image.rb b/test/e2e/docker/dockerhub_private_image.rb index 6982d5b1..c2391a31 100644 --- a/test/e2e/docker/dockerhub_private_image.rb +++ b/test/e2e/docker/dockerhub_private_image.rb @@ -78,7 +78,9 @@ {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello World"} {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello World","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/docker/env_vars.rb b/test/e2e/docker/env_vars.rb index 0fb40206..cf52729e 100644 --- a/test/e2e/docker/env_vars.rb +++ b/test/e2e/docker/env_vars.rb @@ -83,7 +83,9 @@ {"event":"cmd_output", "timestamp":"*", "output":"$PATH:/etc/a\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"echo $D","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/docker/epilogue_on_fail.rb b/test/e2e/docker/epilogue_on_fail.rb index 7c9d5d08..88cb990b 100644 --- a/test/e2e/docker/epilogue_on_fail.rb +++ b/test/e2e/docker/epilogue_on_fail.rb @@ -27,15 +27,15 @@ ], "epilogue_always_commands": [ - { "directive": "echo Hello Epilogue" } + { "directive": "echo Hello Epilogue $SEMAPHORE_JOB_RESULT" } ], "epilogue_on_pass_commands": [ - { "directive": "echo Hello On Pass Epilogue" } + { "directive": "echo Hello On Pass Epilogue $SEMAPHORE_JOB_RESULT" } ], "epilogue_on_fail_commands": [ - { "directive": "echo Hello On Fail Epilogue" } + { "directive": "echo Hello On Fail Epilogue $SEMAPHORE_JOB_RESULT" } ], "callbacks": { @@ -67,16 +67,17 @@ {"event":"cmd_started", "timestamp":"*", "directive":"false"} {"event":"cmd_finished", "timestamp":"*", "directive":"false","exit_code":1,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello Epilogue"} - {"event":"cmd_output", "timestamp":"*", "output":"Hello Epilogue\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello Epilogue","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello Epilogue $SEMAPHORE_JOB_RESULT"} + {"event":"cmd_output", "timestamp":"*", "output":"Hello Epilogue failed\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello Epilogue $SEMAPHORE_JOB_RESULT","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello On Fail Epilogue"} - {"event":"cmd_output", "timestamp":"*", "output":"Hello On Fail Epilogue\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello On Fail Epilogue","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello On Fail Epilogue $SEMAPHORE_JOB_RESULT"} + {"event":"cmd_output", "timestamp":"*", "output":"Hello On Fail Epilogue failed\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello On Fail Epilogue $SEMAPHORE_JOB_RESULT","exit_code":0,"finished_at":"*","started_at":"*"} {"event":"job_finished", "timestamp":"*", "result":"failed"} LOG diff --git a/test/e2e/docker/epilogue_on_pass.rb b/test/e2e/docker/epilogue_on_pass.rb index 0786c07e..0614fd55 100644 --- a/test/e2e/docker/epilogue_on_pass.rb +++ b/test/e2e/docker/epilogue_on_pass.rb @@ -27,15 +27,15 @@ ], "epilogue_always_commands": [ - { "directive": "echo Hello Epilogue" } + { "directive": "echo Hello Epilogue $SEMAPHORE_JOB_RESULT" } ], "epilogue_on_pass_commands": [ - { "directive": "echo Hello On Pass Epilogue" } + { "directive": "echo Hello On Pass Epilogue $SEMAPHORE_JOB_RESULT" } ], "epilogue_on_fail_commands": [ - { "directive": "echo Hello On Fail Epilogue" } + { "directive": "echo Hello On Fail Epilogue $SEMAPHORE_JOB_RESULT" } ], "callbacks": { @@ -68,16 +68,17 @@ {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello World","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello Epilogue"} - {"event":"cmd_output", "timestamp":"*", "output":"Hello Epilogue\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello Epilogue","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello Epilogue $SEMAPHORE_JOB_RESULT"} + {"event":"cmd_output", "timestamp":"*", "output":"Hello Epilogue passed\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello Epilogue $SEMAPHORE_JOB_RESULT","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello On Pass Epilogue"} - {"event":"cmd_output", "timestamp":"*", "output":"Hello On Pass Epilogue\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello On Pass Epilogue","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello On Pass Epilogue $SEMAPHORE_JOB_RESULT"} + {"event":"cmd_output", "timestamp":"*", "output":"Hello On Pass Epilogue passed\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello On Pass Epilogue $SEMAPHORE_JOB_RESULT","exit_code":0,"finished_at":"*","started_at":"*"} {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/docker/failed_job.rb b/test/e2e/docker/failed_job.rb index 1475322d..51ac3392 100644 --- a/test/e2e/docker/failed_job.rb +++ b/test/e2e/docker/failed_job.rb @@ -54,7 +54,9 @@ {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} {"event":"cmd_started", "timestamp":"*", "directive":"false"} {"event":"cmd_finished", "timestamp":"*", "directive":"false","exit_code":1,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"failed"} LOG diff --git a/test/e2e/docker/file_injection.rb b/test/e2e/docker/file_injection.rb index 01384668..4289f145 100644 --- a/test/e2e/docker/file_injection.rb +++ b/test/e2e/docker/file_injection.rb @@ -23,7 +23,7 @@ "files": [ { "path": "test.txt", "content": "#{`echo "hello" | base64 | tr -d '\n'`}", "mode": "0644" }, { "path": "/a/b/c", "content": "#{`echo "hello" | base64 | tr -d '\n'`}", "mode": "0644" }, - { "path": "/tmp/a", "content": "#{`echo "hello" | base64 | tr -d '\n'`}", "mode": "+x" } + { "path": "/tmp/a", "content": "#{`echo "hello" | base64 | tr -d '\n'`}", "mode": "0600" } ], "commands": [ @@ -61,7 +61,7 @@ {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} {"event":"cmd_output", "timestamp":"*", "output":"Injecting test.txt with file mode 0644\\n"} {"event":"cmd_output", "timestamp":"*", "output":"Injecting /a/b/c with file mode 0644\\n"} - {"event":"cmd_output", "timestamp":"*", "output":"Injecting /tmp/a with file mode +x\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Injecting /tmp/a with file mode 0600\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} {"event":"cmd_started", "timestamp":"*", "directive":"cat test.txt"} @@ -73,10 +73,12 @@ {"event":"cmd_finished", "timestamp":"*", "directive":"cat /a/b/c","exit_code":0,"finished_at":"*","started_at":"*"} {"event":"cmd_started", "timestamp":"*", "directive":"stat -c '%a' /tmp/a"} - {"event":"cmd_output", "timestamp":"*", "output":"755\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"600\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"stat -c '%a' /tmp/a","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/docker/file_injection_broken_file_mode.rb b/test/e2e/docker/file_injection_broken_file_mode.rb index ad4e3a87..ba118a5a 100644 --- a/test/e2e/docker/file_injection_broken_file_mode.rb +++ b/test/e2e/docker/file_injection_broken_file_mode.rb @@ -61,7 +61,9 @@ {"event":"cmd_output", "timestamp":"*", "output":"Failed to set file mode to obviously broken\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":1,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"failed"} LOG diff --git a/test/e2e/docker/hello_world.rb b/test/e2e/docker/hello_world.rb index d56577da..cbca96ce 100644 --- a/test/e2e/docker/hello_world.rb +++ b/test/e2e/docker/hello_world.rb @@ -55,7 +55,9 @@ {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello World"} {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello World","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/docker/host_setup_commands.rb b/test/e2e/docker/host_setup_commands.rb index dd77fe2e..4241b0fe 100644 --- a/test/e2e/docker/host_setup_commands.rb +++ b/test/e2e/docker/host_setup_commands.rb @@ -58,7 +58,9 @@ {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello World"} {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello World","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/docker/job_stopping_on_epilogue.rb b/test/e2e/docker/job_stopping_on_epilogue.rb index 4fe622fa..5ccbbe85 100644 --- a/test/e2e/docker/job_stopping_on_epilogue.rb +++ b/test/e2e/docker/job_stopping_on_epilogue.rb @@ -44,8 +44,10 @@ {"event":"cmd_started", "timestamp":"*", "directive":"echo 'here'"} {"event":"cmd_output", "timestamp":"*", "output":"here\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"echo 'here'","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"sleep infinity"} {"event":"cmd_finished", "timestamp":"*", "directive":"sleep infinity","exit_code":1,"finished_at":"*","started_at":"*"} {"event":"job_finished", "timestamp":"*", "result":"stopped"} diff --git a/test/e2e/docker/multiple_containers.rb b/test/e2e/docker/multiple_containers.rb index fbb6f51f..4c8cbad3 100644 --- a/test/e2e/docker/multiple_containers.rb +++ b/test/e2e/docker/multiple_containers.rb @@ -59,7 +59,9 @@ {"event":"cmd_started", "timestamp":"*", "directive":"docker ps -a | grep postgres | wc -l"} {"event":"cmd_output", "timestamp":"*", "output":"1\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"docker ps -a | grep postgres | wc -l","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/docker/stty_restoration.rb b/test/e2e/docker/stty_restoration.rb index 8c5356f9..a2bac135 100644 --- a/test/e2e/docker/stty_restoration.rb +++ b/test/e2e/docker/stty_restoration.rb @@ -63,8 +63,9 @@ {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello World","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/docker/unicode.rb b/test/e2e/docker/unicode.rb index 33b13d72..413c3c29 100644 --- a/test/e2e/docker/unicode.rb +++ b/test/e2e/docker/unicode.rb @@ -62,7 +62,9 @@ {"event":"cmd_output", "timestamp":"*", "output":"諸説存在し。\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"echo 特定の伝説に拠る物語の由来については諸説存在し。特定の伝説に拠る物語の由来については諸説存在し。特定の伝説に拠る物語の由来については諸説存在し。","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/docker/unknown_command.rb b/test/e2e/docker/unknown_command.rb index 92e7a40f..6e5ebf8f 100644 --- a/test/e2e/docker/unknown_command.rb +++ b/test/e2e/docker/unknown_command.rb @@ -57,7 +57,9 @@ {"event":"cmd_output", "timestamp":"*", "output":"bash: echhhho: command not found\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"echhhho Hello World","exit_code":127,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"failed"} LOG diff --git a/test/e2e/self-hosted/docker_compose_host_env_vars.rb b/test/e2e/self-hosted/docker_compose_host_env_vars.rb index 34a6149f..f9fce855 100644 --- a/test/e2e/self-hosted/docker_compose_host_env_vars.rb +++ b/test/e2e/self-hosted/docker_compose_host_env_vars.rb @@ -94,7 +94,9 @@ {"event":"cmd_output", "timestamp":"*", "output":"$PATH:/etc/a\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"echo $D","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/self-hosted/docker_compose_host_files.rb b/test/e2e/self-hosted/docker_compose_host_files.rb index 236d41dd..13711b0b 100644 --- a/test/e2e/self-hosted/docker_compose_host_files.rb +++ b/test/e2e/self-hosted/docker_compose_host_files.rb @@ -81,7 +81,9 @@ {"event":"cmd_output", "timestamp":"*", "output":"Hello from file2.txt"} {"event":"cmd_finished", "timestamp":"*", "directive":"cat /tmp/agent/file2.txt","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/self-hosted/docker_compose_missing_host_files.rb b/test/e2e/self-hosted/docker_compose_missing_host_files.rb index 335c21ae..d82d94b7 100644 --- a/test/e2e/self-hosted/docker_compose_missing_host_files.rb +++ b/test/e2e/self-hosted/docker_compose_missing_host_files.rb @@ -87,7 +87,9 @@ {"event":"cmd_output", "timestamp":"*", "output":"cat: /tmp/agent/notfound.txt: No such file or directory\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"cat /tmp/agent/notfound.txt","exit_code":1,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"failed"} LOG diff --git a/test/e2e/self-hosted/no_ssh_jump_points.rb b/test/e2e/self-hosted/no_ssh_jump_points.rb index 47af62de..d19ae123 100644 --- a/test/e2e/self-hosted/no_ssh_jump_points.rb +++ b/test/e2e/self-hosted/no_ssh_jump_points.rb @@ -42,7 +42,9 @@ {"event":"cmd_output", "timestamp":"*", "output":"0\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"ls -1q ~/.ssh | wc -l","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG \ No newline at end of file diff --git a/test/e2e/self-hosted/shell_host_env_vars.rb b/test/e2e/self-hosted/shell_host_env_vars.rb index df2cc8d3..ced10156 100644 --- a/test/e2e/self-hosted/shell_host_env_vars.rb +++ b/test/e2e/self-hosted/shell_host_env_vars.rb @@ -75,7 +75,9 @@ {"event":"cmd_output", "timestamp":"*", "output":"$PATH:/etc/a\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"echo $D","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/shell/broken_unicode.rb b/test/e2e/shell/broken_unicode.rb index 9e5bb347..e02aa33a 100644 --- a/test/e2e/shell/broken_unicode.rb +++ b/test/e2e/shell/broken_unicode.rb @@ -41,8 +41,9 @@ {"event":"cmd_output", "timestamp":"*", "output":"\ufffd\ufffd\ufffd\ufffd\ufffd"} {"event":"cmd_finished", "timestamp":"*", "directive": "echo | awk '{ printf(\\\"%c%c%c%c%c\\\", 150, 150, 150, 150, 150) }'","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/shell/command_aliases.rb b/test/e2e/shell/command_aliases.rb index f91ba630..1c6d9187 100644 --- a/test/e2e/shell/command_aliases.rb +++ b/test/e2e/shell/command_aliases.rb @@ -37,7 +37,8 @@ {"event":"cmd_output", "timestamp":"*", "output":"Running: echo Hello World\\n"} {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"Display Hello World","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/shell/env_vars.rb b/test/e2e/shell/env_vars.rb index dda413b7..cca49122 100644 --- a/test/e2e/shell/env_vars.rb +++ b/test/e2e/shell/env_vars.rb @@ -64,7 +64,8 @@ {"event":"cmd_output", "timestamp":"*", "output":"$PATH:/etc/a\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"echo $D","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/shell/epilogue_on_fail.rb b/test/e2e/shell/epilogue_on_fail.rb index b963cd34..021b68a3 100644 --- a/test/e2e/shell/epilogue_on_fail.rb +++ b/test/e2e/shell/epilogue_on_fail.rb @@ -47,8 +47,9 @@ {"event":"cmd_started", "timestamp":"*", "directive":"false"} {"event":"cmd_finished", "timestamp":"*", "directive":"false","exit_code":1,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello Epilogue"} {"event":"cmd_output", "timestamp":"*", "output":"Hello Epilogue\\n"} diff --git a/test/e2e/shell/epilogue_on_pass.rb b/test/e2e/shell/epilogue_on_pass.rb index 5122ebbf..a9d0e940 100644 --- a/test/e2e/shell/epilogue_on_pass.rb +++ b/test/e2e/shell/epilogue_on_pass.rb @@ -48,8 +48,9 @@ {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello World","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello Epilogue"} {"event":"cmd_output", "timestamp":"*", "output":"Hello Epilogue\\n"} diff --git a/test/e2e/shell/failed_job.rb b/test/e2e/shell/failed_job.rb index c6f128e6..ae684497 100644 --- a/test/e2e/shell/failed_job.rb +++ b/test/e2e/shell/failed_job.rb @@ -35,7 +35,8 @@ {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} {"event":"cmd_started", "timestamp":"*", "directive":"false"} {"event":"cmd_finished", "timestamp":"*", "directive":"false","exit_code":1,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} {"event":"job_finished", "timestamp":"*", "result":"failed"} LOG diff --git a/test/e2e/shell/file_injection.rb b/test/e2e/shell/file_injection.rb index d578049a..d4d1d9f8 100644 --- a/test/e2e/shell/file_injection.rb +++ b/test/e2e/shell/file_injection.rb @@ -12,7 +12,7 @@ "files": [ { "path": "test.txt", "content": "#{`echo "hello" | base64`.strip}", "mode": "0644" }, { "path": "/a/b/c", "content": "#{`echo "hello" | base64`.strip}", "mode": "0644" }, - { "path": "/tmp/a", "content": "#{`echo "hello" | base64`.strip}", "mode": "+x" } + { "path": "/tmp/a", "content": "#{`echo "hello" | base64`.strip}", "mode": "0600" } ], "commands": [ @@ -42,7 +42,7 @@ {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} {"event":"cmd_output", "timestamp":"*", "output":"Injecting test.txt with file mode 0644\\n"} {"event":"cmd_output", "timestamp":"*", "output":"Injecting /a/b/c with file mode 0644\\n"} - {"event":"cmd_output", "timestamp":"*", "output":"Injecting /tmp/a with file mode +x\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Injecting /tmp/a with file mode 0600\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} {"event":"cmd_started", "timestamp":"*", "directive":"cat test.txt"} @@ -54,10 +54,12 @@ {"event":"cmd_finished", "timestamp":"*", "directive":"cat /a/b/c","exit_code":0,"finished_at":"*","started_at":"*"} {"event":"cmd_started", "timestamp":"*", "directive":"stat -c '%a' /tmp/a"} - {"event":"cmd_output", "timestamp":"*", "output":"755\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"600\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"stat -c '%a' /tmp/a","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/shell/file_injection_broken_file_mode.rb b/test/e2e/shell/file_injection_broken_file_mode.rb index e7e30046..58e2aa13 100644 --- a/test/e2e/shell/file_injection_broken_file_mode.rb +++ b/test/e2e/shell/file_injection_broken_file_mode.rb @@ -39,10 +39,11 @@ {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} {"event":"cmd_output", "timestamp":"*", "output":"Injecting test.txt with file mode obviously broken\\n"} - {"event":"cmd_output", "timestamp":"*", "output":"Failed to set file mode to obviously broken\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"bad file permission 'obviously broken'\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":1,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} {"event":"job_finished", "timestamp":"*", "result":"failed"} LOG diff --git a/test/e2e/shell/hello_world.rb b/test/e2e/shell/hello_world.rb index c6b2b8b8..aec80e80 100644 --- a/test/e2e/shell/hello_world.rb +++ b/test/e2e/shell/hello_world.rb @@ -36,7 +36,8 @@ {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello World"} {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello World","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/shell/job_stopping_on_epilogue.rb b/test/e2e/shell/job_stopping_on_epilogue.rb index 4fe622fa..33f82eb2 100644 --- a/test/e2e/shell/job_stopping_on_epilogue.rb +++ b/test/e2e/shell/job_stopping_on_epilogue.rb @@ -44,8 +44,9 @@ {"event":"cmd_started", "timestamp":"*", "directive":"echo 'here'"} {"event":"cmd_output", "timestamp":"*", "output":"here\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"echo 'here'","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} {"event":"cmd_started", "timestamp":"*", "directive":"sleep infinity"} {"event":"cmd_finished", "timestamp":"*", "directive":"sleep infinity","exit_code":1,"finished_at":"*","started_at":"*"} {"event":"job_finished", "timestamp":"*", "result":"stopped"} diff --git a/test/e2e/shell/killing_root_bash.rb b/test/e2e/shell/killing_root_bash.rb index 71be096d..e83238c5 100644 --- a/test/e2e/shell/killing_root_bash.rb +++ b/test/e2e/shell/killing_root_bash.rb @@ -47,7 +47,8 @@ {"event":"cmd_finished", "timestamp":"*", "directive":"sleep infinity &","exit_code":0,"finished_at":"*","started_at":"*"} {"event":"cmd_started", "timestamp":"*", "directive":"exit 1"} {"event":"cmd_finished", "timestamp":"*", "directive":"exit 1","exit_code":1,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed","exit_code":1,"started_at":"*","finished_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":1,"started_at":"*","finished_at":"*"} {"event":"job_finished", "timestamp":"*", "result":"failed"} LOG diff --git a/test/e2e/shell/set_e.rb b/test/e2e/shell/set_e.rb index e22d6fe4..9a086870 100644 --- a/test/e2e/shell/set_e.rb +++ b/test/e2e/shell/set_e.rb @@ -51,7 +51,8 @@ {"event":"cmd_finished", "timestamp":"*", "directive":"set -e","exit_code":0,"finished_at":"*","started_at":"*"} {"event":"cmd_started", "timestamp":"*", "directive":"false"} {"event":"cmd_finished", "timestamp":"*", "directive":"false","exit_code":1,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed","exit_code":1,"started_at":"*","finished_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":1,"started_at":"*","finished_at":"*"} {"event":"job_finished", "timestamp":"*", "result":"failed"} LOG diff --git a/test/e2e/shell/set_pipefail.rb b/test/e2e/shell/set_pipefail.rb index 932dc0ce..a6cc9e99 100644 --- a/test/e2e/shell/set_pipefail.rb +++ b/test/e2e/shell/set_pipefail.rb @@ -52,7 +52,8 @@ {"event":"cmd_started", "timestamp":"*", "directive":"cat non_existant | sort"} {"event":"cmd_output", "timestamp":"*", "output":"cat: non_existant: No such file or directory\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"cat non_existant | sort","exit_code":1,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed","exit_code":1,"started_at":"*","finished_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":1,"started_at":"*","finished_at":"*"} {"event":"job_finished", "timestamp":"*", "result":"failed"} LOG diff --git a/test/e2e/shell/stty_restoration.rb b/test/e2e/shell/stty_restoration.rb index 002f5554..df921f57 100644 --- a/test/e2e/shell/stty_restoration.rb +++ b/test/e2e/shell/stty_restoration.rb @@ -39,7 +39,8 @@ {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello World"} {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello World","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/shell/unicode.rb b/test/e2e/shell/unicode.rb index 9a54f627..2e0d0e51 100644 --- a/test/e2e/shell/unicode.rb +++ b/test/e2e/shell/unicode.rb @@ -64,8 +64,9 @@ {"event":"cmd_output", "timestamp":"*", "output":"━━━━━━━━━━━━━━━━━━━━━━\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"echo ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/shell/unknown_command.rb b/test/e2e/shell/unknown_command.rb index fd419e99..44522b59 100644 --- a/test/e2e/shell/unknown_command.rb +++ b/test/e2e/shell/unknown_command.rb @@ -36,7 +36,8 @@ {"event":"cmd_started", "timestamp":"*", "directive":"echhhho Hello World"} {"event":"cmd_output", "timestamp":"*", "output":"bash: echhhho: command not found\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"echhhho Hello World","exit_code":127,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=failed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} {"event":"job_finished", "timestamp":"*", "result":"failed"} LOG diff --git a/test/e2e_support/docker-compose-listen.yml b/test/e2e_support/docker-compose-listen.yml index e4b2574e..5f39f3a1 100644 --- a/test/e2e_support/docker-compose-listen.yml +++ b/test/e2e_support/docker-compose-listen.yml @@ -6,7 +6,7 @@ services: context: ../.. dockerfile: Dockerfile.test - command: 'bash -c "service ssh restart && ./agent start --config-file /tmp/agent/config.yaml"' + command: 'bash -c "service ssh restart && SEMAPHORE_AGENT_LOG_LEVEL=DEBUG ./agent start --config-file /tmp/agent/config.yaml"' ports: - "30000:8000" From e46323ea16ecfca991443c1174374b6928b8dc0e Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Thu, 10 Mar 2022 08:46:06 -0300 Subject: [PATCH 015/130] Move shell executor E2Es to Go tests (#145) --- .github/workflows/test.yml | 16 + .semaphore/semaphore.yml | 81 +- Makefile | 9 +- main.go | 5 +- pkg/api/job_request.go | 13 +- pkg/api/job_request_test.go | 59 ++ pkg/eventlogger/default.go | 3 +- pkg/eventlogger/filebackend.go | 12 + pkg/eventlogger/filebackend_test.go | 50 + pkg/eventlogger/httpbackend.go | 2 +- pkg/eventlogger/httpbackend_test.go | 56 + pkg/eventlogger/inmemorybackend.go | 33 +- pkg/eventlogger/test_helpers.go | 57 ++ pkg/executors/docker_compose_executor_test.go | 18 +- pkg/executors/shell_executor.go | 10 +- pkg/executors/shell_executor_test.go | 441 ++++++-- pkg/executors/ssh_jump_point.go | 2 +- pkg/jobs/job.go | 76 +- pkg/jobs/job_test.go | 953 ++++++++++++++++++ pkg/listener/job_processor.go | 27 +- pkg/listener/listener.go | 11 +- pkg/listener/listener_test.go | 793 +++++++++++++++ pkg/listener/selfhostedapi/logs.go | 31 - pkg/osinfo/{osinfo_test.go => name_test.go} | 0 pkg/shell/env.go | 10 +- pkg/shell/env_test.go | 172 ++++ pkg/shell/process.go | 25 +- pkg/shell/pty_test.go | 28 + pkg/shell/shell_test.go | 84 +- test/e2e/{shell => hosted}/ssh_jump_points.rb | 0 .../self-hosted/broken_finished_callback.rb | 28 - test/e2e/self-hosted/broken_get_job.rb | 30 - .../self-hosted/broken_teardown_callback.rb | 28 - test/e2e/self-hosted/no_ssh_jump_points.rb | 50 - test/e2e/self-hosted/shell_host_env_vars.rb | 83 -- test/e2e/self-hosted/shutdown.rb | 47 - test/e2e/self-hosted/shutdown_idle.rb | 34 - .../e2e/self-hosted/shutdown_while_waiting.rb | 8 - test/e2e/shell/broken_unicode.rb | 49 - test/e2e/shell/command_aliases.rb | 44 - test/e2e/shell/env_vars.rb | 71 -- test/e2e/shell/epilogue_on_fail.rb | 63 -- test/e2e/shell/epilogue_on_pass.rb | 64 -- test/e2e/shell/failed_job.rb | 42 - test/e2e/shell/file_injection.rb | 65 -- .../shell/file_injection_broken_file_mode.rb | 49 - test/e2e/shell/hello_world.rb | 43 - test/e2e/shell/job_stopping.rb | 46 - test/e2e/shell/job_stopping_on_epilogue.rb | 53 - test/e2e/shell/killing_root_bash.rb | 54 - test/e2e/shell/set_e.rb | 58 -- test/e2e/shell/set_pipefail.rb | 59 -- test/e2e/shell/stty_restoration.rb | 46 - test/e2e/shell/unicode.rb | 72 -- test/e2e/shell/unknown_command.rb | 43 - test/support/commands.go | 153 +++ test/support/hub.go | 274 +++++ test/support/loghub.go | 64 ++ test/support/test_logger.go | 5 +- 59 files changed, 3278 insertions(+), 1524 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 pkg/api/job_request_test.go create mode 100644 pkg/eventlogger/filebackend_test.go create mode 100644 pkg/eventlogger/httpbackend_test.go create mode 100644 pkg/eventlogger/test_helpers.go create mode 100644 pkg/jobs/job_test.go create mode 100644 pkg/listener/listener_test.go delete mode 100644 pkg/listener/selfhostedapi/logs.go rename pkg/osinfo/{osinfo_test.go => name_test.go} (100%) create mode 100644 pkg/shell/env_test.go create mode 100644 pkg/shell/pty_test.go rename test/e2e/{shell => hosted}/ssh_jump_points.rb (100%) delete mode 100644 test/e2e/self-hosted/broken_finished_callback.rb delete mode 100644 test/e2e/self-hosted/broken_get_job.rb delete mode 100644 test/e2e/self-hosted/broken_teardown_callback.rb delete mode 100644 test/e2e/self-hosted/no_ssh_jump_points.rb delete mode 100644 test/e2e/self-hosted/shell_host_env_vars.rb delete mode 100644 test/e2e/self-hosted/shutdown.rb delete mode 100644 test/e2e/self-hosted/shutdown_idle.rb delete mode 100644 test/e2e/self-hosted/shutdown_while_waiting.rb delete mode 100644 test/e2e/shell/broken_unicode.rb delete mode 100644 test/e2e/shell/command_aliases.rb delete mode 100644 test/e2e/shell/env_vars.rb delete mode 100644 test/e2e/shell/epilogue_on_fail.rb delete mode 100644 test/e2e/shell/epilogue_on_pass.rb delete mode 100644 test/e2e/shell/failed_job.rb delete mode 100644 test/e2e/shell/file_injection.rb delete mode 100644 test/e2e/shell/file_injection_broken_file_mode.rb delete mode 100644 test/e2e/shell/hello_world.rb delete mode 100644 test/e2e/shell/job_stopping.rb delete mode 100644 test/e2e/shell/job_stopping_on_epilogue.rb delete mode 100644 test/e2e/shell/killing_root_bash.rb delete mode 100644 test/e2e/shell/set_e.rb delete mode 100644 test/e2e/shell/set_pipefail.rb delete mode 100644 test/e2e/shell/stty_restoration.rb delete mode 100644 test/e2e/shell/unicode.rb delete mode 100644 test/e2e/shell/unknown_command.rb create mode 100644 test/support/commands.go create mode 100644 test/support/hub.go create mode 100644 test/support/loghub.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..46a68647 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,16 @@ +on: [push, pull_request] +name: Test +jobs: + unit-testing: + runs-on: windows-latest + steps: + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: 1.16.x + - name: Check out repository code + uses: actions/checkout@v2 + - name: Install gotestsum + run: go install gotest.tools/gotestsum@latest + - name: Test + run: gotestsum --format short-verbose --packages="./..." -- -p 1 diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index a26a412d..f329a9e7 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -67,56 +67,13 @@ blocks: jobs: - name: Unit Tests commands: + - go install gotest.tools/gotestsum@latest - make test - - name: "Shell Executor E2E tests" - dependencies: [] - task: - env_vars: - - name: GO111MODULE - value: "on" - - prologue: - commands: - - sem-version go 1.16 - - checkout - - go version - - go get - - go build - - mkdir /tmp/agent - epilogue: - commands: - - if [ "$TEST_MODE" = "api" ]; then docker exec -ti agent cat /tmp/agent_log; else docker logs e2e_support_agent_1; fi - - if [ "$TEST_MODE" = "api" ]; then echo "No hub"; else docker logs e2e_support_hub_1; fi - - jobs: - - name: Shell + always: commands: - - "make e2e TEST=shell/$TEST" - matrix: - - env_var: TEST_MODE - values: - - api - - listen - - env_var: TEST - values: - - command_aliases - - env_vars - - failed_job - - job_stopping - - job_stopping_on_epilogue - - file_injection - - file_injection_broken_file_mode - - stty_restoration - - epilogue_on_pass - - epilogue_on_fail - - unicode - - unknown_command - - broken_unicode - - killing_root_bash - - set_e - - set_pipefail + - test-results publish junit-report.xml - name: "Docker Executor E2E" dependencies: [] @@ -188,6 +145,31 @@ blocks: - host_setup_commands - multiple_containers + - name: "Hosted E2E tests" + dependencies: [] + task: + env_vars: + - name: GO111MODULE + value: "on" + + prologue: + commands: + - sem-version go 1.16 + - checkout + - go version + - go get + - go build + - mkdir /tmp/agent + + epilogue: + commands: + - docker exec -ti agent cat /tmp/agent_log + + jobs: + - name: Test SSH jump point + commands: + - "TEST_MODE=api make e2e TEST=hosted/ssh_jump_points" + - name: "Self hosted E2E" dependencies: [] task: @@ -218,17 +200,10 @@ blocks: matrix: - env_var: TEST values: - - broken_finished_callback - - broken_teardown_callback - - broken_get_job - docker_compose_host_env_vars - docker_compose_host_files - docker_compose_missing_host_files - docker_compose_fail_on_missing_host_files - - shell_host_env_vars - - shutdown - - shutdown_idle - - shutdown_while_waiting promotions: - name: Release diff --git a/Makefile b/Makefile index 08500785..18ac7bdf 100644 --- a/Makefile +++ b/Makefile @@ -21,13 +21,6 @@ check.deps: check.prepare registry.semaphoreci.com/ruby:2.7 \ bash -c 'cd /app && $(SECURITY_TOOLBOX_TMP_DIR)/dependencies --language go -d' -go.install: - cd /tmp - sudo curl -O https://dl.google.com/go/go1.11.linux-amd64.tar.gz - sudo tar -xf go1.11.linux-amd64.tar.gz - sudo mv go /usr/local - cd - - lint: revive -formatter friendly -config lint.toml ./... @@ -40,7 +33,7 @@ serve: .PHONY: serve test: - go test -p 1 -short -v ./... + gotestsum --format short-verbose --junitfile junit-report.xml --packages="./..." -- -p 1 .PHONY: test build: diff --git a/main.go b/main.go index 409754ae..0c03f42a 100644 --- a/main.go +++ b/main.go @@ -160,6 +160,8 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { Endpoint: viper.GetString(config.Endpoint), Token: viper.GetString(config.Token), RegisterRetryLimit: 30, + GetJobRetryLimit: 10, + CallbackRetryLimit: 60, Scheme: scheme, ShutdownHookPath: viper.GetString(config.ShutdownHookPath), DisconnectAfterJob: viper.GetBool(config.DisconnectAfterJob), @@ -168,10 +170,11 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { FileInjections: fileInjections, FailOnMissingFiles: viper.GetBool(config.FailOnMissingFiles), AgentVersion: VERSION, + ExitOnShutdown: true, } go func() { - _, err := listener.Start(httpClient, config, logfile) + _, err := listener.Start(httpClient, config) if err != nil { log.Panicf("Could not start agent: %v", err) } diff --git a/pkg/api/job_request.go b/pkg/api/job_request.go index 8b606767..de3d6907 100644 --- a/pkg/api/job_request.go +++ b/pkg/api/job_request.go @@ -50,15 +50,18 @@ type File struct { } func (f *File) NormalizePath(homeDir string) string { - if filepath.IsAbs(f.Path) { - return filepath.FromSlash(f.Path) + // convert path to platform-specific one first + path := filepath.FromSlash(f.Path) + + if filepath.IsAbs(path) { + return path } - if strings.HasPrefix(f.Path, "~") { - return filepath.FromSlash(strings.ReplaceAll(f.Path, "~", homeDir)) + if strings.HasPrefix(path, "~") { + return strings.ReplaceAll(path, "~", homeDir) } - return filepath.FromSlash(filepath.Join(homeDir, f.Path)) + return filepath.Join(homeDir, path) } func (f *File) ParseMode() (fs.FileMode, error) { diff --git a/pkg/api/job_request_test.go b/pkg/api/job_request_test.go new file mode 100644 index 00000000..06bf4cd4 --- /dev/null +++ b/pkg/api/job_request_test.go @@ -0,0 +1,59 @@ +package api + +import ( + "path/filepath" + "runtime" + "testing" + + assert "github.com/stretchr/testify/assert" +) + +func Test__JobRequest(t *testing.T) { + homeDir := filepath.Join("/first", "second", "home") + + t.Run("file path with ~ is normalized", func(t *testing.T) { + file := File{Path: "~/dir/somefile", Content: "", Mode: "0644"} + if runtime.GOOS == "windows" { + assert.Equal(t, file.NormalizePath(homeDir), "\\first\\second\\home\\dir\\somefile") + } else { + assert.Equal(t, file.NormalizePath(homeDir), "/first/second/home/dir/somefile") + } + }) + + t.Run("absolute file path remains the same", func(t *testing.T) { + if runtime.GOOS == "windows" { + file := File{Path: "C:\\first\\second\\home\\somefile", Content: "", Mode: "0644"} + assert.Equal(t, file.NormalizePath(homeDir), "C:\\first\\second\\home\\somefile") + } else { + file := File{Path: "/first/second/home/somefile", Content: "", Mode: "0644"} + assert.Equal(t, file.NormalizePath(homeDir), "/first/second/home/somefile") + } + }) + + t.Run("relative file path is put on home directory", func(t *testing.T) { + file := File{Path: "somefile", Content: "", Mode: "0644"} + if runtime.GOOS == "windows" { + assert.Equal(t, file.NormalizePath(homeDir), "\\first\\second\\home\\somefile") + } else { + assert.Equal(t, file.NormalizePath(homeDir), "/first/second/home/somefile") + } + }) + + t.Run("accepted file modes", func(t *testing.T) { + fileModes := []string{"0600", "0644", "0777"} + for _, fileMode := range fileModes { + file := File{Path: "somefile", Content: "", Mode: fileMode} + _, err := file.ParseMode() + assert.Nil(t, err) + } + }) + + t.Run("bad file modes", func(t *testing.T) { + fileModes := []string{"+x", "+r", "+w", "+rw"} + for _, fileMode := range fileModes { + file := File{Path: "somefile", Content: "", Mode: fileMode} + _, err := file.ParseMode() + assert.NotNil(t, err) + } + }) +} diff --git a/pkg/eventlogger/default.go b/pkg/eventlogger/default.go index 5a5888ac..15120030 100644 --- a/pkg/eventlogger/default.go +++ b/pkg/eventlogger/default.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "time" "github.com/semaphoreci/agent/pkg/api" ) @@ -24,7 +25,7 @@ func CreateLogger(request *api.JobRequest) (*Logger, error) { } func Default() (*Logger, error) { - path := filepath.Join(os.TempDir(), "job_log.json") + path := filepath.Join(os.TempDir(), fmt.Sprintf("job_log_%d.json", time.Now().UnixNano())) backend, err := NewFileBackend(path) if err != nil { return nil, err diff --git a/pkg/eventlogger/filebackend.go b/pkg/eventlogger/filebackend.go index 10e209b1..3f60717e 100644 --- a/pkg/eventlogger/filebackend.go +++ b/pkg/eventlogger/filebackend.go @@ -52,6 +52,18 @@ func (l *FileBackend) Write(event interface{}) error { } func (l *FileBackend) Close() error { + err := l.file.Close() + if err != nil { + log.Errorf("Error closing file %s: %v\n", l.file.Name(), err) + return err + } + + log.Debugf("Removing %s\n", l.file.Name()) + if err := os.Remove(l.file.Name()); err != nil { + log.Errorf("Error removing logger file %s: %v\n", l.file.Name(), err) + return err + } + return nil } diff --git a/pkg/eventlogger/filebackend_test.go b/pkg/eventlogger/filebackend_test.go new file mode 100644 index 00000000..2e04fb76 --- /dev/null +++ b/pkg/eventlogger/filebackend_test.go @@ -0,0 +1,50 @@ +package eventlogger + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + "time" + + testsupport "github.com/semaphoreci/agent/test/support" + "github.com/stretchr/testify/assert" +) + +func Test__LogsArePushedToFile(t *testing.T) { + tmpFileName := filepath.Join(os.TempDir(), fmt.Sprintf("logs_%d.json", time.Now().UnixNano())) + fileBackend, err := NewFileBackend(tmpFileName) + assert.Nil(t, err) + assert.Nil(t, fileBackend.Open()) + + timestamp := int(time.Now().Unix()) + assert.Nil(t, fileBackend.Write(&JobStartedEvent{Timestamp: timestamp, Event: "job_started"})) + assert.Nil(t, fileBackend.Write(&CommandStartedEvent{Timestamp: timestamp, Event: "cmd_started", Directive: "echo hello"})) + assert.Nil(t, fileBackend.Write(&CommandOutputEvent{Timestamp: timestamp, Event: "cmd_output", Output: "hello\n"})) + assert.Nil(t, fileBackend.Write(&CommandFinishedEvent{ + Timestamp: timestamp, + Event: "cmd_finished", + Directive: "echo hello", + ExitCode: 0, + StartedAt: timestamp, + FinishedAt: timestamp, + })) + assert.Nil(t, fileBackend.Write(&JobFinishedEvent{Timestamp: timestamp, Event: "job_finished", Result: "passed"})) + + bytes, err := ioutil.ReadFile(tmpFileName) + assert.Nil(t, err) + logs := strings.Split(string(bytes), "\n") + + assert.Equal(t, []string{ + fmt.Sprintf(`{"event":"job_started","timestamp":%d}`, timestamp), + fmt.Sprintf(`{"event":"cmd_started","timestamp":%d,"directive":"echo hello"}`, timestamp), + fmt.Sprintf(`{"event":"cmd_output","timestamp":%d,"output":"hello\n"}`, timestamp), + fmt.Sprintf(`{"event":"cmd_finished","timestamp":%d,"directive":"echo hello","exit_code":0,"started_at":%d,"finished_at":%d}`, timestamp, timestamp, timestamp), + fmt.Sprintf(`{"event":"job_finished","timestamp":%d,"result":"passed"}`, timestamp), + }, testsupport.FilterEmpty(logs)) + + err = fileBackend.Close() + assert.Nil(t, err) +} diff --git a/pkg/eventlogger/httpbackend.go b/pkg/eventlogger/httpbackend.go index 16d8bcbb..9f36c3a9 100644 --- a/pkg/eventlogger/httpbackend.go +++ b/pkg/eventlogger/httpbackend.go @@ -24,7 +24,7 @@ type HTTPBackend struct { } func NewHTTPBackend(url, token string) (*HTTPBackend, error) { - path := filepath.Join(os.TempDir(), "job_log.json") + path := filepath.Join(os.TempDir(), fmt.Sprintf("job_log_%d.json", time.Now().UnixNano())) fileBackend, err := NewFileBackend(path) if err != nil { return nil, err diff --git a/pkg/eventlogger/httpbackend_test.go b/pkg/eventlogger/httpbackend_test.go new file mode 100644 index 00000000..e54aa621 --- /dev/null +++ b/pkg/eventlogger/httpbackend_test.go @@ -0,0 +1,56 @@ +package eventlogger + +import ( + "testing" + "time" + + testsupport "github.com/semaphoreci/agent/test/support" + "github.com/stretchr/testify/assert" +) + +func Test__LogsArePushedToHTTPEndpoint(t *testing.T) { + mockServer := testsupport.NewLoghubMockServer() + mockServer.Init() + + httpBackend, err := NewHTTPBackend(mockServer.URL(), "token") + assert.Nil(t, err) + assert.Nil(t, httpBackend.Open()) + + timestamp := int(time.Now().Unix()) + assert.Nil(t, httpBackend.Write(&JobStartedEvent{Timestamp: timestamp, Event: "job_started"})) + assert.Nil(t, httpBackend.Write(&CommandStartedEvent{Timestamp: timestamp, Event: "cmd_started", Directive: "echo hello"})) + assert.Nil(t, httpBackend.Write(&CommandOutputEvent{Timestamp: timestamp, Event: "cmd_output", Output: "hello\n"})) + assert.Nil(t, httpBackend.Write(&CommandFinishedEvent{ + Timestamp: timestamp, + Event: "cmd_finished", + Directive: "echo hello", + ExitCode: 0, + StartedAt: timestamp, + FinishedAt: timestamp, + })) + assert.Nil(t, httpBackend.Write(&JobFinishedEvent{Timestamp: timestamp, Event: "job_finished", Result: "passed"})) + + // Wait until everything is pushed + time.Sleep(2 * time.Second) + + err = httpBackend.Close() + assert.Nil(t, err) + + eventObjects, err := TransformToObjects(mockServer.GetLogs()) + assert.Nil(t, err) + + simplifiedEvents, err := SimplifyLogEvents(eventObjects, true) + assert.Nil(t, err) + + assert.Equal(t, []string{ + "job_started", + + "directive: echo hello", + "hello\n", + "Exit Code: 0", + + "job_finished: passed", + }, simplifiedEvents) + + mockServer.Close() +} diff --git a/pkg/eventlogger/inmemorybackend.go b/pkg/eventlogger/inmemorybackend.go index 086f0f42..010a27c4 100644 --- a/pkg/eventlogger/inmemorybackend.go +++ b/pkg/eventlogger/inmemorybackend.go @@ -1,7 +1,6 @@ package eventlogger import ( - "fmt" "strings" ) @@ -27,31 +26,15 @@ func (l *InMemoryBackend) Close() error { return nil } -func (l *InMemoryBackend) SimplifiedEvents() []string { - events := []string{} - - for _, event := range l.Events { - switch e := event.(type) { - case *JobStartedEvent: - events = append(events, "job_started") - case *JobFinishedEvent: - events = append(events, "job_finished") - case *CommandStartedEvent: - events = append(events, "directive: "+e.Directive) - case *CommandOutputEvent: - events = append(events, e.Output) - case *CommandFinishedEvent: - events = append(events, fmt.Sprintf("Exit Code: %d", e.ExitCode)) - default: - panic("Unknown shell event") - } - } - - return events +func (l *InMemoryBackend) SimplifiedEvents(includeOutput bool) ([]string, error) { + return SimplifyLogEvents(l.Events, includeOutput) } -func (l *InMemoryBackend) SimplifiedEventsWithoutDockerPull() []string { - logs := l.SimplifiedEvents() +func (l *InMemoryBackend) SimplifiedEventsWithoutDockerPull() ([]string, error) { + logs, err := l.SimplifiedEvents(true) + if err != nil { + return []string{}, err + } start := 0 @@ -71,5 +54,5 @@ func (l *InMemoryBackend) SimplifiedEventsWithoutDockerPull() []string { } } - return append([]string{logs[start]}, logs[end:]...) + return append([]string{logs[start]}, logs[end:]...), nil } diff --git a/pkg/eventlogger/test_helpers.go b/pkg/eventlogger/test_helpers.go new file mode 100644 index 00000000..295382ff --- /dev/null +++ b/pkg/eventlogger/test_helpers.go @@ -0,0 +1,57 @@ +package eventlogger + +import ( + "encoding/json" + "fmt" +) + +func TransformToObjects(events []string) ([]interface{}, error) { + objects := []interface{}{} + for _, event := range events { + var object map[string]interface{} + err := json.Unmarshal([]byte(event), &object) + if err != nil { + return []interface{}{}, err + } + + switch eventType := object["event"].(string); { + case eventType == "job_started": + objects = append(objects, &JobStartedEvent{Event: eventType}) + case eventType == "job_finished": + objects = append(objects, &JobFinishedEvent{Event: eventType, Result: object["result"].(string)}) + case eventType == "cmd_started": + objects = append(objects, &CommandStartedEvent{Event: eventType, Directive: object["directive"].(string)}) + case eventType == "cmd_output": + objects = append(objects, &CommandOutputEvent{Event: eventType, Output: object["output"].(string)}) + case eventType == "cmd_finished": + objects = append(objects, &CommandFinishedEvent{Event: eventType, ExitCode: int(object["exit_code"].(float64))}) + } + } + + return objects, nil +} + +func SimplifyLogEvents(events []interface{}, includeOutput bool) ([]string, error) { + simplified := []string{} + + for _, event := range events { + switch e := event.(type) { + case *JobStartedEvent: + simplified = append(simplified, "job_started") + case *JobFinishedEvent: + simplified = append(simplified, "job_finished: "+e.Result) + case *CommandStartedEvent: + simplified = append(simplified, "directive: "+e.Directive) + case *CommandOutputEvent: + if includeOutput { + simplified = append(simplified, e.Output) + } + case *CommandFinishedEvent: + simplified = append(simplified, fmt.Sprintf("Exit Code: %d", e.ExitCode)) + default: + return []string{}, fmt.Errorf("unknown shell event") + } + } + + return simplified, nil +} diff --git a/pkg/executors/docker_compose_executor_test.go b/pkg/executors/docker_compose_executor_test.go index 100354d8..6d309a92 100644 --- a/pkg/executors/docker_compose_executor_test.go +++ b/pkg/executors/docker_compose_executor_test.go @@ -17,20 +17,20 @@ func startComposeExecutor() (*DockerComposeExecutor, *eventlogger.Logger, *event request := &api.JobRequest{ Compose: api.Compose{ Containers: []api.Container{ - api.Container{ + { Name: "main", Image: "ruby:2.6", }, - api.Container{ + { Name: "db", Image: "postgres:9.6", Command: "postgres start", EnvVars: []api.EnvVar{ - api.EnvVar{ + { Name: "FOO", Value: base64.StdEncoding.EncodeToString([]byte("BAR")), }, - api.EnvVar{ + { Name: "FAZ", Value: base64.StdEncoding.EncodeToString([]byte("ZEZ")), }, @@ -105,7 +105,10 @@ func Test__DockerComposeExecutor(t *testing.T) { e.Stop() e.Cleanup() - assert.Equal(t, testLoggerBackend.SimplifiedEventsWithoutDockerPull(), []string{ + simplifiedLogEvents, err := testLoggerBackend.SimplifiedEventsWithoutDockerPull() + assert.Nil(t, err) + + assert.Equal(t, simplifiedLogEvents, []string{ "directive: Pulling docker images...", "Exit Code: 0", @@ -162,7 +165,10 @@ func Test__DockerComposeExecutor__StopingRunningJob(t *testing.T) { time.Sleep(1 * time.Second) - assert.Equal(t, testLoggerBackend.SimplifiedEventsWithoutDockerPull(), []string{ + simplifiedLogEvents, err := testLoggerBackend.SimplifiedEventsWithoutDockerPull() + assert.Nil(t, err) + + assert.Equal(t, simplifiedLogEvents, []string{ "directive: Pulling docker images...", "Exit Code: 0", diff --git a/pkg/executors/shell_executor.go b/pkg/executors/shell_executor.go index 3ba9e99e..7ad80ecc 100644 --- a/pkg/executors/shell_executor.go +++ b/pkg/executors/shell_executor.go @@ -3,7 +3,6 @@ package executors import ( "fmt" "os" - "path" "path/filepath" "runtime" "strings" @@ -170,8 +169,9 @@ func (e *ShellExecutor) InjectFiles(files []api.File) int { } for _, f := range files { - output := fmt.Sprintf("Injecting %s with file mode %s\n", f.Path, f.Mode) + destPath := f.NormalizePath(homeDir) + output := fmt.Sprintf("Injecting %s with file mode %s\n", destPath, f.Mode) e.Logger.LogCommandOutput(output) content, err := f.Decode() @@ -181,10 +181,10 @@ func (e *ShellExecutor) InjectFiles(files []api.File) int { return exitCode } - destPath := f.NormalizePath(homeDir) - err = os.MkdirAll(path.Dir(destPath), 0644) + parentDir := filepath.Dir(destPath) + err = os.MkdirAll(parentDir, 0750) if err != nil { - e.Logger.LogCommandOutput(fmt.Sprintf("Failed to create directories for '%s': %v\n", destPath, err)) + e.Logger.LogCommandOutput(fmt.Sprintf("Failed to create directory '%s': %v\n", parentDir, err)) exitCode = 1 break } diff --git a/pkg/executors/shell_executor_test.go b/pkg/executors/shell_executor_test.go index f7c9da61..4540032b 100644 --- a/pkg/executors/shell_executor_test.go +++ b/pkg/executors/shell_executor_test.go @@ -2,6 +2,11 @@ package executors import ( "encoding/base64" + "fmt" + "io/fs" + "os" + "path/filepath" + "runtime" "testing" "time" @@ -12,152 +17,337 @@ import ( assert "github.com/stretchr/testify/assert" ) -func Test__ShellExecutor(t *testing.T) { - testsupport.SetupTestLogs() +var UnicodeOutput1 = `特定の伝説に拠る物語の由来については諸説存在し。特定の伝説に拠る物語の由来については諸説存在し。特定の伝説に拠る物語の由来については諸説存在し。` +var UnicodeOutput2 = `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━` - testLogger, testLoggerBackend := eventlogger.DefaultTestLogger() +func Test__ShellExecutor__SSHJumpPointIsCreatedForHosted(t *testing.T) { + sshJumpPointPath := filepath.Join(os.TempDir(), "ssh_jump_point") + os.Remove(sshJumpPointPath) + _, _ = setupShellExecutor(t, false) + assert.FileExists(t, sshJumpPointPath) + os.Remove(sshJumpPointPath) +} - request := &api.JobRequest{ - SSHPublicKeys: []api.PublicKey{ - api.PublicKey(base64.StdEncoding.EncodeToString([]byte("ssh-rsa aaaaa"))), +func Test__ShellExecutor__SSHJumpPointIsNotCreatedForSelfHosted(t *testing.T) { + sshJumpPointPath := filepath.Join(os.TempDir(), "ssh_jump_point") + os.Remove(sshJumpPointPath) + _, _ = setupShellExecutor(t, true) + assert.NoFileExists(t, sshJumpPointPath) + os.Remove(sshJumpPointPath) +} + +func Test__ShellExecutor__EnvVars(t *testing.T) { + e, testLoggerBackend := setupShellExecutor(t, true) + assert.Zero(t, e.ExportEnvVars( + []api.EnvVar{ + {Name: "A", Value: base64.StdEncoding.EncodeToString([]byte("AAA"))}, + {Name: "B", Value: base64.StdEncoding.EncodeToString([]byte("BBB"))}, + {Name: "VAR_WITH_QUOTES", Value: base64.StdEncoding.EncodeToString([]byte("quotes ' quotes"))}, + {Name: "VAR_WITH_ENV_VAR", Value: base64.StdEncoding.EncodeToString([]byte(testsupport.NestedEnvVarValue("PATH", ":/etc/a")))}, }, - } + []config.HostEnvVar{ + {Name: "C", Value: "CCC"}, + {Name: "D", Value: "DDD"}, + }, + )) + + assert.Zero(t, e.RunCommand(testsupport.EchoEnvVar("A"), false, "")) + assert.Zero(t, e.RunCommand(testsupport.EchoEnvVar("B"), false, "")) + assert.Zero(t, e.RunCommand(testsupport.EchoEnvVar("VAR_WITH_QUOTES"), false, "")) + assert.Zero(t, e.RunCommand(testsupport.EchoEnvVar("VAR_WITH_ENV_VAR"), false, "")) + assert.Zero(t, e.RunCommand(testsupport.EchoEnvVar("C"), false, "")) + assert.Zero(t, e.RunCommand(testsupport.EchoEnvVar("D"), false, "")) + assert.Zero(t, e.RunCommand(testsupport.EchoEnvVar("E"), false, "")) + assert.Zero(t, e.Stop()) + assert.Zero(t, e.Cleanup()) + + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + assert.Equal(t, simplifiedEvents, []string{ + "directive: Exporting environment variables", + "Exporting A\n", + "Exporting B\n", + "Exporting C\n", + "Exporting D\n", + "Exporting VAR_WITH_ENV_VAR\n", + "Exporting VAR_WITH_QUOTES\n", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("A")), + "AAA", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("B")), + "BBB", + "Exit Code: 0", - e := NewShellExecutor(request, testLogger, true) + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("VAR_WITH_QUOTES")), + "quotes ' quotes", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("VAR_WITH_ENV_VAR")), + testsupport.NestedEnvVarValue("PATH", ":/etc/a"), + "Exit Code: 0", - e.Prepare() - e.Start() + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("C")), + "CCC", + "Exit Code: 0", - e.RunCommand("echo 'here'", false, "") + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("D")), + "DDD", + "Exit Code: 0", - multilineCmd := ` - if [ -d /etc ]; then - echo 'etc exists, multiline huzzahh!' - fi - ` + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("E")), + "Exit Code: 0", + }) +} - e.RunCommand(multilineCmd, false, "") +func Test__ShellExecutor__InjectFiles(t *testing.T) { + e, testLoggerBackend := setupShellExecutor(t, true) + homeDir, _ := os.UserHomeDir() - envVars := []api.EnvVar{ - {Name: "A", Value: "Zm9vCg=="}, + absoluteFile := api.File{ + Path: filepath.Join(os.TempDir(), "absolute-file.txt"), + Content: base64.StdEncoding.EncodeToString([]byte("absolute\n")), + Mode: "0400", } - e.ExportEnvVars(envVars, []config.HostEnvVar{}) - e.RunCommand("echo $A", false, "") + absoluteFileInMissingDir := api.File{ + Path: filepath.Join(os.TempDir(), "somedir", "absolute-file-missing-dir.txt"), + Content: base64.StdEncoding.EncodeToString([]byte("absolute-in-missing-dir\n")), + Mode: "0440", + } - files := []api.File{ - { - Path: "/tmp/random-file.txt", - Content: "YWFhYmJiCgo=", - Mode: "0600", - }, + relativeFile := api.File{ + Path: filepath.Join("somedir", "relative-file.txt"), + Content: base64.StdEncoding.EncodeToString([]byte("relative\n")), + Mode: "0600", } - e.InjectFiles(files) - e.RunCommand("cat /tmp/random-file.txt", false, "") + relativeFileInMissingDir := api.File{ + Path: filepath.Join("somedir", "relative-file-in-missing-dir.txt"), + Content: base64.StdEncoding.EncodeToString([]byte("relative-in-missing-dir\n")), + Mode: "0644", + } - e.RunCommand("echo $?", false, "") + homeFile := api.File{ + Path: "~/home-file.txt", + Content: base64.StdEncoding.EncodeToString([]byte("home\n")), + Mode: "0777", + } - e.Stop() - e.Cleanup() + assert.Zero(t, e.InjectFiles([]api.File{ + absoluteFile, + absoluteFileInMissingDir, + relativeFile, + relativeFileInMissingDir, + homeFile, + })) + + assert.Zero(t, e.RunCommand(testsupport.Cat(absoluteFile.NormalizePath(homeDir)), false, "")) + assert.Zero(t, e.RunCommand(testsupport.Cat(absoluteFileInMissingDir.NormalizePath(homeDir)), false, "")) + assert.Zero(t, e.RunCommand(testsupport.Cat(relativeFile.NormalizePath(homeDir)), false, "")) + assert.Zero(t, e.RunCommand(testsupport.Cat(relativeFileInMissingDir.NormalizePath(homeDir)), false, "")) + assert.Zero(t, e.RunCommand(testsupport.Cat(homeFile.NormalizePath(homeDir)), false, "")) + assert.Zero(t, e.Stop()) + assert.Zero(t, e.Cleanup()) + + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + assert.Equal(t, simplifiedEvents, []string{ + "directive: Injecting Files", + fmt.Sprintf("Injecting %s with file mode 0400\n", absoluteFile.NormalizePath(homeDir)), + fmt.Sprintf("Injecting %s with file mode 0440\n", absoluteFileInMissingDir.NormalizePath(homeDir)), + fmt.Sprintf("Injecting %s with file mode 0600\n", relativeFile.NormalizePath(homeDir)), + fmt.Sprintf("Injecting %s with file mode 0644\n", relativeFileInMissingDir.NormalizePath(homeDir)), + fmt.Sprintf("Injecting %s with file mode 0777\n", homeFile.NormalizePath(homeDir)), + "Exit Code: 0", - assert.Equal(t, testLoggerBackend.SimplifiedEvents(), []string{ - "directive: echo 'here'", - "here\n", + fmt.Sprintf("directive: %s", testsupport.Cat(absoluteFile.NormalizePath(homeDir))), + "absolute\n", "Exit Code: 0", - "directive: " + multilineCmd, - "etc exists, multiline huzzahh!\n", + fmt.Sprintf("directive: %s", testsupport.Cat(absoluteFileInMissingDir.NormalizePath(homeDir))), + "absolute-in-missing-dir\n", "Exit Code: 0", - "directive: Exporting environment variables", - "Exporting A\n", + fmt.Sprintf("directive: %s", testsupport.Cat(relativeFile.NormalizePath(homeDir))), + "relative\n", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.Cat(relativeFileInMissingDir.NormalizePath(homeDir))), + "relative-in-missing-dir\n", "Exit Code: 0", - "directive: echo $A", - "foo\n", + fmt.Sprintf("directive: %s", testsupport.Cat(homeFile.NormalizePath(homeDir))), + "home\n", + "Exit Code: 0", + }) + + // Assert file modes + if runtime.GOOS == "windows" { + // windows file modes are a bit different, since it uses ACLs and not flags like unix. + // Therefore, 04xx means everybody can read it, 06xx means everybody can write and read it + // See: https://pkg.go.dev/os#Chmod + assertFileMode(t, absoluteFile.NormalizePath(homeDir), fs.FileMode(0444)) + assertFileMode(t, absoluteFileInMissingDir.NormalizePath(homeDir), fs.FileMode(0444)) + assertFileMode(t, relativeFile.NormalizePath(homeDir), fs.FileMode(0666)) + assertFileMode(t, relativeFileInMissingDir.NormalizePath(homeDir), fs.FileMode(0666)) + assertFileMode(t, homeFile.NormalizePath(homeDir), fs.FileMode(0666)) + } else { + assertFileMode(t, absoluteFile.NormalizePath(homeDir), fs.FileMode(0400)) + assertFileMode(t, absoluteFileInMissingDir.NormalizePath(homeDir), fs.FileMode(0440)) + assertFileMode(t, relativeFile.NormalizePath(homeDir), fs.FileMode(0600)) + assertFileMode(t, relativeFileInMissingDir.NormalizePath(homeDir), fs.FileMode(0644)) + assertFileMode(t, homeFile.NormalizePath(homeDir), fs.FileMode(0777)) + } + + os.Remove(absoluteFile.NormalizePath(homeDir)) + os.Remove(absoluteFileInMissingDir.NormalizePath(homeDir)) + os.Remove(relativeFile.NormalizePath(homeDir)) + os.Remove(relativeFileInMissingDir.NormalizePath(homeDir)) + os.Remove(homeFile.NormalizePath(homeDir)) +} + +func Test__ShellExecutor__MultilineCommand(t *testing.T) { + e, testLoggerBackend := setupShellExecutor(t, true) + + assert.Zero(t, e.RunCommand(testsupport.Multiline(), false, "")) + assert.Zero(t, e.Stop()) + assert.Zero(t, e.Cleanup()) + + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + assert.Equal(t, simplifiedEvents, []string{ + fmt.Sprintf("directive: %s", testsupport.Multiline()), + "etc exists, multiline huzzahh!\n", "Exit Code: 0", + }) +} + +func Test__ShellExecutor__ChangesCurrentDirectory(t *testing.T) { + e, testLoggerBackend := setupShellExecutor(t, true) + + dirName := "somedir" + absolutePath := filepath.Join(os.TempDir(), dirName, "some-file.txt") + relativePath := filepath.Join(dirName, "some-file.txt") + + fileInDir := api.File{ + Path: absolutePath, + Content: base64.StdEncoding.EncodeToString([]byte("content")), + Mode: "0644", + } + + assert.Zero(t, e.InjectFiles([]api.File{fileInDir})) + + // fails because current directory is not 'dirName' + assert.NotZero(t, e.RunCommand(testsupport.Cat(relativePath), false, "")) + + // works because we are now in the correct directory + assert.Zero(t, e.RunCommand(testsupport.Chdir(os.TempDir()), false, "")) + assert.Zero(t, e.RunCommand(testsupport.Cat(relativePath), false, "")) + assert.Zero(t, e.Stop()) + assert.Zero(t, e.Cleanup()) + + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(false) + assert.Nil(t, err) + + assert.Equal(t, simplifiedEvents, []string{ "directive: Injecting Files", - "Injecting /tmp/random-file.txt with file mode 0600\n", "Exit Code: 0", - "directive: cat /tmp/random-file.txt", - "aaabbb\n\n", + fmt.Sprintf("directive: %s", testsupport.Cat(relativePath)), + "Exit Code: 1", + + fmt.Sprintf("directive: %s", testsupport.Chdir(os.TempDir())), "Exit Code: 0", - "directive: echo $?", - "0\n", + fmt.Sprintf("directive: %s", testsupport.Cat(relativePath)), "Exit Code: 0", }) } -func Test__ShellExecutor__StopingRunningJob(t *testing.T) { - testsupport.SetupTestLogs() +func Test__ShellExecutor__ChangesEnvVars(t *testing.T) { + e, testLoggerBackend := setupShellExecutor(t, true) - testLogger, testLoggerBackend := eventlogger.DefaultTestLogger() + varName := "IMPORTANT_VAR" + assert.Zero(t, e.RunCommand(testsupport.EchoEnvVar(varName), false, "")) + assert.Zero(t, e.RunCommand(testsupport.SetEnvVar(varName, "IMPORTANT_VAR_VALUE"), false, "")) + assert.Zero(t, e.RunCommand(testsupport.EchoEnvVar(varName), false, "")) + assert.Zero(t, e.RunCommand(testsupport.UnsetEnvVar(varName), false, "")) - request := &api.JobRequest{ - SSHPublicKeys: []api.PublicKey{ - api.PublicKey(base64.StdEncoding.EncodeToString([]byte("ssh-rsa aaaaa"))), - }, - } + assert.Zero(t, e.Stop()) + assert.Zero(t, e.Cleanup()) + + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + assert.Equal(t, simplifiedEvents, []string{ + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar(varName)), + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.SetEnvVar(varName, "IMPORTANT_VAR_VALUE")), + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar(varName)), + "IMPORTANT_VAR_VALUE", + "Exit Code: 0", - e := NewShellExecutor(request, testLogger, true) + fmt.Sprintf("directive: %s", testsupport.UnsetEnvVar(varName)), + "Exit Code: 0", + }) +} - e.Prepare() - e.Start() +func Test__ShellExecutor__StoppingRunningJob(t *testing.T) { + e, testLoggerBackend := setupShellExecutor(t, true) go func() { - e.RunCommand("echo 'here'", false, "") - e.RunCommand("sleep 5", false, "") + e.RunCommand("echo here", false, "") + e.RunCommand("sleep 20", false, "") }() - time.Sleep(1 * time.Second) + time.Sleep(10 * time.Second) - e.Stop() - e.Cleanup() + assert.Zero(t, e.Stop()) + assert.Zero(t, e.Cleanup()) time.Sleep(1 * time.Second) - assert.Equal(t, testLoggerBackend.SimplifiedEvents()[0:4], []string{ - "directive: echo 'here'", + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + assert.Equal(t, simplifiedEvents[0:4], []string{ + "directive: echo here", "here\n", "Exit Code: 0", - "directive: sleep 5", + "directive: sleep 20", }) } func Test__ShellExecutor__LargeCommandOutput(t *testing.T) { - testsupport.SetupTestLogs() - - testLogger, testLoggerBackend := eventlogger.DefaultTestLogger() - - request := &api.JobRequest{ - SSHPublicKeys: []api.PublicKey{ - api.PublicKey(base64.StdEncoding.EncodeToString([]byte("ssh-rsa aaaaa"))), - }, - } - - e := NewShellExecutor(request, testLogger, true) - - e.Prepare() - e.Start() + e, testLoggerBackend := setupShellExecutor(t, true) go func() { - e.RunCommand("for i in {1..100}; { printf 'hello'; }", false, "") + assert.Zero(t, e.RunCommand(testsupport.LargeOutputCommand(), false, "")) }() time.Sleep(5 * time.Second) - e.Stop() - e.Cleanup() + assert.Zero(t, e.Stop()) + assert.Zero(t, e.Cleanup()) time.Sleep(1 * time.Second) - assert.Equal(t, testLoggerBackend.SimplifiedEvents(), []string{ - "directive: for i in {1..100}; { printf 'hello'; }", + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + assert.Equal(t, simplifiedEvents, []string{ + fmt.Sprintf("directive: %s", testsupport.LargeOutputCommand()), "hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello", "hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello", "hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello", @@ -166,3 +356,86 @@ func Test__ShellExecutor__LargeCommandOutput(t *testing.T) { "Exit Code: 0", }) } + +func Test__ShellExecutor__Unicode(t *testing.T) { + e, testLoggerBackend := setupShellExecutor(t, true) + assert.Zero(t, e.RunCommand(testsupport.Output(UnicodeOutput1), false, "")) + assert.Zero(t, e.RunCommand(testsupport.Output(UnicodeOutput2), false, "")) + assert.Zero(t, e.Stop()) + assert.Zero(t, e.Cleanup()) + + time.Sleep(1 * time.Second) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + assert.Equal(t, simplifiedEvents, []string{ + fmt.Sprintf("directive: %s", testsupport.Output(UnicodeOutput1)), + "特定の伝説に拠る物語の由来については諸説存在し。特定の伝説に拠る物", + "語の由来については諸説存在し。特定の伝説に拠る物語の由来については", + "諸説存在し。", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.Output(UnicodeOutput2)), + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", + "━━━━━━━━━━━━━━━━━━━━━━", + "Exit Code: 0", + }) +} + +func Test__ShellExecutor__BrokenUnicode(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip() + } + + e, testLoggerBackend := setupShellExecutor(t, true) + + go func() { + assert.Zero(t, e.RunCommand(testsupport.EchoBrokenUnicode(), false, "")) + }() + + time.Sleep(5 * time.Second) + + assert.Zero(t, e.Stop()) + assert.Zero(t, e.Cleanup()) + + time.Sleep(1 * time.Second) + + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + assert.Equal(t, simplifiedEvents, []string{ + fmt.Sprintf("directive: %s", testsupport.EchoBrokenUnicode()), + "\x96\x96\x96\x96\x96", + "Exit Code: 0", + }) +} + +func basicRequest() *api.JobRequest { + return &api.JobRequest{ + SSHPublicKeys: []api.PublicKey{ + api.PublicKey(base64.StdEncoding.EncodeToString([]byte("ssh-rsa aaaaa"))), + }, + } +} + +func assertFileMode(t *testing.T, fileName string, fileMode fs.FileMode) { + stat, err := os.Stat(fileName) + if assert.Nil(t, err) { + assert.Equal(t, stat.Mode(), fileMode) + } +} + +func setupShellExecutor(t *testing.T, selfHosted bool) (*ShellExecutor, *eventlogger.InMemoryBackend) { + testsupport.SetupTestLogs() + + testLogger, testLoggerBackend := eventlogger.DefaultTestLogger() + e := NewShellExecutor(basicRequest(), testLogger, selfHosted) + + assert.Zero(t, e.Prepare()) + assert.Zero(t, e.Start()) + + return e, testLoggerBackend +} diff --git a/pkg/executors/ssh_jump_point.go b/pkg/executors/ssh_jump_point.go index df59a9a2..5494b559 100644 --- a/pkg/executors/ssh_jump_point.go +++ b/pkg/executors/ssh_jump_point.go @@ -15,6 +15,6 @@ func SetUpSSHJumpPoint(script string) error { } _, err = f.WriteString(script) - + _ = f.Close() return err } diff --git a/pkg/jobs/job.go b/pkg/jobs/job.go index d622a53a..17462075 100644 --- a/pkg/jobs/job.go +++ b/pkg/jobs/job.go @@ -35,6 +35,7 @@ type Job struct { type JobOptions struct { Request *api.JobRequest Client *http.Client + Logger *eventlogger.Logger ExposeKvmDevice bool FileInjections []config.FileInjection FailOnMissingFiles bool @@ -53,6 +54,8 @@ func NewJob(request *api.JobRequest, client *http.Client) (*Job, error) { } func NewJobWithOptions(options *JobOptions) (*Job, error) { + log.Debugf("Job Request %+v", options.Request) + if options.Request.Executor == "" { log.Infof("No executor specified - using %s executor", executors.ExecutorTypeShell) options.Request.Executor = executors.ExecutorTypeShell @@ -63,26 +66,31 @@ func NewJobWithOptions(options *JobOptions) (*Job, error) { options.Request.Logger.Method = eventlogger.LoggerMethodPull } - logger, err := eventlogger.CreateLogger(options.Request) - if err != nil { - return nil, err + job := &Job{ + Client: options.Client, + Request: options.Request, + JobLogArchived: false, + Stopped: false, } - executor, err := CreateExecutor(options.Request, logger, *options) + if options.Logger != nil { + job.Logger = options.Logger + } else { + l, err := eventlogger.CreateLogger(options.Request) + if err != nil { + return nil, err + } + + job.Logger = l + } + + executor, err := CreateExecutor(options.Request, job.Logger, *options) if err != nil { return nil, err } - log.Debugf("Job Request %+v", options.Request) - - return &Job{ - Client: options.Client, - Request: options.Request, - Executor: executor, - JobLogArchived: false, - Stopped: false, - Logger: logger, - }, nil + job.Executor = executor + return job, nil } func CreateExecutor(request *api.JobRequest, logger *eventlogger.Logger, jobOptions JobOptions) (executors.Executor, error) { @@ -103,18 +111,20 @@ func CreateExecutor(request *api.JobRequest, logger *eventlogger.Logger, jobOpti } type RunOptions struct { - EnvVars []config.HostEnvVar - FileInjections []config.FileInjection - OnSuccessfulTeardown func() - OnFailedTeardown func() + EnvVars []config.HostEnvVar + FileInjections []config.FileInjection + OnSuccessfulTeardown func() + OnFailedTeardown func() + CallbackRetryAttempts int } func (job *Job) Run() { job.RunWithOptions(RunOptions{ - EnvVars: []config.HostEnvVar{}, - FileInjections: []config.FileInjection{}, - OnSuccessfulTeardown: nil, - OnFailedTeardown: nil, + EnvVars: []config.HostEnvVar{}, + FileInjections: []config.FileInjection{}, + OnSuccessfulTeardown: nil, + OnFailedTeardown: nil, + CallbackRetryAttempts: 60, }) } @@ -142,7 +152,7 @@ func (job *Job) RunWithOptions(options RunOptions) { } } - err := job.Teardown(result) + err := job.Teardown(result, options.CallbackRetryAttempts) if err != nil { callFuncIfNotNull(options.OnFailedTeardown) } else { @@ -188,7 +198,11 @@ func (job *Job) RunRegularCommands(hostEnvVars []config.HostEnvVar) string { return JobFailed } - exitCode = job.RunCommandsUntilFirstFailure(job.Request.Commands) + if len(job.Request.Commands) == 0 { + exitCode = 0 + } else { + exitCode = job.RunCommandsUntilFirstFailure(job.Request.Commands) + } if job.Stopped { log.Info("Regular commands were stopped") @@ -253,13 +267,13 @@ func (job *Job) RunCommandsUntilFirstFailure(commands []api.Command) int { return lastExitCode } -func (job *Job) Teardown(result string) error { +func (job *Job) Teardown(result string, callbackRetryAttempts int) error { // if job was stopped during the epilogues, result should be stopped if job.Stopped { result = JobStopped } - err := job.SendFinishedCallback(result) + err := job.SendFinishedCallback(result, callbackRetryAttempts) if err != nil { log.Errorf("Could not send finished callback: %v", err) return err @@ -286,7 +300,7 @@ func (job *Job) Teardown(result string) error { log.Errorf("Error closing logger: %+v", err) } - err = job.SendTeardownFinishedCallback() + err = job.SendTeardownFinishedCallback(callbackRetryAttempts) if err != nil { log.Errorf("Could not send teardown finished callback: %v", err) return err @@ -308,17 +322,17 @@ func (job *Job) Stop() { }) } -func (job *Job) SendFinishedCallback(result string) error { +func (job *Job) SendFinishedCallback(result string, retries int) error { payload := fmt.Sprintf(`{"result": "%s"}`, result) log.Infof("Sending finished callback: %+v", payload) - return retry.RetryWithConstantWait("Send finished callback", 60, time.Second, func() error { + return retry.RetryWithConstantWait("Send finished callback", retries, time.Second, func() error { return job.SendCallback(job.Request.Callbacks.Finished, payload) }) } -func (job *Job) SendTeardownFinishedCallback() error { +func (job *Job) SendTeardownFinishedCallback(retries int) error { log.Info("Sending teardown finished callback") - return retry.RetryWithConstantWait("Send teardown finished callback", 60, time.Second, func() error { + return retry.RetryWithConstantWait("Send teardown finished callback", retries, time.Second, func() error { return job.SendCallback(job.Request.Callbacks.TeardownFinished, "{}") }) } diff --git a/pkg/jobs/job_test.go b/pkg/jobs/job_test.go new file mode 100644 index 00000000..2446454b --- /dev/null +++ b/pkg/jobs/job_test.go @@ -0,0 +1,953 @@ +package jobs + +import ( + "encoding/base64" + "fmt" + "net/http" + "os/exec" + "runtime" + "testing" + "time" + + "github.com/semaphoreci/agent/pkg/api" + eventlogger "github.com/semaphoreci/agent/pkg/eventlogger" + testsupport "github.com/semaphoreci/agent/test/support" + "github.com/stretchr/testify/assert" +) + +func Test__EnvVarsAreAvailableToCommands(t *testing.T) { + testLogger, testLoggerBackend := eventlogger.DefaultTestLogger() + request := &api.JobRequest{ + Commands: []api.Command{ + {Directive: testsupport.EchoEnvVar("A")}, + {Directive: testsupport.EchoEnvVar("B")}, + {Directive: testsupport.EchoEnvVar("C")}, + }, + EnvVars: []api.EnvVar{ + {Name: "A", Value: base64.StdEncoding.EncodeToString([]byte("VALUE_A"))}, + {Name: "B", Value: base64.StdEncoding.EncodeToString([]byte("VALUE_B"))}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + }, + } + + job, err := NewJobWithOptions(&JobOptions{ + Request: request, + Client: http.DefaultClient, + Logger: testLogger, + }) + + assert.Nil(t, err) + + job.Run() + assert.True(t, job.Finished) + + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + assert.Equal(t, simplifiedEvents, []string{ + "job_started", + + "directive: Exporting environment variables", + "Exporting A\n", + "Exporting B\n", + "Exit Code: 0", + + "directive: Injecting Files", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("A")), + "VALUE_A", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("B")), + "VALUE_B", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("C")), + "Exit Code: 0", + + "directive: Exporting environment variables", + "Exporting SEMAPHORE_JOB_RESULT\n", + "Exit Code: 0", + + "job_finished: passed", + }) +} + +func Test__EnvVarsAreAvailableToEpilogueAlwaysAndOnPass(t *testing.T) { + testLogger, testLoggerBackend := eventlogger.DefaultTestLogger() + request := &api.JobRequest{ + Commands: []api.Command{}, + EnvVars: []api.EnvVar{ + {Name: "A", Value: base64.StdEncoding.EncodeToString([]byte("VALUE_A"))}, + {Name: "B", Value: base64.StdEncoding.EncodeToString([]byte("VALUE_B"))}, + }, + EpilogueAlwaysCommands: []api.Command{ + {Directive: testsupport.Output("On EpilogueAlways")}, + {Directive: testsupport.EchoEnvVar("A")}, + {Directive: testsupport.EchoEnvVar("B")}, + {Directive: testsupport.EchoEnvVar("C")}, + {Directive: testsupport.EchoEnvVar("SEMAPHORE_JOB_RESULT")}, + }, + EpilogueOnPassCommands: []api.Command{ + {Directive: testsupport.Output("On EpilogueOnPass")}, + {Directive: testsupport.EchoEnvVar("A")}, + {Directive: testsupport.EchoEnvVar("B")}, + {Directive: testsupport.EchoEnvVar("C")}, + {Directive: testsupport.EchoEnvVar("SEMAPHORE_JOB_RESULT")}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + }, + } + + job, err := NewJobWithOptions(&JobOptions{ + Request: request, + Client: http.DefaultClient, + Logger: testLogger, + }) + + assert.Nil(t, err) + + job.Run() + assert.True(t, job.Finished) + + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + assert.Equal(t, simplifiedEvents, []string{ + "job_started", + + "directive: Exporting environment variables", + "Exporting A\n", + "Exporting B\n", + "Exit Code: 0", + + "directive: Injecting Files", + "Exit Code: 0", + + "directive: Exporting environment variables", + "Exporting SEMAPHORE_JOB_RESULT\n", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.Output("On EpilogueAlways")), + "On EpilogueAlways", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("A")), + "VALUE_A", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("B")), + "VALUE_B", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("C")), + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("SEMAPHORE_JOB_RESULT")), + "passed", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.Output("On EpilogueOnPass")), + "On EpilogueOnPass", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("A")), + "VALUE_A", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("B")), + "VALUE_B", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("C")), + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("SEMAPHORE_JOB_RESULT")), + "passed", + "Exit Code: 0", + + "job_finished: passed", + }) +} + +func Test__EnvVarsAreAvailableToEpilogueAlwaysAndOnFail(t *testing.T) { + testLogger, testLoggerBackend := eventlogger.DefaultTestLogger() + request := &api.JobRequest{ + Commands: []api.Command{ + {Directive: "badcommand"}, + }, + EnvVars: []api.EnvVar{ + {Name: "A", Value: base64.StdEncoding.EncodeToString([]byte("VALUE_A"))}, + {Name: "B", Value: base64.StdEncoding.EncodeToString([]byte("VALUE_B"))}, + }, + EpilogueAlwaysCommands: []api.Command{ + {Directive: testsupport.Output("On EpilogueAlways")}, + {Directive: testsupport.EchoEnvVar("A")}, + {Directive: testsupport.EchoEnvVar("B")}, + {Directive: testsupport.EchoEnvVar("C")}, + {Directive: testsupport.EchoEnvVar("SEMAPHORE_JOB_RESULT")}, + }, + EpilogueOnFailCommands: []api.Command{ + {Directive: testsupport.Output("On EpilogueOnFail")}, + {Directive: testsupport.EchoEnvVar("A")}, + {Directive: testsupport.EchoEnvVar("B")}, + {Directive: testsupport.EchoEnvVar("C")}, + {Directive: testsupport.EchoEnvVar("SEMAPHORE_JOB_RESULT")}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + }, + } + + job, err := NewJobWithOptions(&JobOptions{ + Request: request, + Client: http.DefaultClient, + Logger: testLogger, + }) + + assert.Nil(t, err) + + job.Run() + assert.True(t, job.Finished) + + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + testsupport.AssertSimplifiedJobLogs(t, simplifiedEvents, []string{ + "job_started", + + "directive: Exporting environment variables", + "Exporting A\n", + "Exporting B\n", + "Exit Code: 0", + + "directive: Injecting Files", + "Exit Code: 0", + + "directive: badcommand", + "*** OUTPUT ***", + fmt.Sprintf("Exit Code: %d", testsupport.UnknownCommandExitCode()), + + "directive: Exporting environment variables", + "Exporting SEMAPHORE_JOB_RESULT\n", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.Output("On EpilogueAlways")), + "On EpilogueAlways", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("A")), + "VALUE_A", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("B")), + "VALUE_B", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("C")), + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("SEMAPHORE_JOB_RESULT")), + "failed", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.Output("On EpilogueOnFail")), + "On EpilogueOnFail", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("A")), + "VALUE_A", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("B")), + "VALUE_B", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("C")), + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("SEMAPHORE_JOB_RESULT")), + "failed", + "Exit Code: 0", + + "job_finished: failed", + }) +} + +func Test__EpilogueOnPassOnlyExecutesOnSuccessfulJob(t *testing.T) { + testLogger, testLoggerBackend := eventlogger.DefaultTestLogger() + request := &api.JobRequest{ + EnvVars: []api.EnvVar{}, + Commands: []api.Command{ + {Directive: testsupport.Output("hello")}, + }, + EpilogueAlwaysCommands: []api.Command{ + {Directive: testsupport.Output("On epilogue always")}, + }, + EpilogueOnFailCommands: []api.Command{ + {Directive: testsupport.Output("On epilogue on fail")}, + }, + EpilogueOnPassCommands: []api.Command{ + {Directive: testsupport.Output("On epilogue on pass")}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + }, + } + + job, err := NewJobWithOptions(&JobOptions{ + Request: request, + Client: http.DefaultClient, + Logger: testLogger, + }) + + assert.Nil(t, err) + + job.Run() + assert.True(t, job.Finished) + + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + assert.Equal(t, simplifiedEvents, []string{ + "job_started", + + "directive: Exporting environment variables", + "Exit Code: 0", + + "directive: Injecting Files", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.Output("hello")), + "hello", + "Exit Code: 0", + + "directive: Exporting environment variables", + "Exporting SEMAPHORE_JOB_RESULT\n", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.Output("On epilogue always")), + "On epilogue always", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.Output("On epilogue on pass")), + "On epilogue on pass", + "Exit Code: 0", + + "job_finished: passed", + }) +} + +func Test__EpilogueOnFailOnlyExecutesOnFailedJob(t *testing.T) { + testLogger, testLoggerBackend := eventlogger.DefaultTestLogger() + request := &api.JobRequest{ + EnvVars: []api.EnvVar{}, + Commands: []api.Command{ + {Directive: "badcommand"}, + }, + EpilogueAlwaysCommands: []api.Command{ + {Directive: testsupport.Output("On epilogue always")}, + }, + EpilogueOnFailCommands: []api.Command{ + {Directive: testsupport.Output("On epilogue on fail")}, + }, + EpilogueOnPassCommands: []api.Command{ + {Directive: testsupport.Output("On epilogue on pass")}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + }, + } + + job, err := NewJobWithOptions(&JobOptions{ + Request: request, + Client: http.DefaultClient, + Logger: testLogger, + }) + + assert.Nil(t, err) + + job.Run() + assert.True(t, job.Finished) + + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + testsupport.AssertSimplifiedJobLogs(t, simplifiedEvents, []string{ + "job_started", + + "directive: Exporting environment variables", + "Exit Code: 0", + + "directive: Injecting Files", + "Exit Code: 0", + + "directive: badcommand", + "*** OUTPUT ***", + fmt.Sprintf("Exit Code: %d", testsupport.UnknownCommandExitCode()), + + "directive: Exporting environment variables", + "Exporting SEMAPHORE_JOB_RESULT\n", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.Output("On epilogue always")), + "On epilogue always", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.Output("On epilogue on fail")), + "On epilogue on fail", + "Exit Code: 0", + + "job_finished: passed", + }) +} + +func Test__UsingCommandAliases(t *testing.T) { + testLogger, testLoggerBackend := eventlogger.DefaultTestLogger() + request := &api.JobRequest{ + EnvVars: []api.EnvVar{}, + Commands: []api.Command{ + {Directive: testsupport.Output("hello world"), Alias: "Display Hello World"}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + }, + } + + job, err := NewJobWithOptions(&JobOptions{ + Request: request, + Client: http.DefaultClient, + Logger: testLogger, + }) + + assert.Nil(t, err) + + job.Run() + assert.True(t, job.Finished) + + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + assert.Equal(t, simplifiedEvents, []string{ + "job_started", + + "directive: Exporting environment variables", + "Exit Code: 0", + + "directive: Injecting Files", + "Exit Code: 0", + + "directive: Display Hello World", + fmt.Sprintf("Running: %s\n", testsupport.Output("hello world")), + "hello world", + "Exit Code: 0", + + "directive: Exporting environment variables", + "Exporting SEMAPHORE_JOB_RESULT\n", + "Exit Code: 0", + + "job_finished: passed", + }) +} + +func Test__StopJob(t *testing.T) { + testLogger, testLoggerBackend := eventlogger.DefaultTestLogger() + request := &api.JobRequest{ + EnvVars: []api.EnvVar{}, + Commands: []api.Command{ + {Directive: "sleep 60"}, + {Directive: testsupport.Output("hello")}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + }, + } + + job, err := NewJobWithOptions(&JobOptions{ + Request: request, + Client: http.DefaultClient, + Logger: testLogger, + }) + + assert.Nil(t, err) + + go job.Run() + + time.Sleep(10 * time.Second) + job.Stop() + + assert.True(t, job.Stopped) + assert.Eventually(t, func() bool { return job.Finished }, 5*time.Second, 1*time.Second) + + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + assert.Equal(t, simplifiedEvents, []string{ + "job_started", + + "directive: Exporting environment variables", + "Exit Code: 0", + + "directive: Injecting Files", + "Exit Code: 0", + + "directive: sleep 60", + fmt.Sprintf("Exit Code: %d", testsupport.StoppedCommandExitCode()), + + "job_finished: stopped", + }) +} + +func Test__StopJobOnEpilogue(t *testing.T) { + testLogger, testLoggerBackend := eventlogger.DefaultTestLogger() + request := &api.JobRequest{ + EnvVars: []api.EnvVar{}, + Commands: []api.Command{ + {Directive: testsupport.Output("hello")}, + }, + EpilogueAlwaysCommands: []api.Command{ + {Directive: "sleep 60"}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + }, + } + + job, err := NewJobWithOptions(&JobOptions{ + Request: request, + Client: http.DefaultClient, + Logger: testLogger, + }) + + assert.Nil(t, err) + + go job.Run() + + time.Sleep(10 * time.Second) + job.Stop() + + assert.True(t, job.Stopped) + assert.Eventually(t, func() bool { return job.Finished }, 5*time.Second, 1*time.Second) + + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + assert.Equal(t, simplifiedEvents, []string{ + "job_started", + + "directive: Exporting environment variables", + "Exit Code: 0", + + "directive: Injecting Files", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.Output("hello")), + "hello", + "Exit Code: 0", + + "directive: Exporting environment variables", + "Exporting SEMAPHORE_JOB_RESULT\n", + "Exit Code: 0", + + "directive: sleep 60", + fmt.Sprintf("Exit Code: %d", testsupport.StoppedCommandExitCode()), + + "job_finished: stopped", + }) +} + +func Test__STTYRestoration(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Windows does not support pty") + } + + testLogger, testLoggerBackend := eventlogger.DefaultTestLogger() + request := &api.JobRequest{ + EnvVars: []api.EnvVar{}, + Commands: []api.Command{ + {Directive: "stty echo"}, + {Directive: "echo Hello World"}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + }, + } + + job, err := NewJobWithOptions(&JobOptions{Request: request, Client: http.DefaultClient, Logger: testLogger}) + assert.Nil(t, err) + + job.Run() + assert.True(t, job.Finished) + + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + assert.Equal(t, simplifiedEvents, []string{ + "job_started", + + "directive: Exporting environment variables", + "Exit Code: 0", + + "directive: Injecting Files", + "Exit Code: 0", + + "directive: stty echo", + "Exit Code: 0", + + "directive: echo Hello World", + "Hello World\n", + "Exit Code: 0", + + "directive: Exporting environment variables", + "Exporting SEMAPHORE_JOB_RESULT\n", + "Exit Code: 0", + + "job_finished: passed", + }) +} + +func Test__BackgroundJobIsKilledAfterJobIsDoneInWindows(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip() + } + + testLogger, testLoggerBackend := eventlogger.DefaultTestLogger() + request := &api.JobRequest{ + EnvVars: []api.EnvVar{}, + Commands: []api.Command{ + {Directive: "Start-Process ping -ArgumentList '-n','300','127.0.0.1'"}, + {Directive: "sleep 5"}, + {Directive: "(Get-Process ping -ErrorAction SilentlyContinue) -and ($true)"}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + }, + } + + job, err := NewJobWithOptions(&JobOptions{Request: request, Client: http.DefaultClient, Logger: testLogger}) + assert.Nil(t, err) + + job.Run() + assert.True(t, job.Finished) + + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + assert.Equal(t, simplifiedEvents, []string{ + "job_started", + + "directive: Exporting environment variables", + "Exit Code: 0", + + "directive: Injecting Files", + "Exit Code: 0", + + "directive: Start-Process ping -ArgumentList '-n','300','127.0.0.1'", + "Exit Code: 0", + + "directive: sleep 5", + "Exit Code: 0", + + "directive: (Get-Process ping -ErrorAction SilentlyContinue) -and ($true)", + "True\n", + "Exit Code: 0", + + "directive: Exporting environment variables", + "Exporting SEMAPHORE_JOB_RESULT\n", + "Exit Code: 0", + + "job_finished: passed", + }) + + // assert process is not running anymore + cmd := exec.Command( + "powershell", + "-NoProfile", + "-NonInteractive", + "-Command", + "(Get-Process ping -ErrorAction SilentlyContinue) -and ($true)", + ) + + output, err := cmd.CombinedOutput() + assert.Nil(t, err) + assert.Equal(t, "False\r\n", string(output)) +} + +func Test__BackgroundJobIsKilledAfterJobIsDoneInNonWindows(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip() + } + + testLogger, testLoggerBackend := eventlogger.DefaultTestLogger() + request := &api.JobRequest{ + EnvVars: []api.EnvVar{}, + Commands: []api.Command{ + {Directive: "ping -c 300 127.0.0.1 > /dev/null &"}, + {Directive: "sleep 5"}, + {Directive: "pgrep ping > /dev/null && true"}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + }, + } + + job, err := NewJobWithOptions(&JobOptions{Request: request, Client: http.DefaultClient, Logger: testLogger}) + assert.Nil(t, err) + + job.Run() + assert.True(t, job.Finished) + + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + assert.Equal(t, simplifiedEvents, []string{ + "job_started", + + "directive: Exporting environment variables", + "Exit Code: 0", + + "directive: Injecting Files", + "Exit Code: 0", + + "directive: ping -c 300 127.0.0.1 > /dev/null &", + "Exit Code: 0", + + "directive: sleep 5", + "Exit Code: 0", + + "directive: pgrep ping > /dev/null && true", + "Exit Code: 0", + + "directive: Exporting environment variables", + "Exporting SEMAPHORE_JOB_RESULT\n", + "Exit Code: 0", + + "job_finished: passed", + }) + + // assert process is not running anymore + cmd := exec.Command( + "bash", + "-c", + "'pgrep ping > /dev/null && true'", + ) + + _, err = cmd.CombinedOutput() + assert.NotNil(t, err) +} + +func Test__KillingRootBash(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip() + } + + testLogger, testLoggerBackend := eventlogger.DefaultTestLogger() + request := &api.JobRequest{ + EnvVars: []api.EnvVar{}, + Commands: []api.Command{ + {Directive: "sleep infinity &"}, + {Directive: "exit 1"}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + }, + } + + job, err := NewJobWithOptions(&JobOptions{Request: request, Client: http.DefaultClient, Logger: testLogger}) + assert.Nil(t, err) + + job.Run() + assert.True(t, job.Finished) + + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + assert.Equal(t, simplifiedEvents, []string{ + "job_started", + + "directive: Exporting environment variables", + "Exit Code: 0", + + "directive: Injecting Files", + "Exit Code: 0", + + "directive: sleep infinity &", + "Exit Code: 0", + + "directive: exit 1", + "Exit Code: 1", + + "directive: Exporting environment variables", + "Exporting SEMAPHORE_JOB_RESULT\n", + "Exit Code: 1", + + "job_finished: failed", + }) +} + +func Test__BashSetE(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip() + } + + testLogger, testLoggerBackend := eventlogger.DefaultTestLogger() + request := &api.JobRequest{ + EnvVars: []api.EnvVar{}, + Commands: []api.Command{ + {Directive: "sleep infinity &"}, + {Directive: "set -e"}, + {Directive: "false"}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + }, + } + + job, err := NewJobWithOptions(&JobOptions{Request: request, Client: http.DefaultClient, Logger: testLogger}) + assert.Nil(t, err) + + job.Run() + assert.True(t, job.Finished) + + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + assert.Equal(t, simplifiedEvents, []string{ + "job_started", + + "directive: Exporting environment variables", + "Exit Code: 0", + + "directive: Injecting Files", + "Exit Code: 0", + + "directive: sleep infinity &", + "Exit Code: 0", + + "directive: set -e", + "Exit Code: 0", + + "directive: false", + "Exit Code: 1", + + "directive: Exporting environment variables", + "Exporting SEMAPHORE_JOB_RESULT\n", + "Exit Code: 1", + + "job_finished: failed", + }) +} + +func Test__BashSetPipefail(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip() + } + + testLogger, testLoggerBackend := eventlogger.DefaultTestLogger() + request := &api.JobRequest{ + EnvVars: []api.EnvVar{}, + Commands: []api.Command{ + {Directive: "sleep infinity &"}, + {Directive: "set -eo pipefail"}, + {Directive: "cat non_existant | sort"}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + }, + } + + job, err := NewJobWithOptions(&JobOptions{Request: request, Client: http.DefaultClient, Logger: testLogger}) + assert.Nil(t, err) + + job.Run() + assert.True(t, job.Finished) + + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + assert.Equal(t, simplifiedEvents, []string{ + "job_started", + + "directive: Exporting environment variables", + "Exit Code: 0", + + "directive: Injecting Files", + "Exit Code: 0", + + "directive: sleep infinity &", + "Exit Code: 0", + + "directive: set -eo pipefail", + "Exit Code: 0", + + "directive: cat non_existant | sort", + "cat: non_existant: No such file or directory\n", + "Exit Code: 1", + + "directive: Exporting environment variables", + "Exporting SEMAPHORE_JOB_RESULT\n", + "Exit Code: 1", + + "job_finished: failed", + }) +} diff --git a/pkg/listener/job_processor.go b/pkg/listener/job_processor.go index 83e3f33b..28223c99 100644 --- a/pkg/listener/job_processor.go +++ b/pkg/listener/job_processor.go @@ -28,17 +28,20 @@ func StartJobProcessor(httpClient *http.Client, apiClient *selfhostedapi.API, co State: selfhostedapi.AgentStateWaitingForJobs, SyncInterval: 5 * time.Second, DisconnectRetryAttempts: 100, + GetJobRetryAttempts: config.GetJobRetryLimit, + CallbackRetryAttempts: config.CallbackRetryLimit, ShutdownHookPath: config.ShutdownHookPath, DisconnectAfterJob: config.DisconnectAfterJob, DisconnectAfterIdleTimeout: config.DisconnectAfterIdleTimeout, EnvVars: config.EnvVars, FileInjections: config.FileInjections, FailOnMissingFiles: config.FailOnMissingFiles, + ExitOnShutdown: config.ExitOnShutdown, } go p.Start() - p.SetupInteruptHandler() + p.SetupInterruptHandler() return p, nil } @@ -54,6 +57,8 @@ type JobProcessor struct { LastSuccessfulSync time.Time LastStateChangeAt time.Time DisconnectRetryAttempts int + GetJobRetryAttempts int + CallbackRetryAttempts int ShutdownHookPath string StopSync bool DisconnectAfterJob bool @@ -61,6 +66,8 @@ type JobProcessor struct { EnvVars []config.HostEnvVar FileInjections []config.FileInjection FailOnMissingFiles bool + ExitOnShutdown bool + ShutdownReason ShutdownReason } func (p *JobProcessor) Start() { @@ -187,9 +194,10 @@ func (p *JobProcessor) RunJob(jobID string) { p.CurrentJob = job go job.RunWithOptions(jobs.RunOptions{ - EnvVars: p.EnvVars, - FileInjections: p.FileInjections, - OnSuccessfulTeardown: p.JobFinished, + EnvVars: p.EnvVars, + CallbackRetryAttempts: p.CallbackRetryAttempts, + FileInjections: p.FileInjections, + OnSuccessfulTeardown: p.JobFinished, OnFailedTeardown: func() { if p.DisconnectAfterJob { p.Shutdown(ShutdownReasonJobFinished, 1) @@ -202,7 +210,7 @@ func (p *JobProcessor) RunJob(jobID string) { func (p *JobProcessor) getJobWithRetries(jobID string) (*api.JobRequest, error) { var jobRequest *api.JobRequest - err := retry.RetryWithConstantWait("Get job", 10, 3*time.Second, func() error { + err := retry.RetryWithConstantWait("Get job", p.GetJobRetryAttempts, 3*time.Second, func() error { job, err := p.APIClient.GetJob(jobID) if err != nil { return err @@ -236,7 +244,7 @@ func (p *JobProcessor) WaitForJobs() { p.setState(selfhostedapi.AgentStateWaitingForJobs) } -func (p *JobProcessor) SetupInteruptHandler() { +func (p *JobProcessor) SetupInterruptHandler() { c := make(chan os.Signal) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { @@ -263,10 +271,15 @@ func (p *JobProcessor) disconnect() { } func (p *JobProcessor) Shutdown(reason ShutdownReason, code int) { + p.ShutdownReason = reason + p.disconnect() p.executeShutdownHook(reason) log.Infof("Agent shutting down due to: %s", reason) - os.Exit(code) + + if p.ExitOnShutdown { + os.Exit(code) + } } func (p *JobProcessor) executeShutdownHook(reason ShutdownReason) { diff --git a/pkg/listener/listener.go b/pkg/listener/listener.go index a96349a9..004a4d3d 100644 --- a/pkg/listener/listener.go +++ b/pkg/listener/listener.go @@ -4,7 +4,6 @@ import ( "crypto/rand" "encoding/base64" "fmt" - "io" "net/http" "os" "time" @@ -25,6 +24,8 @@ type Listener struct { type Config struct { Endpoint string RegisterRetryLimit int + GetJobRetryLimit int + CallbackRetryLimit int Token string Scheme string ShutdownHookPath string @@ -33,10 +34,11 @@ type Config struct { EnvVars []config.HostEnvVar FileInjections []config.FileInjection FailOnMissingFiles bool + ExitOnShutdown bool AgentVersion string } -func Start(httpClient *http.Client, config Config, logger io.Writer) (*Listener, error) { +func Start(httpClient *http.Client, config Config) (*Listener, error) { listener := &Listener{ Config: config, Client: selfhostedapi.New(httpClient, config.Scheme, config.Endpoint, config.Token), @@ -62,6 +64,11 @@ func Start(httpClient *http.Client, config Config, logger io.Writer) (*Listener, return listener, nil } +// only used during tests +func (l *Listener) Stop() { + l.JobProcessor.Shutdown(ShutdownReasonRequested, 0) +} + func (l *Listener) DisplayHelloMessage() { fmt.Println(" ") fmt.Println(" 00000000000 ") diff --git a/pkg/listener/listener_test.go b/pkg/listener/listener_test.go new file mode 100644 index 00000000..9184d470 --- /dev/null +++ b/pkg/listener/listener_test.go @@ -0,0 +1,793 @@ +package listener + +import ( + "fmt" + "io/ioutil" + "net/http" + "os" + "runtime" + "strings" + "testing" + "time" + + "github.com/semaphoreci/agent/pkg/api" + "github.com/semaphoreci/agent/pkg/config" + "github.com/semaphoreci/agent/pkg/eventlogger" + "github.com/semaphoreci/agent/pkg/listener/selfhostedapi" + testsupport "github.com/semaphoreci/agent/test/support" + "github.com/stretchr/testify/assert" +) + +func Test__Register(t *testing.T) { + testsupport.SetupTestLogs() + + loghubMockServer := testsupport.NewLoghubMockServer() + loghubMockServer.Init() + + hubMockServer := testsupport.NewHubMockServer() + hubMockServer.Init() + hubMockServer.UseLogsURL(loghubMockServer.URL()) + + config := Config{ + ExitOnShutdown: false, + Endpoint: hubMockServer.Host(), + Token: "token", + RegisterRetryLimit: 5, + Scheme: "http", + EnvVars: []config.HostEnvVar{}, + FileInjections: []config.FileInjection{}, + AgentVersion: "0.0.7", + } + + listener, err := Start(http.DefaultClient, config) + assert.Nil(t, err) + + if assert.Nil(t, hubMockServer.WaitUntilRegistered()) { + registerRequest := hubMockServer.GetRegisterRequest() + assert.NotEmpty(t, registerRequest.Arch) + assert.NotEmpty(t, registerRequest.Hostname) + assert.NotEmpty(t, registerRequest.Name) + assert.NotEmpty(t, registerRequest.OS) + assert.NotZero(t, registerRequest.PID) + assert.Equal(t, registerRequest.Version, "0.0.7") + } + + listener.Stop() + hubMockServer.Close() + loghubMockServer.Close() +} + +func Test__RegisterRequestIsRetried(t *testing.T) { + testsupport.SetupTestLogs() + + loghubMockServer := testsupport.NewLoghubMockServer() + loghubMockServer.Init() + + hubMockServer := testsupport.NewHubMockServer() + hubMockServer.Init() + hubMockServer.UseLogsURL(loghubMockServer.URL()) + hubMockServer.RejectRegisterAttempts(3) + + config := Config{ + ExitOnShutdown: false, + Endpoint: hubMockServer.Host(), + Token: "token", + RegisterRetryLimit: 5, + Scheme: "http", + EnvVars: []config.HostEnvVar{}, + FileInjections: []config.FileInjection{}, + AgentVersion: "0.0.7", + } + + listener, err := Start(http.DefaultClient, config) + assert.Nil(t, err) + + if assert.Nil(t, hubMockServer.WaitUntilRegistered()) { + assert.Equal(t, 3, hubMockServer.RegisterAttempts) + registerRequest := hubMockServer.GetRegisterRequest() + assert.NotEmpty(t, registerRequest.Arch) + assert.NotEmpty(t, registerRequest.Hostname) + assert.NotEmpty(t, registerRequest.Name) + assert.NotEmpty(t, registerRequest.OS) + assert.NotZero(t, registerRequest.PID) + assert.Equal(t, registerRequest.Version, "0.0.7") + } + + listener.Stop() + hubMockServer.Close() + loghubMockServer.Close() +} + +func Test__RegistrationFails(t *testing.T) { + testsupport.SetupTestLogs() + + loghubMockServer := testsupport.NewLoghubMockServer() + loghubMockServer.Init() + + hubMockServer := testsupport.NewHubMockServer() + hubMockServer.Init() + hubMockServer.UseLogsURL(loghubMockServer.URL()) + hubMockServer.RejectRegisterAttempts(10) + + config := Config{ + ExitOnShutdown: false, + Endpoint: hubMockServer.Host(), + Token: "token", + RegisterRetryLimit: 5, + Scheme: "http", + EnvVars: []config.HostEnvVar{}, + FileInjections: []config.FileInjection{}, + AgentVersion: "0.0.7", + } + + _, err := Start(http.DefaultClient, config) + assert.NotNil(t, err) + assert.Equal(t, 4, hubMockServer.RegisterAttempts) + + hubMockServer.Close() + loghubMockServer.Close() +} + +func Test__ShutdownHookIsExecuted(t *testing.T) { + testsupport.SetupTestLogs() + + loghubMockServer := testsupport.NewLoghubMockServer() + loghubMockServer.Init() + + hubMockServer := testsupport.NewHubMockServer() + hubMockServer.Init() + hubMockServer.UseLogsURL(loghubMockServer.URL()) + + hook, err := tempFileWithExtension() + assert.Nil(t, err) + + /* + * To assert that the shutdown hook was executed, + * we make it create a file with the same name + .done suffix. + * If that file exists after the listener stopped, + * it means the shutdown hook was executed. + */ + destination := fmt.Sprintf("%s.done", hook) + err = ioutil.WriteFile(hook, []byte(testsupport.CopyFile(hook, destination)), 0777) + assert.Nil(t, err) + + config := Config{ + ExitOnShutdown: false, + Endpoint: hubMockServer.Host(), + Token: "token", + RegisterRetryLimit: 5, + Scheme: "http", + EnvVars: []config.HostEnvVar{}, + FileInjections: []config.FileInjection{}, + AgentVersion: "0.0.7", + ShutdownHookPath: hook, + } + + listener, err := Start(http.DefaultClient, config) + assert.Nil(t, err) + + // listener has not been stopped yet, so file created by shutdown hook does not exist yet + assert.NoFileExists(t, destination) + + time.Sleep(time.Second) + listener.Stop() + + // listener has been stopped, so file created by shutdown hook should exist + assert.FileExists(t, destination) + + os.Remove(hook) + os.Remove(destination) + hubMockServer.Close() + loghubMockServer.Close() +} + +func Test__ShutdownHookCanSeeShutdownReason(t *testing.T) { + testsupport.SetupTestLogs() + + loghubMockServer := testsupport.NewLoghubMockServer() + loghubMockServer.Init() + + hubMockServer := testsupport.NewHubMockServer() + hubMockServer.Init() + hubMockServer.UseLogsURL(loghubMockServer.URL()) + + hook, err := tempFileWithExtension() + assert.Nil(t, err) + + /* + * To assert that the shutdown hook has access to the SEMAPHORE_AGENT_SHUTDOWN_REASON + * variable, we tell the shutdown hook script to write its value on a new file. + */ + destination := fmt.Sprintf("%s.done", hook) + err = ioutil.WriteFile(hook, []byte(testsupport.EchoEnvVarToFile("SEMAPHORE_AGENT_SHUTDOWN_REASON", destination)), 0777) + assert.Nil(t, err) + + config := Config{ + ExitOnShutdown: false, + Endpoint: hubMockServer.Host(), + Token: "token", + RegisterRetryLimit: 5, + Scheme: "http", + EnvVars: []config.HostEnvVar{}, + FileInjections: []config.FileInjection{}, + AgentVersion: "0.0.7", + ShutdownHookPath: hook, + } + + listener, err := Start(http.DefaultClient, config) + assert.Nil(t, err) + + // listener has not been stopped yet, so file created by shutdown hook does not exist yet + assert.NoFileExists(t, destination) + + time.Sleep(time.Second) + listener.Stop() + + // listener has been stopped, so file created by shutdown hook should exist + assert.FileExists(t, destination) + + bytes, err := ioutil.ReadFile(destination) + assert.Nil(t, err) + assert.Equal(t, ShutdownReasonRequested.String(), strings.Replace(string(bytes), "\r\n", "", -1)) + + os.Remove(hook) + os.Remove(destination) + hubMockServer.Close() + loghubMockServer.Close() +} + +func Test__ShutdownAfterJobFinished(t *testing.T) { + testsupport.SetupTestLogs() + + loghubMockServer := testsupport.NewLoghubMockServer() + loghubMockServer.Init() + + hubMockServer := testsupport.NewHubMockServer() + hubMockServer.Init() + hubMockServer.UseLogsURL(loghubMockServer.URL()) + + config := Config{ + ExitOnShutdown: false, + DisconnectAfterJob: true, + Endpoint: hubMockServer.Host(), + Token: "token", + RegisterRetryLimit: 5, + Scheme: "http", + EnvVars: []config.HostEnvVar{}, + FileInjections: []config.FileInjection{}, + AgentVersion: "0.0.7", + } + + listener, err := Start(http.DefaultClient, config) + assert.Nil(t, err) + + hubMockServer.AssignJob(&api.JobRequest{ + ID: "Test__ShutdownAfterJobFinished", + Commands: []api.Command{ + {Directive: testsupport.Output("hello world")}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + URL: loghubMockServer.URL(), + Token: "doesnotmatter", + }, + }) + + assert.Nil(t, hubMockServer.WaitUntilDisconnected(30, 2*time.Second)) + assert.Equal(t, listener.JobProcessor.ShutdownReason, ShutdownReasonJobFinished) + + hubMockServer.Close() + loghubMockServer.Close() +} + +func Test__ShutdownAfterIdleTimeout(t *testing.T) { + testsupport.SetupTestLogs() + + loghubMockServer := testsupport.NewLoghubMockServer() + loghubMockServer.Init() + + hubMockServer := testsupport.NewHubMockServer() + hubMockServer.Init() + hubMockServer.UseLogsURL(loghubMockServer.URL()) + + config := Config{ + ExitOnShutdown: false, + DisconnectAfterIdleTimeout: 15 * time.Second, + Endpoint: hubMockServer.Host(), + Token: "token", + RegisterRetryLimit: 5, + Scheme: "http", + EnvVars: []config.HostEnvVar{}, + FileInjections: []config.FileInjection{}, + AgentVersion: "0.0.7", + } + + listener, err := Start(http.DefaultClient, config) + assert.Nil(t, err) + assert.Nil(t, hubMockServer.WaitUntilDisconnected(15, 2*time.Second)) + assert.Equal(t, listener.JobProcessor.ShutdownReason, ShutdownReasonIdle) + + hubMockServer.Close() + loghubMockServer.Close() +} + +func Test__ShutdownFromUpstreamWhileWaiting(t *testing.T) { + testsupport.SetupTestLogs() + + loghubMockServer := testsupport.NewLoghubMockServer() + loghubMockServer.Init() + + hubMockServer := testsupport.NewHubMockServer() + hubMockServer.Init() + hubMockServer.UseLogsURL(loghubMockServer.URL()) + + config := Config{ + ExitOnShutdown: false, + Endpoint: hubMockServer.Host(), + Token: "token", + RegisterRetryLimit: 5, + Scheme: "http", + EnvVars: []config.HostEnvVar{}, + FileInjections: []config.FileInjection{}, + AgentVersion: "0.0.7", + } + + listener, err := Start(http.DefaultClient, config) + assert.Nil(t, err) + + time.Sleep(time.Second) + hubMockServer.ScheduleShutdown() + + assert.Nil(t, hubMockServer.WaitUntilDisconnected(5, 2*time.Second)) + assert.Equal(t, listener.JobProcessor.ShutdownReason, ShutdownReasonRequested) + + hubMockServer.Close() + loghubMockServer.Close() +} + +func Test__ShutdownFromUpstreamWhileRunningJob(t *testing.T) { + testsupport.SetupTestLogs() + + loghubMockServer := testsupport.NewLoghubMockServer() + loghubMockServer.Init() + + hubMockServer := testsupport.NewHubMockServer() + hubMockServer.Init() + hubMockServer.UseLogsURL(loghubMockServer.URL()) + + config := Config{ + ExitOnShutdown: false, + Endpoint: hubMockServer.Host(), + Token: "token", + RegisterRetryLimit: 5, + Scheme: "http", + EnvVars: []config.HostEnvVar{}, + FileInjections: []config.FileInjection{}, + AgentVersion: "0.0.7", + } + + listener, err := Start(http.DefaultClient, config) + assert.Nil(t, err) + + hubMockServer.AssignJob(&api.JobRequest{ + ID: "Test__ShutdownFromUpstreamWhileRunningJob", + Commands: []api.Command{ + {Directive: "sleep 300"}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + URL: loghubMockServer.URL(), + Token: "doesnotmatter", + }, + }) + + assert.Nil(t, hubMockServer.WaitUntilRunningJob(5, 2*time.Second)) + hubMockServer.ScheduleShutdown() + + assert.Nil(t, hubMockServer.WaitUntilDisconnected(10, 2*time.Second)) + assert.Equal(t, listener.JobProcessor.ShutdownReason, ShutdownReasonRequested) + + hubMockServer.Close() + loghubMockServer.Close() +} + +func Test__HostEnvVarsAreExposedToJob(t *testing.T) { + testsupport.SetupTestLogs() + + loghubMockServer := testsupport.NewLoghubMockServer() + loghubMockServer.Init() + + hubMockServer := testsupport.NewHubMockServer() + hubMockServer.Init() + hubMockServer.UseLogsURL(loghubMockServer.URL()) + + config := Config{ + ExitOnShutdown: false, + Endpoint: hubMockServer.Host(), + Token: "token", + RegisterRetryLimit: 5, + Scheme: "http", + EnvVars: []config.HostEnvVar{ + {Name: "IMPORTANT_HOST_VAR_A", Value: "IMPORTANT_HOST_VAR_A_VALUE"}, + {Name: "IMPORTANT_HOST_VAR_B", Value: "IMPORTANT_HOST_VAR_B_VALUE"}, + }, + FileInjections: []config.FileInjection{}, + AgentVersion: "0.0.7", + } + + listener, err := Start(http.DefaultClient, config) + assert.Nil(t, err) + + hubMockServer.AssignJob(&api.JobRequest{ + ID: "Test__HostEnvVarsAreExposedToJob", + Commands: []api.Command{ + {Directive: testsupport.Output("On regular commands")}, + {Directive: testsupport.EchoEnvVar("IMPORTANT_HOST_VAR_A")}, + {Directive: testsupport.EchoEnvVar("IMPORTANT_HOST_VAR_B")}, + {Directive: testsupport.EchoEnvVar("IMPORTANT_HOST_VAR_C")}, + }, + EpilogueAlwaysCommands: []api.Command{ + {Directive: testsupport.Output("On epilogue always")}, + {Directive: testsupport.EchoEnvVar("IMPORTANT_HOST_VAR_A")}, + {Directive: testsupport.EchoEnvVar("IMPORTANT_HOST_VAR_B")}, + {Directive: testsupport.EchoEnvVar("IMPORTANT_HOST_VAR_C")}, + }, + EpilogueOnPassCommands: []api.Command{ + {Directive: testsupport.Output("On epilogue on pass")}, + {Directive: testsupport.EchoEnvVar("IMPORTANT_HOST_VAR_A")}, + {Directive: testsupport.EchoEnvVar("IMPORTANT_HOST_VAR_B")}, + {Directive: testsupport.EchoEnvVar("IMPORTANT_HOST_VAR_C")}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + URL: loghubMockServer.URL(), + Token: "doesnotmatter", + }, + }) + + assert.Nil(t, hubMockServer.WaitUntilFinishedJob(12, 5*time.Second)) + + eventObjects, err := eventlogger.TransformToObjects(loghubMockServer.GetLogs()) + assert.Nil(t, err) + + simplifiedEvents, err := eventlogger.SimplifyLogEvents(eventObjects, true) + assert.Nil(t, err) + + assert.Equal(t, []string{ + "job_started", + + "directive: Exporting environment variables", + "Exporting IMPORTANT_HOST_VAR_A\n", + "Exporting IMPORTANT_HOST_VAR_B\n", + "Exit Code: 0", + + "directive: Injecting Files", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.Output("On regular commands")), + "On regular commands", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("IMPORTANT_HOST_VAR_A")), + "IMPORTANT_HOST_VAR_A_VALUE", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("IMPORTANT_HOST_VAR_B")), + "IMPORTANT_HOST_VAR_B_VALUE", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("IMPORTANT_HOST_VAR_C")), + "Exit Code: 0", + + "directive: Exporting environment variables", + "Exporting SEMAPHORE_JOB_RESULT\n", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.Output("On epilogue always")), + "On epilogue always", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("IMPORTANT_HOST_VAR_A")), + "IMPORTANT_HOST_VAR_A_VALUE", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("IMPORTANT_HOST_VAR_B")), + "IMPORTANT_HOST_VAR_B_VALUE", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("IMPORTANT_HOST_VAR_C")), + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.Output("On epilogue on pass")), + "On epilogue on pass", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("IMPORTANT_HOST_VAR_A")), + "IMPORTANT_HOST_VAR_A_VALUE", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("IMPORTANT_HOST_VAR_B")), + "IMPORTANT_HOST_VAR_B_VALUE", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.EchoEnvVar("IMPORTANT_HOST_VAR_C")), + "Exit Code: 0", + + "job_finished: passed", + }, simplifiedEvents) + + listener.Stop() + hubMockServer.Close() + loghubMockServer.Close() +} + +func Test__GetJobIsRetried(t *testing.T) { + testsupport.SetupTestLogs() + + loghubMockServer := testsupport.NewLoghubMockServer() + loghubMockServer.Init() + + hubMockServer := testsupport.NewHubMockServer() + hubMockServer.Init() + hubMockServer.UseLogsURL(loghubMockServer.URL()) + hubMockServer.RejectGetJobAttempts(5) + + config := Config{ + DisconnectAfterJob: true, + ExitOnShutdown: false, + Endpoint: hubMockServer.Host(), + Token: "token", + RegisterRetryLimit: 5, + GetJobRetryLimit: 10, + Scheme: "http", + EnvVars: []config.HostEnvVar{}, + FileInjections: []config.FileInjection{}, + AgentVersion: "0.0.7", + } + + listener, err := Start(http.DefaultClient, config) + assert.Nil(t, err) + + hubMockServer.AssignJob(&api.JobRequest{ + ID: "Test__GetJobIsRetried", + Commands: []api.Command{ + {Directive: testsupport.Output("hello")}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + URL: loghubMockServer.URL(), + Token: "doesnotmatter", + }, + }) + + assert.Nil(t, hubMockServer.WaitUntilDisconnected(10, 2*time.Second)) + assert.Equal(t, listener.JobProcessor.ShutdownReason, ShutdownReasonJobFinished) + assert.Equal(t, hubMockServer.GetJobAttempts, 5) + + hubMockServer.Close() + loghubMockServer.Close() +} + +func Test__ReportsFailedToFetchJob(t *testing.T) { + testsupport.SetupTestLogs() + + loghubMockServer := testsupport.NewLoghubMockServer() + loghubMockServer.Init() + + hubMockServer := testsupport.NewHubMockServer() + hubMockServer.Init() + hubMockServer.UseLogsURL(loghubMockServer.URL()) + hubMockServer.RejectGetJobAttempts(100) + + config := Config{ + DisconnectAfterJob: true, + ExitOnShutdown: false, + Endpoint: hubMockServer.Host(), + Token: "token", + RegisterRetryLimit: 5, + GetJobRetryLimit: 2, + Scheme: "http", + EnvVars: []config.HostEnvVar{}, + FileInjections: []config.FileInjection{}, + AgentVersion: "0.0.7", + } + + listener, err := Start(http.DefaultClient, config) + assert.Nil(t, err) + + hubMockServer.AssignJob(&api.JobRequest{ + ID: "Test__ReportsFailedToFetchJob", + Commands: []api.Command{}, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + URL: loghubMockServer.URL(), + Token: "doesnotmatter", + }, + }) + + assert.Nil(t, hubMockServer.WaitUntilFailure(string(selfhostedapi.AgentStateFailedToFetchJob), 12, 5*time.Second)) + + listener.Stop() + hubMockServer.Close() + loghubMockServer.Close() +} + +func Test__ReportsFailedToConstructJob(t *testing.T) { + testsupport.SetupTestLogs() + + loghubMockServer := testsupport.NewLoghubMockServer() + loghubMockServer.Init() + + hubMockServer := testsupport.NewHubMockServer() + hubMockServer.Init() + hubMockServer.UseLogsURL(loghubMockServer.URL()) + + config := Config{ + DisconnectAfterJob: true, + ExitOnShutdown: false, + Endpoint: hubMockServer.Host(), + Token: "token", + RegisterRetryLimit: 5, + GetJobRetryLimit: 2, + Scheme: "http", + EnvVars: []config.HostEnvVar{}, + FileInjections: []config.FileInjection{}, + AgentVersion: "0.0.7", + } + + listener, err := Start(http.DefaultClient, config) + assert.Nil(t, err) + + hubMockServer.AssignJob(&api.JobRequest{ + ID: "Test__ReportsFailedToConstructJob", + Executor: "doesnotexist", + Commands: []api.Command{}, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + URL: loghubMockServer.URL(), + Token: "doesnotmatter", + }, + }) + + assert.Nil(t, hubMockServer.WaitUntilFailure(string(selfhostedapi.AgentStateFailedToConstructJob), 10, 2*time.Second)) + + listener.Stop() + hubMockServer.Close() + loghubMockServer.Close() +} + +func Test__ReportsFailedToSendFinishedCallback(t *testing.T) { + testsupport.SetupTestLogs() + + loghubMockServer := testsupport.NewLoghubMockServer() + loghubMockServer.Init() + + hubMockServer := testsupport.NewHubMockServer() + hubMockServer.Init() + hubMockServer.UseLogsURL(loghubMockServer.URL()) + + config := Config{ + ExitOnShutdown: false, + Endpoint: hubMockServer.Host(), + Token: "token", + RegisterRetryLimit: 5, + GetJobRetryLimit: 2, + CallbackRetryLimit: 5, + Scheme: "http", + EnvVars: []config.HostEnvVar{}, + FileInjections: []config.FileInjection{}, + AgentVersion: "0.0.7", + } + + listener, err := Start(http.DefaultClient, config) + assert.Nil(t, err) + + hubMockServer.AssignJob(&api.JobRequest{ + ID: "Test__ReportsFailedToSendFinishedCallback", + Commands: []api.Command{}, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/500", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + URL: loghubMockServer.URL(), + Token: "doesnotmatter", + }, + }) + + assert.Nil(t, hubMockServer.WaitUntilFailure(string(selfhostedapi.AgentStateFailedToSendCallback), 10, 2*time.Second)) + + listener.Stop() + hubMockServer.Close() + loghubMockServer.Close() +} + +func Test__ReportsFailedToSendTeardownFinishedCallback(t *testing.T) { + testsupport.SetupTestLogs() + + loghubMockServer := testsupport.NewLoghubMockServer() + loghubMockServer.Init() + + hubMockServer := testsupport.NewHubMockServer() + hubMockServer.Init() + hubMockServer.UseLogsURL(loghubMockServer.URL()) + + config := Config{ + ExitOnShutdown: false, + Endpoint: hubMockServer.Host(), + Token: "token", + RegisterRetryLimit: 5, + GetJobRetryLimit: 2, + CallbackRetryLimit: 5, + Scheme: "http", + EnvVars: []config.HostEnvVar{}, + FileInjections: []config.FileInjection{}, + AgentVersion: "0.0.7", + } + + listener, err := Start(http.DefaultClient, config) + assert.Nil(t, err) + + hubMockServer.AssignJob(&api.JobRequest{ + ID: "Test__ReportsFailedToSendTeardownFinishedCallback", + Commands: []api.Command{}, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/500", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + URL: loghubMockServer.URL(), + Token: "doesnotmatter", + }, + }) + + assert.Nil(t, hubMockServer.WaitUntilFailure(string(selfhostedapi.AgentStateFailedToSendCallback), 10, 2*time.Second)) + + listener.Stop() + hubMockServer.Close() + loghubMockServer.Close() +} + +func tempFileWithExtension() (string, error) { + tmpFile, err := ioutil.TempFile("", fmt.Sprintf("file*.%s", extension())) + if err != nil { + return "", err + } + + tmpFile.Close() + return tmpFile.Name(), nil +} + +func extension() string { + if runtime.GOOS == "windows" { + return "ps1" + } + + return "sh" +} diff --git a/pkg/listener/selfhostedapi/logs.go b/pkg/listener/selfhostedapi/logs.go deleted file mode 100644 index 033a7df9..00000000 --- a/pkg/listener/selfhostedapi/logs.go +++ /dev/null @@ -1,31 +0,0 @@ -package selfhostedapi - -import ( - "bytes" - "fmt" - "net/http" -) - -func (a *API) LogsPath(jobID string) string { - return a.BasePath() + fmt.Sprintf("/jobs/%s/logs", jobID) -} - -func (a *API) Logs(jobID string, batch *bytes.Buffer) error { - r, err := http.NewRequest("POST", a.LogsPath(jobID), batch) - if err != nil { - return err - } - - a.authorize(r, a.AccessToken) - - resp, err := a.client.Do(r) - if err != nil { - return err - } - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("failed to submit logs, got HTTP %d", resp.StatusCode) - } - - return nil -} diff --git a/pkg/osinfo/osinfo_test.go b/pkg/osinfo/name_test.go similarity index 100% rename from pkg/osinfo/osinfo_test.go rename to pkg/osinfo/name_test.go diff --git a/pkg/shell/env.go b/pkg/shell/env.go index 693f7a02..a96462b9 100644 --- a/pkg/shell/env.go +++ b/pkg/shell/env.go @@ -35,7 +35,7 @@ func CreateEnvironment(envVars []api.EnvVar, HostEnvVars []config.HostEnvVar) (* /* * Create an environment by reading a file created with - * an environment dump in Windows with the 'SET > ' command. + * an environment dump in Windows. */ func CreateEnvironmentFromFile(fileName string) (*Environment, error) { // #nosec @@ -61,10 +61,6 @@ func CreateEnvironmentFromFile(fileName string) (*Environment, error) { return &environment, nil } -func (e *Environment) IsEmpty() bool { - return e.env != nil || len(e.env) == 0 -} - func (e *Environment) Set(name, value string) { if e.env == nil { e.env = map[string]string{} @@ -119,7 +115,9 @@ func (e *Environment) ToFile(fileName string, callback func(name string)) error for _, name := range e.Keys() { value, _ := e.Get(name) fileContent += fmt.Sprintf("export %s=%s\n", name, shellQuote(value)) - callback(name) + if callback != nil { + callback(name) + } } // #nosec diff --git a/pkg/shell/env_test.go b/pkg/shell/env_test.go new file mode 100644 index 00000000..4bc402f2 --- /dev/null +++ b/pkg/shell/env_test.go @@ -0,0 +1,172 @@ +package shell + +import ( + "encoding/base64" + "io/ioutil" + "os" + "runtime" + "testing" + + "github.com/semaphoreci/agent/pkg/api" + "github.com/semaphoreci/agent/pkg/config" + "github.com/stretchr/testify/assert" +) + +func Test__CreateEnvironment(t *testing.T) { + t.Run("vars from job request are base64 decoded", func(t *testing.T) { + varsFromRequest := []api.EnvVar{ + {Name: "A", Value: base64.StdEncoding.EncodeToString([]byte("AAA"))}, + {Name: "B", Value: base64.StdEncoding.EncodeToString([]byte("BBB"))}, + } + + env, err := CreateEnvironment(varsFromRequest, []config.HostEnvVar{}) + assert.Nil(t, err) + assert.NotNil(t, env) + + assertValueExists(t, env, "A", "AAA") + assertValueExists(t, env, "B", "BBB") + }) + + t.Run("vars from host are not base64 decoded", func(t *testing.T) { + varsFromHost := []config.HostEnvVar{ + {Name: "A", Value: "AAA"}, + {Name: "B", Value: "BBB"}, + } + + env, err := CreateEnvironment([]api.EnvVar{}, varsFromHost) + assert.Nil(t, err) + assert.NotNil(t, env) + assertValueExists(t, env, "A", "AAA") + assertValueExists(t, env, "B", "BBB") + }) + + t.Run("var from job request not properly encoded => error", func(t *testing.T) { + varsFromRequest := []api.EnvVar{ + {Name: "A", Value: "AAA"}, + } + + env, err := CreateEnvironment(varsFromRequest, []config.HostEnvVar{}) + assert.NotNil(t, err) + assert.Nil(t, env) + }) + + t.Run("var is overwritten by subsequent var in request", func(t *testing.T) { + varsFromRequest := []api.EnvVar{ + {Name: "FOO", Value: base64.StdEncoding.EncodeToString([]byte("FOO"))}, + {Name: "FOO", Value: base64.StdEncoding.EncodeToString([]byte("BAR"))}, + } + + env, err := CreateEnvironment(varsFromRequest, []config.HostEnvVar{}) + assert.Nil(t, err) + assertValueExists(t, env, "FOO", "BAR") + }) + + t.Run("var is overwritten by subsequent host var", func(t *testing.T) { + varsFromRequest := []api.EnvVar{ + {Name: "FOO", Value: base64.StdEncoding.EncodeToString([]byte("FOO"))}, + {Name: "FOO", Value: base64.StdEncoding.EncodeToString([]byte("BAR"))}, + } + + varsFromHost := []config.HostEnvVar{ + {Name: "FOO", Value: "AAA"}, + } + + env, err := CreateEnvironment(varsFromRequest, varsFromHost) + assert.Nil(t, err) + assertValueExists(t, env, "FOO", "AAA") + }) +} + +func Test__CreateEnvironmentFromFile(t *testing.T) { + file, err := ioutil.TempFile("", "environment-dump") + assert.Nil(t, err) + + content := ` +VAR_A=AAA +VAR_B=BBB +VAR_C=CCC + ` + + _ = ioutil.WriteFile(file.Name(), []byte(content), 0644) + env, err := CreateEnvironmentFromFile(file.Name()) + assert.Nil(t, err) + assert.NotNil(t, env) + + assert.Equal(t, env.Keys(), []string{"VAR_A", "VAR_B", "VAR_C"}) + assertValueExists(t, env, "VAR_A", "AAA") + assertValueExists(t, env, "VAR_B", "BBB") + assertValueExists(t, env, "VAR_C", "CCC") + + file.Close() + os.Remove(file.Name()) +} + +func Test__EnvironmentToFile(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Environment.ToFile() is only used in non-windows") + } + + vars := []api.EnvVar{ + {Name: "Z", Value: base64.StdEncoding.EncodeToString([]byte("ZZZ"))}, + {Name: "O", Value: base64.StdEncoding.EncodeToString([]byte("OOO"))}, + {Name: "QUOTED", Value: base64.StdEncoding.EncodeToString([]byte("This is going to get quoted"))}, + } + + env, err := CreateEnvironment(vars, []config.HostEnvVar{}) + assert.Nil(t, err) + assert.NotNil(t, env) + + file, err := ioutil.TempFile("", ".env") + assert.Nil(t, err) + + err = env.ToFile(file.Name(), nil) + assert.Nil(t, err) + + content, err := ioutil.ReadFile(file.Name()) + assert.Nil(t, err) + assert.Equal(t, string(content), "export O=OOO\nexport QUOTED='This is going to get quoted'\nexport Z=ZZZ\n") + + file.Close() + os.Remove(file.Name()) +} + +func Test__EnvironmentToSlice(t *testing.T) { + varsFromRequest := []api.EnvVar{ + {Name: "A", Value: base64.StdEncoding.EncodeToString([]byte("AAA"))}, + {Name: "B", Value: base64.StdEncoding.EncodeToString([]byte("BBB"))}, + {Name: "C", Value: base64.StdEncoding.EncodeToString([]byte("CCC"))}, + } + + env, err := CreateEnvironment(varsFromRequest, []config.HostEnvVar{}) + assert.Nil(t, err) + assert.Contains(t, env.ToSlice(), "A=AAA") + assert.Contains(t, env.ToSlice(), "B=BBB") + assert.Contains(t, env.ToSlice(), "C=CCC") +} + +func Test__EnvironmentAppend(t *testing.T) { + vars := []api.EnvVar{ + {Name: "C", Value: base64.StdEncoding.EncodeToString([]byte("CCC"))}, + {Name: "D", Value: base64.StdEncoding.EncodeToString([]byte("DDD"))}, + {Name: "A", Value: base64.StdEncoding.EncodeToString([]byte("AAA"))}, + } + + other, _ := CreateEnvironment(vars, []config.HostEnvVar{}) + appended := []string{} + + first := Environment{} + first.Append(other, func(name, value string) { + appended = append(appended, name) + }) + + assert.Equal(t, appended, []string{"A", "C", "D"}) + assertValueExists(t, &first, "A", "AAA") + assertValueExists(t, &first, "C", "CCC") + assertValueExists(t, &first, "D", "DDD") +} + +func assertValueExists(t *testing.T, env *Environment, key, expectedValue string) { + value, ok := env.Get(key) + assert.True(t, ok) + assert.Equal(t, value, expectedValue) +} diff --git a/pkg/shell/process.go b/pkg/shell/process.go index 17839094..9cffb803 100644 --- a/pkg/shell/process.go +++ b/pkg/shell/process.go @@ -4,7 +4,6 @@ import ( "flag" "fmt" "io" - "io/ioutil" "math/rand" "os" "os/exec" @@ -313,12 +312,32 @@ func (p *Process) loadCommand() error { func (p *Process) writeCommandToFile(cmdFilePath, command string) error { // #nosec - err := ioutil.WriteFile(cmdFilePath, []byte(command), 0644) + file, err := os.OpenFile(cmdFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return err } - return nil + /* + * UTF8 files without a BOM containing non-ASCII characters may break in Windows PowerShell, + * since it misinterprets it as being encoded in the legacy "ANSI" codepage. + * Since we need to support non-ASCII characters, we need a UTF-8 file with a BOM. + * See: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_character_encoding + */ + if runtime.GOOS == "windows" { + _, err = file.Write([]byte{0xEF, 0xBB, 0xBF}) + if err != nil { + _ = file.Close() + return err + } + } + + _, err = file.Write([]byte(command)) + if err != nil { + _ = file.Close() + return err + } + + return file.Close() } func (p *Process) readBufferSize() int { diff --git a/pkg/shell/pty_test.go b/pkg/shell/pty_test.go new file mode 100644 index 00000000..663682c4 --- /dev/null +++ b/pkg/shell/pty_test.go @@ -0,0 +1,28 @@ +package shell + +import ( + "os/exec" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test__PTYIsNotSupportedOnWindows(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip() + } + + _, err := StartPTY(exec.Command("powershell")) + assert.NotNil(t, err) +} + +func Test__PTYIsSupportedOnNonWindows(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip() + } + + tty, err := StartPTY(exec.Command("bash", "--login")) + assert.Nil(t, err) + tty.Close() +} diff --git a/pkg/shell/shell_test.go b/pkg/shell/shell_test.go index 91d612cf..094955d6 100644 --- a/pkg/shell/shell_test.go +++ b/pkg/shell/shell_test.go @@ -2,17 +2,52 @@ package shell import ( "bytes" - "io/ioutil" - "log" + "os" + "runtime" "testing" assert "github.com/stretchr/testify/assert" ) +func Test__Shell__NewShell(t *testing.T) { + shell, err := NewShell(os.TempDir()) + assert.Nil(t, err) + assert.NotNil(t, shell.Cwd) + + if runtime.GOOS == "windows" { + assert.Equal(t, shell.Executable, "powershell") + } else { + assert.Equal(t, shell.Executable, "bash") + } + + if runtime.GOOS == "windows" { + assert.Equal(t, shell.Args, []string{"-NoProfile", "-NonInteractive"}) + } else { + assert.Equal(t, shell.Args, []string{"--login"}) + } +} + +func Test__Shell__Start(t *testing.T) { + shell, err := NewShell(os.TempDir()) + assert.Nil(t, err) + + err = shell.Start() + assert.Nil(t, err) + + if runtime.GOOS == "windows" { + assert.Nil(t, shell.BootCommand) + assert.Nil(t, shell.TTY) + } else { + assert.NotNil(t, shell.BootCommand) + assert.NotNil(t, shell.TTY) + } +} + func Test__Shell__SimpleHelloWorld(t *testing.T) { var output bytes.Buffer - shell := bashShell() + shell, _ := NewShell(os.TempDir()) + shell.Start() p1 := shell.NewProcess("echo Hello") p1.OnStdout(func(line string) { @@ -26,9 +61,22 @@ func Test__Shell__SimpleHelloWorld(t *testing.T) { func Test__Shell__HandlingBashProcessKill(t *testing.T) { var output bytes.Buffer - shell := bashShell() + shell, _ := NewShell(os.TempDir()) + shell.Start() + + var cmd string + if runtime.GOOS == "windows" { + cmd = ` + echo Hello + if ($?) { + Exit 1 + } + ` + } else { + cmd = "echo Hello && exit 1" + } - p1 := shell.NewProcess("echo 'Hello' && exit 1") + p1 := shell.NewProcess(cmd) p1.OnStdout(func(line string) { output.WriteString(line) }) @@ -38,6 +86,10 @@ func Test__Shell__HandlingBashProcessKill(t *testing.T) { } func Test__Shell__HandlingBashProcessKillThatHasBackgroundJobs(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip() + } + var output bytes.Buffer // @@ -51,7 +103,8 @@ func Test__Shell__HandlingBashProcessKillThatHasBackgroundJobs(t *testing.T) { // it stops the read procedure. // - shell := bashShell() + shell, _ := NewShell(os.TempDir()) + shell.Start() p1 := shell.NewProcess("sleep infinity &") p1.OnStdout(func(line string) { @@ -59,7 +112,7 @@ func Test__Shell__HandlingBashProcessKillThatHasBackgroundJobs(t *testing.T) { }) p1.Run() - p2 := shell.NewProcess("echo 'Hello' && exit 1") + p2 := shell.NewProcess("echo 'Hello' && sleep 1 && exit 1") p2.OnStdout(func(line string) { output.WriteString(line) }) @@ -67,20 +120,3 @@ func Test__Shell__HandlingBashProcessKillThatHasBackgroundJobs(t *testing.T) { assert.Equal(t, output.String(), "Hello\n") } - -func tempStorageFolder() string { - dir, err := ioutil.TempDir("", "agent-test") - if err != nil { - log.Fatal(err) - } - - return dir -} - -func bashShell() *Shell { - dir := tempStorageFolder() - shell, _ := NewShell(dir) - shell.Start() - - return shell -} diff --git a/test/e2e/shell/ssh_jump_points.rb b/test/e2e/hosted/ssh_jump_points.rb similarity index 100% rename from test/e2e/shell/ssh_jump_points.rb rename to test/e2e/hosted/ssh_jump_points.rb diff --git a/test/e2e/self-hosted/broken_finished_callback.rb b/test/e2e/self-hosted/broken_finished_callback.rb deleted file mode 100644 index 8d3b4081..00000000 --- a/test/e2e/self-hosted/broken_finished_callback.rb +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -require_relative '../../e2e' - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - - "env_vars": [], - - "files": [], - - "commands": [ - { "directive": "echo 'hello'" } - ], - - "epilogue_always_commands": [], - - "callbacks": { - "finished": "#{bad_callback_url}", - "teardown_finished": "#{bad_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_job_to_get_stuck diff --git a/test/e2e/self-hosted/broken_get_job.rb b/test/e2e/self-hosted/broken_get_job.rb deleted file mode 100644 index 5de7b711..00000000 --- a/test/e2e/self-hosted/broken_get_job.rb +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -require_relative '../../e2e' - -$JOB_ID = "bad-job-id" - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - - "env_vars": [], - - "files": [], - - "commands": [ - { "directive": "echo 'hello'" } - ], - - "epilogue_always_commands": [], - - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_job_to_get_stuck diff --git a/test/e2e/self-hosted/broken_teardown_callback.rb b/test/e2e/self-hosted/broken_teardown_callback.rb deleted file mode 100644 index 25893778..00000000 --- a/test/e2e/self-hosted/broken_teardown_callback.rb +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -require_relative '../../e2e' - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - - "env_vars": [], - - "files": [], - - "commands": [ - { "directive": "echo 'hello'" } - ], - - "epilogue_always_commands": [], - - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{bad_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_job_to_get_stuck diff --git a/test/e2e/self-hosted/no_ssh_jump_points.rb b/test/e2e/self-hosted/no_ssh_jump_points.rb deleted file mode 100644 index d19ae123..00000000 --- a/test/e2e/self-hosted/no_ssh_jump_points.rb +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -require_relative '../../e2e' - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - - "ssh_public_keys": [], - - "env_vars": [], - - "files": [], - - "commands": [ - { "directive": "ls -1q ~/.ssh | wc -l" } - ], - - "epilogue_always_commands": [], - - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_job_to_finish - -assert_job_log <<-LOG - {"event":"job_started", "timestamp":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"ls -1q ~/.ssh | wc -l"} - {"event":"cmd_output", "timestamp":"*", "output":"0\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"ls -1q ~/.ssh | wc -l","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} - - {"event":"job_finished", "timestamp":"*", "result":"passed"} -LOG \ No newline at end of file diff --git a/test/e2e/self-hosted/shell_host_env_vars.rb b/test/e2e/self-hosted/shell_host_env_vars.rb deleted file mode 100644 index ced10156..00000000 --- a/test/e2e/self-hosted/shell_host_env_vars.rb +++ /dev/null @@ -1,83 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -$AGENT_CONFIG = { - "endpoint" => "hub:4567", - "token" => "321h1l2jkh1jk42341", - "no-https" => true, - "shutdown-hook-path" => "", - "disconnect-after-job" => false, - "env-vars" => [ - "A=hello", - "B=how are you?", - "C=quotes ' quotes", - "D=$PATH:/etc/a" - ], - "files" => [], - "fail-on-missing-files" => false -} - -require_relative '../../e2e' - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - - "env_vars": [], - - "files": [], - - "commands": [ - { "directive": "echo $A" }, - { "directive": "echo $B" }, - { "directive": "echo $C" }, - { "directive": "echo $D" } - ], - - "epilogue_always_commands": [], - - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_job_to_finish - -assert_job_log <<-LOG - {"event":"job_started", "timestamp":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting A\\n"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting B\\n"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting C\\n"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting D\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"echo $A"} - {"event":"cmd_output", "timestamp":"*", "output":"hello\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo $A","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"echo $B"} - {"event":"cmd_output", "timestamp":"*", "output":"how are you?\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo $B","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"echo $C"} - {"event":"cmd_output", "timestamp":"*", "output":"quotes ' quotes\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo $C","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"echo $D"} - {"event":"cmd_output", "timestamp":"*", "output":"$PATH:/etc/a\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo $D","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} - - {"event":"job_finished", "timestamp":"*", "result":"passed"} -LOG diff --git a/test/e2e/self-hosted/shutdown.rb b/test/e2e/self-hosted/shutdown.rb deleted file mode 100644 index d1fc0037..00000000 --- a/test/e2e/self-hosted/shutdown.rb +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -require_relative '../../e2e' - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - - "env_vars": [], - - "files": [], - - "commands": [ - { "directive": "sleep infinity" } - ], - - "epilogue_always_commands": [], - - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_command_to_start("sleep infinity") - -shutdown_agent - -wait_for_agent_to_shutdown - -assert_job_log <<-LOG - {"event":"job_started", "timestamp":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"sleep infinity"} - {"event":"cmd_finished", "timestamp":"*", "directive":"sleep infinity","exit_code":1,"finished_at":"*","started_at":"*"} - - {"event":"job_finished", "timestamp":"*", "result":"stopped"} -LOG diff --git a/test/e2e/self-hosted/shutdown_idle.rb b/test/e2e/self-hosted/shutdown_idle.rb deleted file mode 100644 index bcfafbad..00000000 --- a/test/e2e/self-hosted/shutdown_idle.rb +++ /dev/null @@ -1,34 +0,0 @@ -$AGENT_CONFIG = { - "endpoint" => "hub:4567", - "token" => "321h1l2jkh1jk42341", - "no-https" => true, - "shutdown-hook-path" => "", - "disconnect-after-job" => false, - "disconnect-after-idle-timeout" => 30, - "env-vars" => [], - "files" => [], - "fail-on-missing-files" => false -} - -require_relative '../../e2e' - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - "env_vars": [], - "files": [], - "commands": [ - { "directive": "sleep 5" } - ], - - "epilogue_always_commands": [], - - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_agent_to_shutdown diff --git a/test/e2e/self-hosted/shutdown_while_waiting.rb b/test/e2e/self-hosted/shutdown_while_waiting.rb deleted file mode 100644 index 5d996e3a..00000000 --- a/test/e2e/self-hosted/shutdown_while_waiting.rb +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -require_relative '../../e2e' - -shutdown_agent - -wait_for_agent_to_shutdown diff --git a/test/e2e/shell/broken_unicode.rb b/test/e2e/shell/broken_unicode.rb deleted file mode 100644 index e02aa33a..00000000 --- a/test/e2e/shell/broken_unicode.rb +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/ruby - -Encoding.default_external = Encoding::UTF_8 - -# rubocop:disable all - -require_relative '../../e2e' - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - - "env_vars": [], - "files": [], - - "commands": [ - { "directive": "echo | awk '{ printf(\\\"%c%c%c%c%c\\\", 150, 150, 150, 150, 150) }'"} - ], - - "epilogue_always_commands": [], - - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_job_to_finish - -assert_job_log <<-LOG - {"event":"job_started", "timestamp":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive": "echo | awk '{ printf(\\\"%c%c%c%c%c\\\", 150, 150, 150, 150, 150) }'"} - {"event":"cmd_output", "timestamp":"*", "output":"\ufffd\ufffd\ufffd\ufffd\ufffd"} - {"event":"cmd_finished", "timestamp":"*", "directive": "echo | awk '{ printf(\\\"%c%c%c%c%c\\\", 150, 150, 150, 150, 150) }'","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} - - {"event":"job_finished", "timestamp":"*", "result":"passed"} -LOG diff --git a/test/e2e/shell/command_aliases.rb b/test/e2e/shell/command_aliases.rb deleted file mode 100644 index 1c6d9187..00000000 --- a/test/e2e/shell/command_aliases.rb +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -require_relative '../../e2e' - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - - "env_vars": [], - - "files": [], - - "commands": [ - { "directive": "echo Hello World", "alias": "Display Hello World" } - ], - - "epilogue_always_commands": [], - - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_job_to_finish - -assert_job_log <<-LOG - {"event":"job_started", "timestamp":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Display Hello World"} - {"event":"cmd_output", "timestamp":"*", "output":"Running: echo Hello World\\n"} - {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Display Hello World","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} - {"event":"job_finished", "timestamp":"*", "result":"passed"} -LOG diff --git a/test/e2e/shell/env_vars.rb b/test/e2e/shell/env_vars.rb deleted file mode 100644 index cca49122..00000000 --- a/test/e2e/shell/env_vars.rb +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -require_relative '../../e2e' - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - - "env_vars": [ - { "name": "A", "value": "#{`echo "hello" | base64`.strip}" }, - { "name": "B", "value": "#{`echo "how are you?" | base64`.strip}" }, - { "name": "C", "value": "#{`echo "quotes ' quotes" | base64`.strip}" }, - { "name": "D", "value": "#{`echo '$PATH:/etc/a' | base64`.strip}" } - ], - - "files": [], - - "commands": [ - { "directive": "echo $A" }, - { "directive": "echo $B" }, - { "directive": "echo $C" }, - { "directive": "echo $D" } - ], - - "epilogue_always_commands": [], - - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_job_to_finish - -assert_job_log <<-LOG - {"event":"job_started", "timestamp":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting A\\n"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting B\\n"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting C\\n"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting D\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"echo $A"} - {"event":"cmd_output", "timestamp":"*", "output":"hello\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo $A","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"echo $B"} - {"event":"cmd_output", "timestamp":"*", "output":"how are you?\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo $B","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"echo $C"} - {"event":"cmd_output", "timestamp":"*", "output":"quotes ' quotes\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo $C","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"echo $D"} - {"event":"cmd_output", "timestamp":"*", "output":"$PATH:/etc/a\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo $D","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} - {"event":"job_finished", "timestamp":"*", "result":"passed"} -LOG diff --git a/test/e2e/shell/epilogue_on_fail.rb b/test/e2e/shell/epilogue_on_fail.rb deleted file mode 100644 index 021b68a3..00000000 --- a/test/e2e/shell/epilogue_on_fail.rb +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -require_relative '../../e2e' - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - - "env_vars": [], - - "files": [], - - "commands": [ - { "directive": "false" } - ], - - "epilogue_always_commands": [ - { "directive": "echo Hello Epilogue" } - ], - - "epilogue_on_pass_commands": [ - { "directive": "echo Hello On Pass Epilogue" } - ], - - "epilogue_on_fail_commands": [ - { "directive": "echo Hello On Fail Epilogue" } - ], - - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_job_to_finish - -assert_job_log <<-LOG - {"event":"job_started", "timestamp":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"false"} - {"event":"cmd_finished", "timestamp":"*", "directive":"false","exit_code":1,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello Epilogue"} - {"event":"cmd_output", "timestamp":"*", "output":"Hello Epilogue\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello Epilogue","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello On Fail Epilogue"} - {"event":"cmd_output", "timestamp":"*", "output":"Hello On Fail Epilogue\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello On Fail Epilogue","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"job_finished", "timestamp":"*", "result":"failed"} -LOG diff --git a/test/e2e/shell/epilogue_on_pass.rb b/test/e2e/shell/epilogue_on_pass.rb deleted file mode 100644 index a9d0e940..00000000 --- a/test/e2e/shell/epilogue_on_pass.rb +++ /dev/null @@ -1,64 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -require_relative '../../e2e' - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - - "env_vars": [], - - "files": [], - - "commands": [ - { "directive": "echo Hello World" } - ], - - "epilogue_always_commands": [ - { "directive": "echo Hello Epilogue" } - ], - - "epilogue_on_pass_commands": [ - { "directive": "echo Hello On Pass Epilogue" } - ], - - "epilogue_on_fail_commands": [ - { "directive": "echo Hello On Fail Epilogue" } - ], - - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_job_to_finish - -assert_job_log <<-LOG - {"event":"job_started", "timestamp":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello World"} - {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello World","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello Epilogue"} - {"event":"cmd_output", "timestamp":"*", "output":"Hello Epilogue\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello Epilogue","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello On Pass Epilogue"} - {"event":"cmd_output", "timestamp":"*", "output":"Hello On Pass Epilogue\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello On Pass Epilogue","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"job_finished", "timestamp":"*", "result":"passed"} -LOG diff --git a/test/e2e/shell/failed_job.rb b/test/e2e/shell/failed_job.rb deleted file mode 100644 index ae684497..00000000 --- a/test/e2e/shell/failed_job.rb +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -require_relative '../../e2e' - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - - "env_vars": [], - - "files": [], - - "commands": [ - { "directive": "false" } - ], - - "epilogue_always_commands": [], - - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_job_to_finish - -assert_job_log <<-LOG - {"event":"job_started", "timestamp":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"false"} - {"event":"cmd_finished", "timestamp":"*", "directive":"false","exit_code":1,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} - {"event":"job_finished", "timestamp":"*", "result":"failed"} -LOG diff --git a/test/e2e/shell/file_injection.rb b/test/e2e/shell/file_injection.rb deleted file mode 100644 index d4d1d9f8..00000000 --- a/test/e2e/shell/file_injection.rb +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -require_relative '../../e2e' - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - - "env_vars": [], - - "files": [ - { "path": "test.txt", "content": "#{`echo "hello" | base64`.strip}", "mode": "0644" }, - { "path": "/a/b/c", "content": "#{`echo "hello" | base64`.strip}", "mode": "0644" }, - { "path": "/tmp/a", "content": "#{`echo "hello" | base64`.strip}", "mode": "0600" } - ], - - "commands": [ - { "directive": "cat test.txt" }, - { "directive": "cat /a/b/c" }, - { "directive": "stat -c '%a' /tmp/a" } - ], - - "epilogue_always_commands": [], - - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_job_to_finish - -assert_job_log <<-LOG - {"event":"job_started", "timestamp":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} - {"event":"cmd_output", "timestamp":"*", "output":"Injecting test.txt with file mode 0644\\n"} - {"event":"cmd_output", "timestamp":"*", "output":"Injecting /a/b/c with file mode 0644\\n"} - {"event":"cmd_output", "timestamp":"*", "output":"Injecting /tmp/a with file mode 0600\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"cat test.txt"} - {"event":"cmd_output", "timestamp":"*", "output":"hello\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"cat test.txt","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"cat /a/b/c"} - {"event":"cmd_output", "timestamp":"*", "output":"hello\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"cat /a/b/c","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"stat -c '%a' /tmp/a"} - {"event":"cmd_output", "timestamp":"*", "output":"600\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"stat -c '%a' /tmp/a","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} - - {"event":"job_finished", "timestamp":"*", "result":"passed"} -LOG diff --git a/test/e2e/shell/file_injection_broken_file_mode.rb b/test/e2e/shell/file_injection_broken_file_mode.rb deleted file mode 100644 index 58e2aa13..00000000 --- a/test/e2e/shell/file_injection_broken_file_mode.rb +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -require_relative '../../e2e' - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - - "env_vars": [], - - "files": [ - { "path": "test.txt", "content": "#{`echo "hello" | base64`.strip}", "mode": "obviously broken" } - ], - - "commands": [ - { "directive": "cat test.txt" }, - { "directive": "cat /a/b/c" }, - { "directive": "stat -c '%a' /tmp/a" } - ], - - "epilogue_always_commands": [], - - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_job_to_finish - -assert_job_log <<-LOG - {"event":"job_started", "timestamp":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} - {"event":"cmd_output", "timestamp":"*", "output":"Injecting test.txt with file mode obviously broken\\n"} - {"event":"cmd_output", "timestamp":"*", "output":"bad file permission 'obviously broken'\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":1,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} - {"event":"job_finished", "timestamp":"*", "result":"failed"} -LOG diff --git a/test/e2e/shell/hello_world.rb b/test/e2e/shell/hello_world.rb deleted file mode 100644 index aec80e80..00000000 --- a/test/e2e/shell/hello_world.rb +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -require_relative '../../e2e' - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - - "env_vars": [], - - "files": [], - - "commands": [ - { "directive": "echo Hello World" } - ], - - "epilogue_always_commands": [], - - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_job_to_finish - -assert_job_log <<-LOG - {"event":"job_started", "timestamp":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello World"} - {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello World","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} - {"event":"job_finished", "timestamp":"*", "result":"passed"} -LOG diff --git a/test/e2e/shell/job_stopping.rb b/test/e2e/shell/job_stopping.rb deleted file mode 100644 index 74a69153..00000000 --- a/test/e2e/shell/job_stopping.rb +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -require_relative '../../e2e' - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - - "env_vars": [], - - "files": [], - - "commands": [ - { "directive": "sleep infinity" }, - { "directive": "echo 'here'" } - ], - - "epilogue_always_commands": [], - - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_command_to_start("sleep infinity") - -sleep 1 - -stop_job - -wait_for_job_to_finish - -assert_job_log <<-LOG - {"event":"job_started", "timestamp":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"sleep infinity"} - {"event":"cmd_finished", "timestamp":"*", "directive":"sleep infinity","exit_code":1,"finished_at":"*","started_at":"*"} - {"event":"job_finished", "timestamp":"*", "result":"stopped"} -LOG diff --git a/test/e2e/shell/job_stopping_on_epilogue.rb b/test/e2e/shell/job_stopping_on_epilogue.rb deleted file mode 100644 index 33f82eb2..00000000 --- a/test/e2e/shell/job_stopping_on_epilogue.rb +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -require_relative '../../e2e' - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - - "env_vars": [], - - "files": [], - - "commands": [ - { "directive": "echo 'here'" } - ], - - "epilogue_always_commands": [ - { "directive": "sleep infinity" } - ], - - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_command_to_start("sleep infinity") - -sleep 1 - -stop_job - -wait_for_job_to_finish - -assert_job_log <<-LOG - {"event":"job_started", "timestamp":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"echo 'here'"} - {"event":"cmd_output", "timestamp":"*", "output":"here\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo 'here'","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"sleep infinity"} - {"event":"cmd_finished", "timestamp":"*", "directive":"sleep infinity","exit_code":1,"finished_at":"*","started_at":"*"} - {"event":"job_finished", "timestamp":"*", "result":"stopped"} -LOG diff --git a/test/e2e/shell/killing_root_bash.rb b/test/e2e/shell/killing_root_bash.rb deleted file mode 100644 index e83238c5..00000000 --- a/test/e2e/shell/killing_root_bash.rb +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -require_relative '../../e2e' - -# -# Running the following set of commands caused the Agent to freeze up. -# -# sleep infinity & -# exit 1 -# -# These are regressions tests that verify that this is no longer a problem. -# - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - - "env_vars": [], - - "files": [], - - "commands": [ - { "directive": "sleep infinity &" }, - { "directive": "exit 1" } - ], - - "epilogue_always_commands": [], - - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_job_to_finish - -assert_job_log <<-LOG - {"event":"job_started", "timestamp":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"sleep infinity &"} - {"event":"cmd_finished", "timestamp":"*", "directive":"sleep infinity &","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"exit 1"} - {"event":"cmd_finished", "timestamp":"*", "directive":"exit 1","exit_code":1,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":1,"started_at":"*","finished_at":"*"} - {"event":"job_finished", "timestamp":"*", "result":"failed"} -LOG diff --git a/test/e2e/shell/set_e.rb b/test/e2e/shell/set_e.rb deleted file mode 100644 index 9a086870..00000000 --- a/test/e2e/shell/set_e.rb +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -require_relative '../../e2e' - -# -# Running the following set of commands caused the Agent to freeze up. -# -# sleep infinity & -# set -e -# false -# -# These are regressions tests that verify that this is no longer a problem. -# - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - - "env_vars": [], - - "files": [], - - "commands": [ - { "directive": "sleep infinity &" }, - { "directive": "set -e" }, - { "directive": "false" } - ], - - "epilogue_always_commands": [], - - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_job_to_finish - -assert_job_log <<-LOG - {"event":"job_started", "timestamp":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"sleep infinity &"} - {"event":"cmd_finished", "timestamp":"*", "directive":"sleep infinity &","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"set -e"} - {"event":"cmd_finished", "timestamp":"*", "directive":"set -e","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"false"} - {"event":"cmd_finished", "timestamp":"*", "directive":"false","exit_code":1,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":1,"started_at":"*","finished_at":"*"} - {"event":"job_finished", "timestamp":"*", "result":"failed"} -LOG diff --git a/test/e2e/shell/set_pipefail.rb b/test/e2e/shell/set_pipefail.rb deleted file mode 100644 index a6cc9e99..00000000 --- a/test/e2e/shell/set_pipefail.rb +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -require_relative '../../e2e' - -# -# Running the following set of commands caused the Agent to freeze up. -# -# sleep infinity & -# set -eo pipefail -# cat non_existant | sort -# -# These are regressions tests that verify that this is no longer a problem. -# - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - - "env_vars": [], - - "files": [], - - "commands": [ - { "directive": "sleep infinity &" }, - { "directive": "set -eo pipefail" }, - { "directive": "cat non_existant | sort" } - ], - - "epilogue_always_commands": [], - - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_job_to_finish - -assert_job_log <<-LOG - {"event":"job_started", "timestamp":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"sleep infinity &"} - {"event":"cmd_finished", "timestamp":"*", "directive":"sleep infinity &","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"set -eo pipefail"} - {"event":"cmd_finished", "timestamp":"*", "directive":"set -eo pipefail","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"cat non_existant | sort"} - {"event":"cmd_output", "timestamp":"*", "output":"cat: non_existant: No such file or directory\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"cat non_existant | sort","exit_code":1,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":1,"started_at":"*","finished_at":"*"} - {"event":"job_finished", "timestamp":"*", "result":"failed"} -LOG diff --git a/test/e2e/shell/stty_restoration.rb b/test/e2e/shell/stty_restoration.rb deleted file mode 100644 index df921f57..00000000 --- a/test/e2e/shell/stty_restoration.rb +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -require_relative '../../e2e' - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - - "env_vars": [], - - "files": [], - - "commands": [ - { "directive": "stty echo" }, - { "directive": "echo Hello World" } - ], - - "epilogue_always_commands": [], - - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_job_to_finish - -assert_job_log <<-LOG - {"event":"job_started", "timestamp":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"stty echo"} - {"event":"cmd_finished", "timestamp":"*", "directive":"stty echo","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello World"} - {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello World","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} - {"event":"job_finished", "timestamp":"*", "result":"passed"} -LOG diff --git a/test/e2e/shell/unicode.rb b/test/e2e/shell/unicode.rb deleted file mode 100644 index 2e0d0e51..00000000 --- a/test/e2e/shell/unicode.rb +++ /dev/null @@ -1,72 +0,0 @@ -#!/bin/ruby - -Encoding.default_external = Encoding::UTF_8 - -# rubocop:disable all - -require_relative '../../e2e' - -# -# This is regression test that verifies that we are correctly processing outgoing -# bytes from the shell. -# -# In case the byte processing is incorrect, the output can contain question mark -# characters instead of valid UTF-8 characters. -# -# Initially, this test only contined Japanese characters to verify that the issue -# has been fixed. However, a sub-case of this issue still caused an issue for a -# customer who is displaying box drawing characters in their tests. -# - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - - "env_vars": [], - "files": [], - - "commands": [ - { "directive": "echo 特定の伝説に拠る物語の由来については諸説存在し。特定の伝説に拠る物語の由来については諸説存在し。特定の伝説に拠る物語の由来については諸説存在し。" }, - { "directive": "echo ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" } - ], - - "epilogue_always_commands": [], - - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_job_to_finish - -assert_job_log <<-LOG - {"event":"job_started", "timestamp":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"echo 特定の伝説に拠る物語の由来については諸説存在し。特定の伝説に拠る物語の由来については諸説存在し。特定の伝説に拠る物語の由来については諸説存在し。"} - {"event":"cmd_output", "timestamp":"*", "output":"特定の伝説に拠る物語の由来については諸説存在し。特定の伝説に拠る物"} - {"event":"cmd_output", "timestamp":"*", "output":"語の由来については諸説存在し。特定の伝説に拠る物語の由来については"} - {"event":"cmd_output", "timestamp":"*", "output":"諸説存在し。\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo 特定の伝説に拠る物語の由来については諸説存在し。特定の伝説に拠る物語の由来については諸説存在し。特定の伝説に拠る物語の由来については諸説存在し。","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"echo ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"} - {"event":"cmd_output", "timestamp":"*", "output":"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"} - {"event":"cmd_output", "timestamp":"*", "output":"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"} - {"event":"cmd_output", "timestamp":"*", "output":"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"} - {"event":"cmd_output", "timestamp":"*", "output":"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"} - {"event":"cmd_output", "timestamp":"*", "output":"━━━━━━━━━━━━━━━━━━━━━━\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} - - {"event":"job_finished", "timestamp":"*", "result":"passed"} -LOG diff --git a/test/e2e/shell/unknown_command.rb b/test/e2e/shell/unknown_command.rb deleted file mode 100644 index 44522b59..00000000 --- a/test/e2e/shell/unknown_command.rb +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -require_relative '../../e2e' - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - - "env_vars": [], - - "files": [], - - "commands": [ - { "directive": "echhhho Hello World" } - ], - - "epilogue_always_commands": [], - - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_job_to_finish - -assert_job_log <<-LOG - {"event":"job_started", "timestamp":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"echhhho Hello World"} - {"event":"cmd_output", "timestamp":"*", "output":"bash: echhhho: command not found\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echhhho Hello World","exit_code":127,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} - {"event":"job_finished", "timestamp":"*", "result":"failed"} -LOG diff --git a/test/support/commands.go b/test/support/commands.go new file mode 100644 index 00000000..7af1a117 --- /dev/null +++ b/test/support/commands.go @@ -0,0 +1,153 @@ +package testsupport + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func AssertSimplifiedJobLogs(t *testing.T, actual, expected []string) { + actualIndex := 0 + expectedIndex := 0 + + for actualIndex < len(actual)-1 && expectedIndex < len(expected)-1 { + actualLine := actual[actualIndex] + expectedLine := expected[expectedIndex] + + if expectedLine == "*** OUTPUT ***" { + if strings.HasPrefix(actualLine, "Exit Code: ") { + expectedIndex++ + } else { + actualIndex++ + } + } else { + if !assert.Equal(t, actualLine, expectedLine) { + break + } else { + actualIndex++ + expectedIndex++ + } + } + } +} + +func Cat(fileName string) string { + if runtime.GOOS == "windows" { + return fmt.Sprintf("Get-Content %s", filepath.FromSlash(fileName)) + } + + return fmt.Sprintf("cat %s", fileName) +} + +func Multiline() string { + if runtime.GOOS == "windows" { + return fmt.Sprintf(` + if (Test-Path %s) { + echo "etc exists, multiline huzzahh!" + } + `, os.TempDir()) + } + + return ` + if [ -d /etc ]; then + echo 'etc exists, multiline huzzahh!' + fi + ` +} + +func Output(line string) string { + if runtime.GOOS == "windows" { + return fmt.Sprintf("Write-Host \"%s\" -NoNewLine", line) + } + + return fmt.Sprintf("echo -n '%s'", line) +} + +func EchoEnvVar(envVar string) string { + if runtime.GOOS == "windows" { + return fmt.Sprintf("Write-Host \"$env:%s\" -NoNewLine", envVar) + } + + return fmt.Sprintf("echo -n $%s", envVar) +} + +func EchoEnvVarToFile(envVar, fileName string) string { + if runtime.GOOS == "windows" { + return fmt.Sprintf("Set-Content -Path %s -Value \"$env:%s\"", fileName, envVar) + } + + return fmt.Sprintf("echo -n $%s > %s", envVar, fileName) +} + +func SetEnvVar(name, value string) string { + if runtime.GOOS == "windows" { + return fmt.Sprintf("$env:%s = '%s'", name, value) + } + + return fmt.Sprintf("export %s=%s", name, value) +} + +func UnsetEnvVar(name string) string { + if runtime.GOOS == "windows" { + return fmt.Sprintf("Remove-Item -Path env:%s", name) + } + + return fmt.Sprintf("unset %s", name) +} + +func LargeOutputCommand() string { + if runtime.GOOS == "windows" { + return "foreach ($i in 1..100) { Write-Host \"hello\" -NoNewLine }" + } + + return "for i in {1..100}; { printf 'hello'; }" +} + +func Chdir(dirName string) string { + if runtime.GOOS == "windows" { + return fmt.Sprintf("Set-Location %s", dirName) + } + + return fmt.Sprintf("cd %s", dirName) +} + +func UnknownCommandExitCode() int { + if runtime.GOOS == "windows" { + return 1 + } + + return 127 +} + +func StoppedCommandExitCode() int { + if runtime.GOOS == "windows" { + return 0 + } + + return 1 +} + +func CopyFile(src, dest string) string { + if runtime.GOOS == "windows" { + return fmt.Sprintf("Copy-Item %s -Destination %s", src, dest) + } + + return fmt.Sprintf("cp %s %s", src, dest) +} + +func NestedEnvVarValue(name, rest string) string { + if runtime.GOOS == "windows" { + return fmt.Sprintf("$env:%s%s", name, rest) + } + + return fmt.Sprintf("$%s%s", name, rest) +} + +func EchoBrokenUnicode() string { + return "echo | awk '{ printf(\"%c%c%c%c%c\", 150, 150, 150, 150, 150) }'" +} diff --git a/test/support/hub.go b/test/support/hub.go new file mode 100644 index 00000000..9882cb4c --- /dev/null +++ b/test/support/hub.go @@ -0,0 +1,274 @@ +package testsupport + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "time" + + "github.com/semaphoreci/agent/pkg/api" + "github.com/semaphoreci/agent/pkg/listener/selfhostedapi" + "github.com/semaphoreci/agent/pkg/retry" +) + +type HubMockServer struct { + Server *httptest.Server + Handler http.Handler + JobRequest *api.JobRequest + LogsURL string + RegisterRequest *selfhostedapi.RegisterRequest + RegisterAttemptRejections int + RegisterAttempts int + GetJobAttemptRejections int + GetJobAttempts int + ShouldShutdown bool + Disconnected bool + RunningJob bool + FinishedJob bool + FailureStatus string +} + +func NewHubMockServer() *HubMockServer { + return &HubMockServer{ + RegisterAttempts: -1, + } +} + +func (m *HubMockServer) Init() { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch path := r.URL.Path; { + case strings.Contains(path, "/register"): + m.handleRegisterRequest(w, r) + case strings.Contains(path, "/sync"): + m.handleSyncRequest(w, r) + case strings.Contains(path, "/disconnect"): + fmt.Printf("[HUB MOCK] Received disconnect request\n") + m.Disconnected = true + w.WriteHeader(200) + case strings.Contains(path, "/jobs/"): + m.handleGetJobRequest(w, r) + } + })) + + m.Server = mockServer +} + +func (m *HubMockServer) handleRegisterRequest(w http.ResponseWriter, r *http.Request) { + m.RegisterAttempts++ + if m.RegisterAttempts < m.RegisterAttemptRejections { + fmt.Printf("[HUB MOCK] Attempts: %d, Rejections: %d, rejecting...\n", m.RegisterAttempts, m.RegisterAttemptRejections) + w.WriteHeader(500) + } + + request := selfhostedapi.RegisterRequest{} + bytes, err := ioutil.ReadAll(r.Body) + if err != nil { + fmt.Printf("[HUB MOCK] Error reading register request body: %v\n", err) + w.WriteHeader(500) + return + } + + err = json.Unmarshal(bytes, &request) + if err != nil { + fmt.Printf("[HUB MOCK] Error unmarshaling register request: %v\n", err) + w.WriteHeader(500) + return + } + + fmt.Printf("[HUB MOCK] Received register request: %v\n", request) + m.RegisterRequest = &request + + registerResponse := &selfhostedapi.RegisterResponse{ + Name: request.Name, + Token: "token", + } + + response, err := json.Marshal(registerResponse) + if err != nil { + fmt.Printf("[HUB MOCK] Error marshaling register response: %v\n", err) + w.WriteHeader(500) + return + } + + _, _ = w.Write(response) +} + +func (m *HubMockServer) handleSyncRequest(w http.ResponseWriter, r *http.Request) { + request := selfhostedapi.SyncRequest{} + bytes, err := ioutil.ReadAll(r.Body) + if err != nil { + fmt.Printf("[HUB MOCK] Error reading sync request body: %v\n", err) + w.WriteHeader(500) + return + } + + err = json.Unmarshal(bytes, &request) + if err != nil { + fmt.Printf("[HUB MOCK] Error unmarshaling sync request: %v\n", err) + w.WriteHeader(500) + return + } + + fmt.Printf("[HUB MOCK] Received sync request: %v\n", request) + + syncResponse := selfhostedapi.SyncResponse{ + Action: selfhostedapi.AgentActionContinue, + } + + switch request.State { + case selfhostedapi.AgentStateWaitingForJobs: + if m.ShouldShutdown { + syncResponse.Action = selfhostedapi.AgentActionShutdown + } + + if m.JobRequest != nil { + syncResponse.Action = selfhostedapi.AgentActionRunJob + syncResponse.JobID = m.JobRequest.ID + } + + case selfhostedapi.AgentStateRunningJob: + m.RunningJob = true + + if m.ShouldShutdown { + syncResponse.Action = selfhostedapi.AgentActionStopJob + syncResponse.JobID = m.JobRequest.ID + } + + case selfhostedapi.AgentStateFinishedJob: + m.JobRequest = nil + m.FinishedJob = true + + if m.ShouldShutdown { + syncResponse.Action = selfhostedapi.AgentActionShutdown + } else { + syncResponse.Action = selfhostedapi.AgentActionWaitForJobs + } + + case selfhostedapi.AgentStateFailedToFetchJob, + selfhostedapi.AgentStateFailedToConstructJob, + selfhostedapi.AgentStateFailedToSendCallback: + m.FailureStatus = string(request.State) + syncResponse.Action = selfhostedapi.AgentActionWaitForJobs + } + + response, err := json.Marshal(syncResponse) + if err != nil { + fmt.Printf("[HUB MOCK] Error marshaling sync response: %v\n", err) + w.WriteHeader(500) + return + } + + _, _ = w.Write(response) +} + +func (m *HubMockServer) handleGetJobRequest(w http.ResponseWriter, r *http.Request) { + m.GetJobAttempts++ + if m.GetJobAttempts < m.GetJobAttemptRejections { + fmt.Printf("[HUB MOCK] Get job, Attempts: %d, Rejections: %d, rejecting...\n", m.GetJobAttempts, m.GetJobAttemptRejections) + w.WriteHeader(500) + } + + if m.JobRequest == nil { + fmt.Printf("[HUB MOCK] No jobRequest in use\n") + w.WriteHeader(404) + return + } + + response, err := json.Marshal(m.JobRequest) + if err != nil { + fmt.Printf("[HUB MOCK] Error marshaling job request: %v\n", err) + w.WriteHeader(500) + return + } + + _, _ = w.Write(response) +} + +func (m *HubMockServer) UseLogsURL(URL string) { + m.LogsURL = URL +} + +func (m *HubMockServer) AssignJob(jobRequest *api.JobRequest) { + m.JobRequest = jobRequest +} + +func (m *HubMockServer) RejectRegisterAttempts(times int) { + m.RegisterAttemptRejections = times +} + +func (m *HubMockServer) RejectGetJobAttempts(times int) { + m.GetJobAttemptRejections = times +} + +func (m *HubMockServer) URL() string { + return m.Server.URL +} + +func (m *HubMockServer) Host() string { + return m.Server.Listener.Addr().String() +} + +func (m *HubMockServer) WaitUntilFailure(status string, attempts int, wait time.Duration) error { + return retry.RetryWithConstantWait("WaitUntilRunningJob", attempts, wait, func() error { + if m.FailureStatus != status { + return fmt.Errorf("still haven't failed with %s", status) + } + + return nil + }) +} + +func (m *HubMockServer) WaitUntilRunningJob(attempts int, wait time.Duration) error { + return retry.RetryWithConstantWait("WaitUntilRunningJob", attempts, wait, func() error { + if !m.RunningJob { + return fmt.Errorf("still not running job") + } + + return nil + }) +} + +func (m *HubMockServer) WaitUntilFinishedJob(attempts int, wait time.Duration) error { + return retry.RetryWithConstantWait("WaitUntilFinishedJob", attempts, wait, func() error { + if !m.FinishedJob { + return fmt.Errorf("still not finished job") + } + + return nil + }) +} + +func (m *HubMockServer) WaitUntilDisconnected(attempts int, wait time.Duration) error { + return retry.RetryWithConstantWait("WaitUntilDisconnected", attempts, wait, func() error { + if !m.Disconnected { + return fmt.Errorf("still not disconnected") + } + + return nil + }) +} + +func (m *HubMockServer) WaitUntilRegistered() error { + return retry.RetryWithConstantWait("WaitUntilRegistered", 10, time.Second, func() error { + if m.RegisterRequest == nil { + return fmt.Errorf("still not registered") + } + + return nil + }) +} + +func (m *HubMockServer) GetRegisterRequest() *selfhostedapi.RegisterRequest { + return m.RegisterRequest +} + +func (m *HubMockServer) ScheduleShutdown() { + m.ShouldShutdown = true +} + +func (m *HubMockServer) Close() { + m.Server.Close() +} diff --git a/test/support/loghub.go b/test/support/loghub.go new file mode 100644 index 00000000..83f782b2 --- /dev/null +++ b/test/support/loghub.go @@ -0,0 +1,64 @@ +package testsupport + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" +) + +type LoghubMockServer struct { + Logs []string + Server *httptest.Server + Handler http.Handler +} + +func NewLoghubMockServer() *LoghubMockServer { + return &LoghubMockServer{ + Logs: []string{}, + } +} + +func (m *LoghubMockServer) Init() { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + fmt.Println("[LOGHUB MOCK] Received logs") + body, err := ioutil.ReadAll(r.Body) + if err != nil { + fmt.Printf("Error reading body: %v\n", err) + } + + logs := strings.Split(string(body), "\n") + m.Logs = append(m.Logs, FilterEmpty(logs)...) + w.WriteHeader(200) + } else { + fmt.Println("NOPE") + } + })) + + m.Server = mockServer +} + +func (m *LoghubMockServer) GetLogs() []string { + return m.Logs +} + +func (m *LoghubMockServer) URL() string { + return m.Server.URL +} + +func (m *LoghubMockServer) Close() { + m.Server.Close() +} + +func FilterEmpty(logs []string) []string { + filtered := []string{} + for _, log := range logs { + if log != "" { + filtered = append(filtered, log) + } + } + + return filtered +} diff --git a/test/support/test_logger.go b/test/support/test_logger.go index b48adc40..e8b87419 100644 --- a/test/support/test_logger.go +++ b/test/support/test_logger.go @@ -5,11 +5,14 @@ import ( "io" "log" "os" + "path/filepath" ) func SetupTestLogs() { + path := filepath.Join(os.TempDir(), "test.log") + // #nosec - f, err := os.OpenFile("/tmp/test.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0777) + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0777) if err != nil { fmt.Printf("error opening file: %v", err) panic("can't open log file") From 8b49ac009fde721244419ab4b5637817e8887fcb Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Thu, 10 Mar 2022 08:58:13 -0300 Subject: [PATCH 016/130] Add PowerShell installation script (#146) --- .goreleaser.yml | 31 +++++++++++++++-- install.ps1 | 91 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 install.ps1 diff --git a/.goreleaser.yml b/.goreleaser.yml index 218969b4..af70a2e2 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -3,7 +3,8 @@ before: hooks: - go get ./... builds: -- env: +- id: non-windows-build + env: - CGO_ENABLED=0 ldflags: - -s -w -X main.VERSION={{.Tag}} @@ -15,9 +16,23 @@ builds: - amd64 - arm - arm64 +- id: windows-build + env: + - CGO_ENABLED=0 + ldflags: + - -s -w -X main.VERSION={{.Tag}} + goos: + - windows + goarch: + - 386 + - amd64 + - arm + - arm64 archives: - - id: agent + - id: non-windows-archive + builds: + - non-windows-build name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' files: - README.md @@ -28,6 +43,18 @@ archives: 386: i386 amd64: x86_64 + - id: windows-archive + builds: + - windows-build + name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' + files: + - README.md + - install.ps1 + replacements: + 386: i386 + amd64: x86_64 + windows: Windows + checksum: name_template: '{{ .ProjectName }}_checksums.txt' changelog: diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 00000000..1f47b5fe --- /dev/null +++ b/install.ps1 @@ -0,0 +1,91 @@ +$ProgressPreference = 'SilentlyContinue' +$ErrorActionPreference = "Stop" +$InstallationDirectory = $PSScriptRoot + +# +# Assert required variables are set +# +if (Test-Path env:SemaphoreEndpoint) { + $SemaphoreEndpoint = $env:SemaphoreEndpoint +} else { + if (-not (Test-Path env:SemaphoreOrganization)) { + Write-Warning 'Either $env:SemaphoreOrganization or $env:SemaphoreEndpoint needs to be specified. Exiting...' + Exit 1 + } + + $SemaphoreEndpoint = "$env:SemaphoreOrganization.semaphoreci.com" + Write-Warning "`$env:SemaphoreEndpoint not set, using '$SemaphoreEndpoint'" +} + +if (-not (Test-Path env:SemaphoreRegistrationToken)) { + Write-Warning 'Registration token cannot be empty, set $env:SemaphoreRegistrationToken. Exiting...' + Exit 1 +} + +# +# Set defaults in case some variables are not set +# +if (Test-Path env:SemaphoreAgentDisconnectAfterJob) { + $DisconnectAfterJob = $env:SemaphoreAgentDisconnectAfterJob +} else { + $DisconnectAfterJob = "false" +} + +if (Test-Path env:SemaphoreAgentDisconnectAfterIdleTimeout) { + $DisconnectAfterIdleTimeout = $env:SemaphoreAgentDisconnectAfterIdleTimeout +} else { + $DisconnectAfterIdleTimeout = 0 +} + +# +# Download and unpack toolbox +# +$ToolboxDirectory = Join-Path $HOME ".toolbox" +$InstallScriptPath = Join-Path $ToolboxDirectory "install-toolbox.ps1" + +Write-Output "> Toolbox will be installed at $ToolboxDirectory." +if (Test-Path $ToolboxDirectory) { + Write-Output "> Toolbox already installed at $ToolboxDirectory. Overriding it..." + Remove-Item -Path $ToolboxDirectory -Force -Recurse +} + +Write-Output "> Downloading and unpacking toolbox..." +Invoke-WebRequest "https://github.com/semaphoreci/toolbox/releases/latest/download/self-hosted-windows.tar" -OutFile toolbox.tar +tar.exe -xf toolbox.tar -C $HOME +Rename-Item "$HOME\toolbox" $ToolboxDirectory +Remove-Item toolbox.tar -Force + +# +# Install toolbox +# +Write-Output "> Installing toolbox..." +& $InstallScriptPath + +# +# Create agent config in current directory +# +$AgentConfig = @" +endpoint: "$SemaphoreEndpoint" +token: "$env:SemaphoreRegistrationToken" +no-https: false +shutdown-hook-path: "$env:SemaphoreAgentShutdownHook" +disconnect-after-job: $DisconnectAfterJob +disconnect-after-idle-timeout: $DisconnectAfterIdleTimeout +env-vars: [] +files: [] +fail-on-missing-files: false +"@ + +$AgentConfigPath = Join-Path $InstallationDirectory "config.yaml" +if (Test-Path $AgentConfigPath) { + Write-Output "> Agent configuration file already exists in $AgentConfigPath. Overriding it..." + Remove-Item -Path $AgentConfigPath -Force -Recurse +} + +New-Item -ItemType File -Path $AgentConfigPath > $null +Set-Content -Path $AgentConfigPath -Value $AgentConfig + +Write-Output "> Successfully installed the agent in $InstallationDirectory." +Write-Output " + Start the agent with: $InstallationDirectory\agent.exe start --config-file $AgentConfigPath +" From 271b1bdb0e732e3dbea4a4d483178314e16f4b41 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Fri, 11 Mar 2022 09:06:09 -0300 Subject: [PATCH 017/130] Check error reading environment file (#148) --- pkg/shell/process.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pkg/shell/process.go b/pkg/shell/process.go index 9cffb803..2c58e1ec 100644 --- a/pkg/shell/process.go +++ b/pkg/shell/process.go @@ -160,15 +160,21 @@ func (p *Process) Run() { * We use a file with all the environment variables available after the command * is executed. From that file, we can update our shell "state". */ - after, _ := CreateEnvironmentFromFile(p.EnvironmentFilePath()) + after, err := CreateEnvironmentFromFile(p.EnvironmentFilePath()) + if err != nil { + log.Errorf("Error creating environment from file %s: %v\n", p.EnvironmentFilePath(), err) + return + } /* * CMD.exe does not have an environment variable such as $PWD, * so we use a custom one to get the current working directory * after a command is executed. */ - newCwd, _ := after.Get("SEMAPHORE_AGENT_CURRENT_DIR") - p.Shell.Chdir(newCwd) + newCwd, exists := after.Get("SEMAPHORE_AGENT_CURRENT_DIR") + if exists { + p.Shell.Chdir(newCwd) + } /* * We use two custom environment variables to track From 2c1decf06966f15c6cda4d7faae9a91ee599c334 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Fri, 11 Mar 2022 14:05:58 -0300 Subject: [PATCH 018/130] Grab STDOUT AND STDERR when executing shutdown hook (#142) --- pkg/listener/job_processor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/listener/job_processor.go b/pkg/listener/job_processor.go index 28223c99..99377546 100644 --- a/pkg/listener/job_processor.go +++ b/pkg/listener/job_processor.go @@ -299,7 +299,7 @@ func (p *JobProcessor) executeShutdownHook(reason ShutdownReason) { } cmd.Env = append(os.Environ(), fmt.Sprintf("SEMAPHORE_AGENT_SHUTDOWN_REASON=%s", reason)) - output, err := cmd.Output() + output, err := cmd.CombinedOutput() if err != nil { log.Errorf("Error executing shutdown hook: %v", err) log.Errorf("Output: %s", string(output)) From 60c13fb17b3c475e024610b334c3ead0f00c4680 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Tue, 15 Mar 2022 11:52:39 -0300 Subject: [PATCH 019/130] Support SEMAPHORE_ENDPOINT on installation script (#149) --- install.sh | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/install.sh b/install.sh index 18a64dc0..67355f1d 100755 --- a/install.sh +++ b/install.sh @@ -10,12 +10,16 @@ if [[ "$EUID" -ne 0 ]]; then exit 1 fi -if [[ -z $SEMAPHORE_ORGANIZATION ]]; then - read -p "Enter organization: " SEMAPHORE_ORGANIZATION +if [[ -z $SEMAPHORE_ENDPOINT ]]; then if [[ -z $SEMAPHORE_ORGANIZATION ]]; then - echo "Organization cannot be empty." - exit 1 + read -p "Enter organization: " SEMAPHORE_ORGANIZATION + if [[ -z $SEMAPHORE_ORGANIZATION ]]; then + echo "Organization cannot be empty." + exit 1 + fi fi + + SEMAPHORE_ENDPOINT="$SEMAPHORE_ORGANIZATION.semaphoreci.com" fi if [[ -z $SEMAPHORE_REGISTRATION_TOKEN ]]; then @@ -64,7 +68,7 @@ rm toolbox.tar SEMAPHORE_AGENT_DISCONNECT_AFTER_JOB=${SEMAPHORE_AGENT_DISCONNECT_AFTER_JOB:-false} SEMAPHORE_AGENT_DISCONNECT_AFTER_IDLE_TIMEOUT=${SEMAPHORE_AGENT_DISCONNECT_AFTER_IDLE_TIMEOUT:-0} AGENT_CONFIG=$(cat <<-END -endpoint: "$SEMAPHORE_ORGANIZATION.semaphoreci.com" +endpoint: "$SEMAPHORE_ENDPOINT" token: "$SEMAPHORE_REGISTRATION_TOKEN" no-https: false shutdown-hook-path: "$SEMAPHORE_AGENT_SHUTDOWN_HOOK" From 15e575160ab60953e80f6b971496b1fcd177f591 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Mon, 2 May 2022 12:58:55 -0300 Subject: [PATCH 020/130] fix: update github.com/spf13/viper to v1.11.0 (#150) --- go.mod | 8 +-- go.sum | 210 ++++++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 183 insertions(+), 35 deletions(-) diff --git a/go.mod b/go.mod index 5dfb2325..91082b29 100644 --- a/go.mod +++ b/go.mod @@ -10,11 +10,9 @@ require ( github.com/renderedtext/go-watchman v0.0.0-20210809121718-0632d0d12b0a github.com/sirupsen/logrus v1.8.1 github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.9.0 - github.com/stretchr/testify v1.7.0 - golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 - golang.org/x/text v0.3.7 // indirect - gopkg.in/ini.v1 v1.64.0 // indirect + github.com/spf13/viper v1.11.0 + github.com/stretchr/testify v1.7.1 + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) diff --git a/go.sum b/go.sum index e920757f..3ce53a11 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,7 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= @@ -15,6 +16,7 @@ cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOY cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= @@ -23,15 +25,22 @@ cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSU cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.6.0/go.mod h1:afJwI0vaXwAG54kI7A//lP/lSPDkQORQuMkv56TxEPU= +cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -41,26 +50,43 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= @@ -75,9 +101,11 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= @@ -87,7 +115,13 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.1.0 h1:XUgk2Ex5veyVFVeLm0xhusUTQybEbexJXrvPNOKkSY0= github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= @@ -95,6 +129,7 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -136,6 +171,7 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -150,6 +186,7 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= @@ -160,20 +197,29 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= +github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= @@ -181,98 +227,134 @@ github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= -github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo= -github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/panicwrap v1.0.0 h1:67zIyVakCIvcs69A0FGfZjBdPleaonSgGlXRSRlb6fE= github.com/mitchellh/panicwrap v1.0.0/go.mod h1:pKvZHwWrZowLUzftuFq7coarnxbBXU4aQh3N0BJOeeA= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.0-beta.8 h1:dy81yyLYJDwMTifq24Oi/IslOslRrDSb3jwDggjz3Z0= +github.com/pelletier/go-toml/v2 v2.0.0-beta.8/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/renderedtext/go-watchman v0.0.0-20210809121718-0632d0d12b0a h1:pRX9qebwT+TMdBojMspqDtU1RFLIbH5VzI8aI9yMiyE= github.com/renderedtext/go-watchman v0.0.0-20210809121718-0632d0d12b0a/go.mod h1:Z+qanDzSoUGCbcrTM7G6YCA9ST2KBdte7sCz+HQAp7I= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYIR88KRMEuODE= +github.com/sagikazarmark/crypt v0.5.0/go.mod h1:l+nzl7KWh51rpzp2h7t4MZWyiEWdhNpOAnclKvg+mdA= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= +github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.9.0 h1:yR6EXjTp0y0cLN8OZg1CRZmOBdI88UcGkhgyJhu6nZk= -github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4= +github.com/spf13/viper v1.11.0 h1:7OX/1FS6n7jHD1zGrZTM7WtY13ZELRyosK4k93oPr44= +github.com/spf13/viper v1.11.0/go.mod h1:djo0X/bA5+tYVoCn+C7cAYJGcVn/qYLFTG8gdUsX7Zk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.etcd.io/etcd/api/v3 v3.5.2/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= +go.etcd.io/etcd/client/pkg/v3 v3.5.2/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.2/go.mod h1:2D7ZejHVMIfog1221iLSYlQRzrtECw3kz4I4VAQm3qI= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -284,15 +366,16 @@ go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqe go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -330,7 +413,7 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -338,6 +421,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -360,11 +444,18 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -380,6 +471,11 @@ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -393,11 +489,13 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -432,12 +530,15 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -446,9 +547,20 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI= -golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -510,6 +622,7 @@ golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= @@ -520,6 +633,7 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -546,7 +660,16 @@ google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtuk google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -589,7 +712,9 @@ google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -606,6 +731,24 @@ google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKr google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -631,6 +774,9 @@ google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -645,6 +791,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/statsd.v2 v2.0.0 h1:FXkZSCZIH17vLCO5sO2UucTHsH9pc+17F6pl3JVCwMc= gopkg.in/alexcesaro/statsd.v2 v2.0.0/go.mod h1:i0ubccKGzBVNBpdGV5MocxyA/XlLUJzA7SLonnE4drU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -652,11 +800,13 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.64.0 h1:Mj2zXEXcNb5joEiSA0zc3HZpTst/iyjNiR4CN8tDzOg= -gopkg.in/ini.v1 v1.64.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= +gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= From 3fc6a6af6492e9a736b66ba9ba5682a5480784fd Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Tue, 3 May 2022 19:30:25 -0300 Subject: [PATCH 021/130] feat: allow toolbox version to be specified during installation (#151) --- install.ps1 | 10 ++++++++-- install.sh | 11 ++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/install.ps1 b/install.ps1 index 1f47b5fe..72333f77 100644 --- a/install.ps1 +++ b/install.ps1 @@ -49,8 +49,14 @@ if (Test-Path $ToolboxDirectory) { Remove-Item -Path $ToolboxDirectory -Force -Recurse } -Write-Output "> Downloading and unpacking toolbox..." -Invoke-WebRequest "https://github.com/semaphoreci/toolbox/releases/latest/download/self-hosted-windows.tar" -OutFile toolbox.tar +if (Test-Path env:SemaphoreToolboxVersion) { + Write-Output "> Downloading and unpacking $env:SemaphoreToolboxVersion toolbox..." + Invoke-WebRequest "https://github.com/semaphoreci/toolbox/releases/download/$env:SemaphoreToolboxVersion/self-hosted-windows.tar" -OutFile toolbox.tar +} else { + Write-Output '> $env:SemaphoreToolboxVersion is not set. Downloading and unpacking latest toolbox...' + Invoke-WebRequest "https://github.com/semaphoreci/toolbox/releases/latest/download/self-hosted-windows.tar" -OutFile toolbox.tar +} + tar.exe -xf toolbox.tar -C $HOME Rename-Item "$HOME\toolbox" $ToolboxDirectory Remove-Item toolbox.tar -Force diff --git a/install.sh b/install.sh index 67355f1d..d15eb5c4 100755 --- a/install.sh +++ b/install.sh @@ -44,7 +44,6 @@ fi # # Download toolbox # -echo "Installing toolbox..." USER_HOME_DIRECTORY=$(sudo -u $SEMAPHORE_AGENT_INSTALLATION_USER -H bash -c 'echo $HOME') TOOLBOX_DIRECTORY="$USER_HOME_DIRECTORY/.toolbox" if [[ -d "$TOOLBOX_DIRECTORY" ]]; then @@ -52,9 +51,15 @@ if [[ -d "$TOOLBOX_DIRECTORY" ]]; then rm -rf "$TOOLBOX_DIRECTORY" fi -curl -sL "https://github.com/semaphoreci/toolbox/releases/latest/download/self-hosted-linux.tar" -o toolbox.tar -tar -xf toolbox.tar +if [[ -z "${SEMAPHORE_TOOLBOX_VERSION}" ]]; then + echo "SEMAPHORE_TOOLBOX_VERSION is not set. Installing latest toolbox..." + curl -sL "https://github.com/semaphoreci/toolbox/releases/latest/download/self-hosted-linux.tar" -o toolbox.tar +else + echo "Installing ${SEMAPHORE_TOOLBOX_VERSION} toolbox..." + curl -sL "https://github.com/semaphoreci/toolbox/releases/download/${SEMAPHORE_TOOLBOX_VERSION}/self-hosted-linux.tar" -o toolbox.tar +fi +tar -xf toolbox.tar mv toolbox $TOOLBOX_DIRECTORY sudo chown -R $SEMAPHORE_AGENT_INSTALLATION_USER:$SEMAPHORE_AGENT_INSTALLATION_USER $TOOLBOX_DIRECTORY From 81194e71bae9c2abf567009f008e36695c3ebc7a Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Mon, 30 May 2022 16:26:15 -0300 Subject: [PATCH 022/130] fix: update hub reference dependencies (#152) --- test/hub_reference/Gemfile.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/hub_reference/Gemfile.lock b/test/hub_reference/Gemfile.lock index 43fbd2f2..419dfbdc 100644 --- a/test/hub_reference/Gemfile.lock +++ b/test/hub_reference/Gemfile.lock @@ -1,18 +1,18 @@ GEM remote: https://rubygems.org/ specs: - daemons (1.4.0) + daemons (1.4.1) eventmachine (1.2.7) mustermann (1.1.1) ruby2_keywords (~> 0.0.1) - rack (2.2.3) - rack-protection (2.1.0) + rack (2.2.3.1) + rack-protection (2.2.0) rack - ruby2_keywords (0.0.4) - sinatra (2.1.0) + ruby2_keywords (0.0.5) + sinatra (2.2.0) mustermann (~> 1.0) rack (~> 2.2) - rack-protection (= 2.1.0) + rack-protection (= 2.2.0) tilt (~> 2.0) thin (1.8.1) daemons (~> 1.0, >= 1.0.9) From f49dc8ec0f28fba8ab53ec3394cb967d8a0f1c74 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Mon, 11 Jul 2022 15:18:20 -0300 Subject: [PATCH 023/130] feat: refresh token used for logs (#154) --- pkg/eventlogger/default.go | 12 ++-- pkg/eventlogger/httpbackend.go | 54 ++++++++++----- pkg/eventlogger/httpbackend_test.go | 55 ++++++++++++++- pkg/jobs/job.go | 4 +- pkg/listener/job_processor.go | 3 + pkg/listener/listener_test.go | 75 +++++++++++++++++++++ pkg/listener/selfhostedapi/refresh_token.go | 53 +++++++++++++++ pkg/server/server.go | 1 + test/support/hub.go | 20 ++++++ test/support/loghub.go | 57 +++++++++++----- 10 files changed, 295 insertions(+), 39 deletions(-) create mode 100644 pkg/listener/selfhostedapi/refresh_token.go diff --git a/pkg/eventlogger/default.go b/pkg/eventlogger/default.go index 15120030..4191d20a 100644 --- a/pkg/eventlogger/default.go +++ b/pkg/eventlogger/default.go @@ -13,12 +13,12 @@ import ( const LoggerMethodPull = "pull" const LoggerMethodPush = "push" -func CreateLogger(request *api.JobRequest) (*Logger, error) { +func CreateLogger(request *api.JobRequest, refreshTokenFn func() (string, error)) (*Logger, error) { switch request.Logger.Method { case LoggerMethodPull: return Default() case LoggerMethodPush: - return DefaultHTTP(request) + return DefaultHTTP(request, refreshTokenFn) default: return nil, fmt.Errorf("unknown logger type") } @@ -44,12 +44,16 @@ func Default() (*Logger, error) { return logger, nil } -func DefaultHTTP(request *api.JobRequest) (*Logger, error) { +func DefaultHTTP(request *api.JobRequest, refreshTokenFn func() (string, error)) (*Logger, error) { if request.Logger.URL == "" { return nil, errors.New("HTTP logger needs a URL") } - backend, err := NewHTTPBackend(request.Logger.URL, request.Logger.Token) + if refreshTokenFn == nil { + return nil, errors.New("HTTP logger needs a refresh token function") + } + + backend, err := NewHTTPBackend(request.Logger.URL, request.Logger.Token, refreshTokenFn) if err != nil { return nil, err } diff --git a/pkg/eventlogger/httpbackend.go b/pkg/eventlogger/httpbackend.go index 9f36c3a9..9d252567 100644 --- a/pkg/eventlogger/httpbackend.go +++ b/pkg/eventlogger/httpbackend.go @@ -14,16 +14,17 @@ import ( ) type HTTPBackend struct { - client *http.Client - url string - token string - fileBackend FileBackend - startFrom int - streamChan chan bool - pushLock sync.Mutex + client *http.Client + url string + token string + fileBackend FileBackend + startFrom int + streamChan chan bool + pushLock sync.Mutex + refreshTokenFn func() (string, error) } -func NewHTTPBackend(url, token string) (*HTTPBackend, error) { +func NewHTTPBackend(url, token string, refreshTokenFn func() (string, error)) (*HTTPBackend, error) { path := filepath.Join(os.TempDir(), fmt.Sprintf("job_log_%d.json", time.Now().UnixNano())) fileBackend, err := NewFileBackend(path) if err != nil { @@ -31,11 +32,12 @@ func NewHTTPBackend(url, token string) (*HTTPBackend, error) { } httpBackend := HTTPBackend{ - client: &http.Client{}, - url: url, - token: token, - fileBackend: *fileBackend, - startFrom: 0, + client: &http.Client{}, + url: url, + token: token, + fileBackend: *fileBackend, + startFrom: 0, + refreshTokenFn: refreshTokenFn, } httpBackend.startPushingLogs() @@ -113,12 +115,30 @@ func (l *HTTPBackend) pushLogs() error { return err } - if response.StatusCode != 200 { + switch response.StatusCode { + + // Everything went fine, + // just update the index and move on. + case http.StatusOK: + l.startFrom = nextStartFrom + return nil + + // The token issued for the agent expired. + // Try to refresh the token and try again. + // Here, we only update the token, and we let the caller do the retrying. + case http.StatusUnauthorized: + newToken, err := l.refreshTokenFn() + if err != nil { + return err + } + + l.token = newToken return fmt.Errorf("request to %s failed: %s", url, response.Status) - } - l.startFrom = nextStartFrom - return nil + // something else went wrong + default: + return fmt.Errorf("request to %s failed: %s", url, response.Status) + } } func (l *HTTPBackend) Close() error { diff --git a/pkg/eventlogger/httpbackend_test.go b/pkg/eventlogger/httpbackend_test.go index e54aa621..e71287ca 100644 --- a/pkg/eventlogger/httpbackend_test.go +++ b/pkg/eventlogger/httpbackend_test.go @@ -12,7 +12,7 @@ func Test__LogsArePushedToHTTPEndpoint(t *testing.T) { mockServer := testsupport.NewLoghubMockServer() mockServer.Init() - httpBackend, err := NewHTTPBackend(mockServer.URL(), "token") + httpBackend, err := NewHTTPBackend(mockServer.URL(), "token", func() (string, error) { return "", nil }) assert.Nil(t, err) assert.Nil(t, httpBackend.Open()) @@ -54,3 +54,56 @@ func Test__LogsArePushedToHTTPEndpoint(t *testing.T) { mockServer.Close() } + +func Test__TokenIsRefreshed(t *testing.T) { + mockServer := testsupport.NewLoghubMockServer() + mockServer.Init() + + tokenWasRefreshed := false + + httpBackend, err := NewHTTPBackend(mockServer.URL(), testsupport.ExpiredLogToken, func() (string, error) { + tokenWasRefreshed = true + return "some-new-and-shiny-valid-token", nil + }) + + assert.Nil(t, err) + assert.Nil(t, httpBackend.Open()) + + timestamp := int(time.Now().Unix()) + assert.Nil(t, httpBackend.Write(&JobStartedEvent{Timestamp: timestamp, Event: "job_started"})) + assert.Nil(t, httpBackend.Write(&CommandStartedEvent{Timestamp: timestamp, Event: "cmd_started", Directive: "echo hello"})) + assert.Nil(t, httpBackend.Write(&CommandOutputEvent{Timestamp: timestamp, Event: "cmd_output", Output: "hello\n"})) + assert.Nil(t, httpBackend.Write(&CommandFinishedEvent{ + Timestamp: timestamp, + Event: "cmd_finished", + Directive: "echo hello", + ExitCode: 0, + StartedAt: timestamp, + FinishedAt: timestamp, + })) + assert.Nil(t, httpBackend.Write(&JobFinishedEvent{Timestamp: timestamp, Event: "job_finished", Result: "passed"})) + + // Wait until everything is pushed + time.Sleep(2 * time.Second) + + _ = httpBackend.Close() + assert.True(t, tokenWasRefreshed) + + eventObjects, err := TransformToObjects(mockServer.GetLogs()) + assert.Nil(t, err) + + simplifiedEvents, err := SimplifyLogEvents(eventObjects, true) + assert.Nil(t, err) + + assert.Equal(t, []string{ + "job_started", + + "directive: echo hello", + "hello\n", + "Exit Code: 0", + + "job_finished: passed", + }, simplifiedEvents) + + mockServer.Close() +} diff --git a/pkg/jobs/job.go b/pkg/jobs/job.go index 17462075..22c49f50 100644 --- a/pkg/jobs/job.go +++ b/pkg/jobs/job.go @@ -40,6 +40,7 @@ type JobOptions struct { FileInjections []config.FileInjection FailOnMissingFiles bool SelfHosted bool + RefreshTokenFn func() (string, error) } func NewJob(request *api.JobRequest, client *http.Client) (*Job, error) { @@ -50,6 +51,7 @@ func NewJob(request *api.JobRequest, client *http.Client) (*Job, error) { FileInjections: []config.FileInjection{}, FailOnMissingFiles: false, SelfHosted: false, + RefreshTokenFn: nil, }) } @@ -76,7 +78,7 @@ func NewJobWithOptions(options *JobOptions) (*Job, error) { if options.Logger != nil { job.Logger = options.Logger } else { - l, err := eventlogger.CreateLogger(options.Request) + l, err := eventlogger.CreateLogger(options.Request, options.RefreshTokenFn) if err != nil { return nil, err } diff --git a/pkg/listener/job_processor.go b/pkg/listener/job_processor.go index 99377546..d5d14b29 100644 --- a/pkg/listener/job_processor.go +++ b/pkg/listener/job_processor.go @@ -182,6 +182,9 @@ func (p *JobProcessor) RunJob(jobID string) { FileInjections: p.FileInjections, FailOnMissingFiles: p.FailOnMissingFiles, SelfHosted: true, + RefreshTokenFn: func() (string, error) { + return p.APIClient.RefreshToken() + }, }) if err != nil { diff --git a/pkg/listener/listener_test.go b/pkg/listener/listener_test.go index 9184d470..dc555a52 100644 --- a/pkg/listener/listener_test.go +++ b/pkg/listener/listener_test.go @@ -533,6 +533,81 @@ func Test__HostEnvVarsAreExposedToJob(t *testing.T) { loghubMockServer.Close() } +func Test__LogTokenIsRefreshed(t *testing.T) { + testsupport.SetupTestLogs() + + loghubMockServer := testsupport.NewLoghubMockServer() + loghubMockServer.Init() + + hubMockServer := testsupport.NewHubMockServer() + hubMockServer.Init() + hubMockServer.UseLogsURL(loghubMockServer.URL()) + + config := Config{ + ExitOnShutdown: false, + Endpoint: hubMockServer.Host(), + Token: "token", + RegisterRetryLimit: 5, + Scheme: "http", + EnvVars: []config.HostEnvVar{}, + FileInjections: []config.FileInjection{}, + AgentVersion: "0.0.7", + } + + listener, err := Start(http.DefaultClient, config) + assert.Nil(t, err) + assert.False(t, hubMockServer.TokenIsRefreshed) + + hubMockServer.AssignJob(&api.JobRequest{ + ID: "Test__LogTokenIsRefreshed", + Commands: []api.Command{ + {Directive: testsupport.Output("hello")}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + URL: loghubMockServer.URL(), + Token: testsupport.ExpiredLogToken, + }, + }) + + assert.Nil(t, hubMockServer.WaitUntilFinishedJob(12, 5*time.Second)) + assert.True(t, hubMockServer.TokenIsRefreshed) + + eventObjects, err := eventlogger.TransformToObjects(loghubMockServer.GetLogs()) + assert.Nil(t, err) + + simplifiedEvents, err := eventlogger.SimplifyLogEvents(eventObjects, true) + assert.Nil(t, err) + + assert.Equal(t, []string{ + "job_started", + + "directive: Exporting environment variables", + "Exit Code: 0", + + "directive: Injecting Files", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.Output("hello")), + "hello", + "Exit Code: 0", + + "directive: Exporting environment variables", + "Exporting SEMAPHORE_JOB_RESULT\n", + "Exit Code: 0", + + "job_finished: passed", + }, simplifiedEvents) + + listener.Stop() + hubMockServer.Close() + loghubMockServer.Close() +} + func Test__GetJobIsRetried(t *testing.T) { testsupport.SetupTestLogs() diff --git a/pkg/listener/selfhostedapi/refresh_token.go b/pkg/listener/selfhostedapi/refresh_token.go new file mode 100644 index 00000000..f96e32b6 --- /dev/null +++ b/pkg/listener/selfhostedapi/refresh_token.go @@ -0,0 +1,53 @@ +package selfhostedapi + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + log "github.com/sirupsen/logrus" +) + +type RefreshTokenResponse struct { + Token string `json:"token"` +} + +func (a *API) RefreshTokenPath() string { + return a.BasePath() + "/refresh" +} + +func (a *API) RefreshToken() (string, error) { + r, err := http.NewRequest("POST", a.RefreshTokenPath(), nil) + if err != nil { + return "", err + } + + log.Info("Refreshing token for current job logs...") + + a.authorize(r, a.AccessToken) + + resp, err := a.client.Do(r) + if err != nil { + return "", err + } + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to refresh token, got HTTP %d", resp.StatusCode) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + defer resp.Body.Close() + + response := &RefreshTokenResponse{} + if err := json.Unmarshal(body, response); err != nil { + return "", err + } + + log.Infof("Successfully refreshed token for current job logs.") + return response.Token, nil +} diff --git a/pkg/server/server.go b/pkg/server/server.go index 1007c91c..64d9ace0 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -211,6 +211,7 @@ func (s *Server) Run(w http.ResponseWriter, r *http.Request) { ExposeKvmDevice: true, FileInjections: []config.FileInjection{}, SelfHosted: false, + RefreshTokenFn: nil, }) if err != nil { diff --git a/test/support/hub.go b/test/support/hub.go index 9882cb4c..354e750e 100644 --- a/test/support/hub.go +++ b/test/support/hub.go @@ -28,6 +28,7 @@ type HubMockServer struct { Disconnected bool RunningJob bool FinishedJob bool + TokenIsRefreshed bool FailureStatus string } @@ -50,12 +51,31 @@ func (m *HubMockServer) Init() { w.WriteHeader(200) case strings.Contains(path, "/jobs/"): m.handleGetJobRequest(w, r) + case strings.Contains(path, "/refresh"): + m.handleRefreshRequest(w, r) } })) m.Server = mockServer } +func (m *HubMockServer) handleRefreshRequest(w http.ResponseWriter, r *http.Request) { + fmt.Printf("[HUB MOCK] Received refresh request.\n") + refreshTokenResponse := &selfhostedapi.RefreshTokenResponse{ + Token: "new-token", + } + + response, err := json.Marshal(refreshTokenResponse) + if err != nil { + fmt.Printf("[HUB MOCK] Error marshaling refresh response: %v\n", err) + w.WriteHeader(500) + return + } + + m.TokenIsRefreshed = true + _, _ = w.Write(response) +} + func (m *HubMockServer) handleRegisterRequest(w http.ResponseWriter, r *http.Request) { m.RegisterAttempts++ if m.RegisterAttempts < m.RegisterAttemptRejections { diff --git a/test/support/loghub.go b/test/support/loghub.go index 83f782b2..6f658a35 100644 --- a/test/support/loghub.go +++ b/test/support/loghub.go @@ -8,6 +8,8 @@ import ( "strings" ) +const ExpiredLogToken = "expired-token" + type LoghubMockServer struct { Logs []string Server *httptest.Server @@ -21,25 +23,48 @@ func NewLoghubMockServer() *LoghubMockServer { } func (m *LoghubMockServer) Init() { - mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodPost { - fmt.Println("[LOGHUB MOCK] Received logs") - body, err := ioutil.ReadAll(r.Body) - if err != nil { - fmt.Printf("Error reading body: %v\n", err) - } - - logs := strings.Split(string(body), "\n") - m.Logs = append(m.Logs, FilterEmpty(logs)...) - w.WriteHeader(200) - } else { - fmt.Println("NOPE") - } - })) - + mockServer := httptest.NewServer(http.HandlerFunc(m.handler)) m.Server = mockServer } +func (m *LoghubMockServer) handler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + // just an easy way to mock the expired token scenario + token, err := m.findToken(r) + if err != nil || token == ExpiredLogToken { + w.WriteHeader(http.StatusUnauthorized) + return + } + + fmt.Println("[LOGHUB MOCK] Received logs") + body, err := ioutil.ReadAll(r.Body) + if err != nil { + fmt.Printf("Error reading body: %v\n", err) + } + + logs := strings.Split(string(body), "\n") + m.Logs = append(m.Logs, FilterEmpty(logs)...) + w.WriteHeader(200) +} + +func (m *LoghubMockServer) findToken(r *http.Request) (string, error) { + reqToken := r.Header.Get("Authorization") + if reqToken == "" { + return "", fmt.Errorf("no token found") + } + + splitToken := strings.Split(reqToken, "Bearer ") + if len(splitToken) != 2 { + return "", fmt.Errorf("malformed token") + } + + return splitToken[1], nil +} + func (m *LoghubMockServer) GetLogs() []string { return m.Logs } From 4ca77aa806c2abc63d7de7c59e172fa89f568400 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Mon, 11 Jul 2022 15:48:33 -0300 Subject: [PATCH 024/130] build: update goreleaser config (#155) --- .goreleaser.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index af70a2e2..f058dbe6 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -27,7 +27,6 @@ builds: - 386 - amd64 - arm - - arm64 archives: - id: non-windows-archive From 46b4475f948cf5ddc2f68da677cbb564bbc6057d Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Fri, 15 Jul 2022 08:41:47 -0300 Subject: [PATCH 025/130] fix: limit the amount of logs per request (#156) --- pkg/eventlogger/default.go | 9 +- pkg/eventlogger/filebackend.go | 29 ++-- pkg/eventlogger/httpbackend.go | 214 ++++++++++++++++++------- pkg/eventlogger/httpbackend_test.go | 239 +++++++++++++++++++++++----- pkg/jobs/job.go | 18 ++- pkg/listener/job_processor.go | 30 ++-- pkg/listener/listener.go | 21 ++- pkg/retry/retry.go | 33 +++- pkg/retry/retry_test.go | 24 ++- pkg/server/server.go | 3 +- test/support/hub.go | 85 ++++++---- test/support/loghub.go | 33 +++- 12 files changed, 558 insertions(+), 180 deletions(-) diff --git a/pkg/eventlogger/default.go b/pkg/eventlogger/default.go index 4191d20a..dfa82707 100644 --- a/pkg/eventlogger/default.go +++ b/pkg/eventlogger/default.go @@ -53,7 +53,14 @@ func DefaultHTTP(request *api.JobRequest, refreshTokenFn func() (string, error)) return nil, errors.New("HTTP logger needs a refresh token function") } - backend, err := NewHTTPBackend(request.Logger.URL, request.Logger.Token, refreshTokenFn) + backend, err := NewHTTPBackend(HTTPBackendConfig{ + URL: request.Logger.URL, + Token: request.Logger.Token, + RefreshTokenFn: refreshTokenFn, + LinesPerRequest: MaxLinesPerRequest, + FlushTimeoutInSeconds: DefaultFlushTimeoutInSeconds, + }) + if err != nil { return nil, err } diff --git a/pkg/eventlogger/filebackend.go b/pkg/eventlogger/filebackend.go index 3f60717e..4067d124 100644 --- a/pkg/eventlogger/filebackend.go +++ b/pkg/eventlogger/filebackend.go @@ -67,34 +67,43 @@ func (l *FileBackend) Close() error { return nil } -func (l *FileBackend) Stream(startLine int, writer io.Writer) (int, error) { +func (l *FileBackend) Stream(startingLineNumber, maxLines int, writer io.Writer) (int, error) { fd, err := os.OpenFile(l.path, os.O_RDONLY, os.ModePerm) if err != nil { - return startLine, err + return startingLineNumber, err } reader := bufio.NewReader(fd) - lineIndex := 0 + lineNumber := 0 + linesStreamed := 0 for { line, err := reader.ReadString('\n') if err != nil { if err != io.EOF { _ = fd.Close() - return lineIndex, err + return lineNumber, err } break } - if lineIndex < startLine { - lineIndex++ + // If current line is before the starting line we are after, we just skip it. + if lineNumber < startingLineNumber { + lineNumber++ continue - } else { - lineIndex++ - fmt.Fprintln(writer, line) + } + + // Otherwise, we advance to the next line and stream the current line. + lineNumber++ + fmt.Fprintln(writer, line) + linesStreamed++ + + // if we have streamed the number of lines we want, we stop. + if linesStreamed == maxLines { + break } } - return lineIndex, fd.Close() + return lineNumber, fd.Close() } diff --git a/pkg/eventlogger/httpbackend.go b/pkg/eventlogger/httpbackend.go index 9d252567..a04d08db 100644 --- a/pkg/eventlogger/httpbackend.go +++ b/pkg/eventlogger/httpbackend.go @@ -2,29 +2,50 @@ package eventlogger import ( "bytes" + "errors" "fmt" + "math/rand" "net/http" "os" "path/filepath" - "sync" "time" "github.com/semaphoreci/agent/pkg/retry" log "github.com/sirupsen/logrus" ) +const ( + MaxLinesPerRequest = 2000 + MaxFlushTimeoutInSeconds = 900 + DefaultFlushTimeoutInSeconds = 60 +) + type HTTPBackend struct { - client *http.Client - url string - token string - fileBackend FileBackend - startFrom int - streamChan chan bool - pushLock sync.Mutex - refreshTokenFn func() (string, error) + client *http.Client + fileBackend FileBackend + startFrom int + config HTTPBackendConfig + stop bool + flush bool +} + +type HTTPBackendConfig struct { + URL string + Token string + LinesPerRequest int + FlushTimeoutInSeconds int + RefreshTokenFn func() (string, error) } -func NewHTTPBackend(url, token string, refreshTokenFn func() (string, error)) (*HTTPBackend, error) { +func NewHTTPBackend(config HTTPBackendConfig) (*HTTPBackend, error) { + if config.LinesPerRequest <= 0 || config.LinesPerRequest > MaxLinesPerRequest { + return nil, fmt.Errorf("config.LinesPerRequest must be between 1 and %d", MaxLinesPerRequest) + } + + if config.FlushTimeoutInSeconds <= 0 || config.FlushTimeoutInSeconds > MaxFlushTimeoutInSeconds { + return nil, fmt.Errorf("config.FlushTimeoutInSeconds must be between 1 and %d", MaxFlushTimeoutInSeconds) + } + path := filepath.Join(os.TempDir(), fmt.Sprintf("job_log_%d.json", time.Now().UnixNano())) fileBackend, err := NewFileBackend(path) if err != nil { @@ -32,15 +53,13 @@ func NewHTTPBackend(url, token string, refreshTokenFn func() (string, error)) (* } httpBackend := HTTPBackend{ - client: &http.Client{}, - url: url, - token: token, - fileBackend: *fileBackend, - startFrom: 0, - refreshTokenFn: refreshTokenFn, + client: &http.Client{}, + fileBackend: *fileBackend, + startFrom: 0, + config: config, } - httpBackend.startPushingLogs() + go httpBackend.push() return &httpBackend, nil } @@ -53,63 +72,108 @@ func (l *HTTPBackend) Write(event interface{}) error { return l.fileBackend.Write(event) } -func (l *HTTPBackend) startPushingLogs() { - log.Debugf("Logs will be pushed to %s", l.url) - - ticker := time.NewTicker(time.Second) - l.streamChan = make(chan bool) - - go func() { - for { - select { - case <-ticker.C: - err := l.pushLogs() - if err != nil { - log.Errorf("Error pushing logs: %v", err) - // we don't retry the request here because a new one will happen in 1s, - // so we only retry these requests on Close() - } - case <-l.streamChan: - ticker.Stop() - return - } +func (l *HTTPBackend) push() { + log.Infof("Logs will be pushed to %s", l.config.URL) + + for { + + /* + * Check if streaming is necessary. There are three cases where it isn't necessary anymore: + * 1. The job has exhausted the amount of log space it has available. + * The API will reject all subsequent attempts, so we just stop trying. + * 2. The job is finished and all the logs were already pushed. + * 3. The job is finished, not all logs were pushed, but we gave up because it was taking too long. + */ + if l.stop { + break } - }() -} -func (l *HTTPBackend) stopStreaming() { - if l.streamChan != nil { - close(l.streamChan) + /* + * Wait for the appropriate amount of time + * before trying to send the next batch of logs. + */ + delay := l.delay() + log.Infof("Waiting %v to push next batch of logs...", delay) + time.Sleep(delay) + + /* + * Send the next batch of logs. + * If an error occurs, it will be retried in the next tick, + * so there is no need to retry requests that failed here. + */ + err := l.newRequest() + if err != nil { + log.Errorf("Error pushing logs: %v", err) + } } - log.Debug("Stopped streaming logs") + log.Info("Stopped pushing logs.") } -func (l *HTTPBackend) pushLogs() error { - l.pushLock.Lock() - defer l.pushLock.Unlock() +/* + * The delay between log requests. + * Note that this isn't a rate, + * but a delay between the end of one request and the start of the next one. + */ +func (l *HTTPBackend) delay() time.Duration { + + /* + * if we are flushing, + * we use a tighter range of 500ms - 1000ms. + */ + if l.flush { + min := 500 + max := 1000 + + // #nosec + interval := rand.Intn(max-min) + min + return time.Duration(interval) * time.Millisecond + } + + /* + * if we are not flushing, + * we use a wider range of 1500ms - 3000ms. + */ + min := 1500 + max := 3000 + // #nosec + interval := rand.Intn(max-min) + min + return time.Duration(interval) * time.Millisecond +} + +func (l *HTTPBackend) newRequest() error { buffer := bytes.NewBuffer([]byte{}) - nextStartFrom, err := l.fileBackend.Stream(l.startFrom, buffer) + nextStartFrom, err := l.fileBackend.Stream(l.startFrom, l.config.LinesPerRequest, buffer) if err != nil { return err } + /* + * If no more logs are found, we may be in two scenarios: + * 1. The job is not done, so more logs might be generated. We just skip until there is some new logs. + * 2. The job is done, so no more logs will be generated. We stop pushing altogether. + */ if l.startFrom == nextStartFrom { - log.Debugf("No logs to push - skipping") - // no logs to stream + if l.flush { + log.Infof("No more logs to flush - stopping") + l.stop = true + return nil + } + + log.Infof("No logs to push - skipping") return nil } - url := fmt.Sprintf("%s?start_from=%d", l.url, l.startFrom) - log.Debugf("Pushing logs to %s", url) + log.Infof("Pushing next batch of logs with %d log events...", (nextStartFrom - l.startFrom)) + url := fmt.Sprintf("%s?start_from=%d", l.config.URL, l.startFrom) request, err := http.NewRequest("POST", url, buffer) if err != nil { return err } request.Header.Set("Content-Type", "text/plain") - request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", l.token)) + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", l.config.Token)) response, err := l.client.Do(request) if err != nil { return err @@ -123,16 +187,22 @@ func (l *HTTPBackend) pushLogs() error { l.startFrom = nextStartFrom return nil + // No more space is available for this job's logs. + // The API will keep rejecting the requests if we keep sending them, so just stop. + case http.StatusUnprocessableEntity: + l.stop = true + return errors.New("no more space available for logs - stopping") + // The token issued for the agent expired. // Try to refresh the token and try again. // Here, we only update the token, and we let the caller do the retrying. case http.StatusUnauthorized: - newToken, err := l.refreshTokenFn() + newToken, err := l.config.RefreshTokenFn() if err != nil { return err } - l.token = newToken + l.config.Token = newToken return fmt.Errorf("request to %s failed: %s", url, response.Status) // something else went wrong @@ -142,17 +212,41 @@ func (l *HTTPBackend) pushLogs() error { } func (l *HTTPBackend) Close() error { - l.stopStreaming() - err := retry.RetryWithConstantWait("Push logs", 5, time.Second, func() error { - return l.pushLogs() + /* + * If we have already stopped pushing logs + * due to no more space available, we just proceed. + */ + if l.stop { + return l.fileBackend.Close() + } + + /* + * If not, we try to flush all the remaining logs. + * We wait for them to be flushed for a period of time (60s). + * If they are not yet completely flushed after that period of time, we give up. + */ + l.flush = true + + log.Printf("Waiting for all logs to be flushed...") + err := retry.RetryWithConstantWait(retry.RetryOptions{ + Task: "wait for logs to be flushed", + MaxAttempts: l.config.FlushTimeoutInSeconds, + DelayBetweenAttempts: time.Second, + HideError: true, + Fn: func() error { + if l.stop { + return nil + } + + return fmt.Errorf("not fully flushed") + }, }) if err != nil { - log.Errorf("Could not push all logs to %s: %v", l.url, err) - } else { - log.Infof("All logs successfully pushed to %s", l.url) + log.Errorf("Could not push all logs to %s - giving up", l.config.URL) } + l.stop = true return l.fileBackend.Close() } diff --git a/pkg/eventlogger/httpbackend_test.go b/pkg/eventlogger/httpbackend_test.go index e71287ca..8943615c 100644 --- a/pkg/eventlogger/httpbackend_test.go +++ b/pkg/eventlogger/httpbackend_test.go @@ -8,30 +8,97 @@ import ( "github.com/stretchr/testify/assert" ) +func Test__ArgumentsMustBeValid(t *testing.T) { + t.Run("linesPerRequest cannot be unspecified or 0", func(t *testing.T) { + backend, err := NewHTTPBackend(HTTPBackendConfig{ + URL: "whatever", + Token: "token", + RefreshTokenFn: func() (string, error) { return "", nil }, + }) + + assert.Nil(t, backend) + assert.ErrorContains(t, err, "must be between 1 and 2000") + }) + + t.Run("linesPerRequest cannot be negative", func(t *testing.T) { + backend, err := NewHTTPBackend(HTTPBackendConfig{ + URL: "whatever", + Token: "token", + RefreshTokenFn: func() (string, error) { return "", nil }, + LinesPerRequest: -1, + }) + + assert.Nil(t, backend) + assert.ErrorContains(t, err, "must be between 1 and 2000") + }) + + t.Run("linesPerRequest cannot be above the maximum allowed", func(t *testing.T) { + backend, err := NewHTTPBackend(HTTPBackendConfig{ + URL: "whatever", + Token: "token", + RefreshTokenFn: func() (string, error) { return "", nil }, + LinesPerRequest: 10000, + }) + + assert.Nil(t, backend) + assert.ErrorContains(t, err, "must be between 1 and 2000") + }) + + t.Run("FlushTimeoutInSeconds cannot be unspecified or 0", func(t *testing.T) { + backend, err := NewHTTPBackend(HTTPBackendConfig{ + URL: "whatever", + Token: "token", + LinesPerRequest: MaxLinesPerRequest, + RefreshTokenFn: func() (string, error) { return "", nil }, + }) + + assert.Nil(t, backend) + assert.ErrorContains(t, err, "must be between 1 and 900") + }) + + t.Run("FlushTimeoutInSeconds cannot be negative", func(t *testing.T) { + backend, err := NewHTTPBackend(HTTPBackendConfig{ + URL: "whatever", + Token: "token", + LinesPerRequest: MaxLinesPerRequest, + FlushTimeoutInSeconds: -1, + RefreshTokenFn: func() (string, error) { return "", nil }, + }) + + assert.Nil(t, backend) + assert.ErrorContains(t, err, "must be between 1 and 900") + }) + + t.Run("FlushTimeoutInSeconds cannot be above the maximum allowed", func(t *testing.T) { + backend, err := NewHTTPBackend(HTTPBackendConfig{ + URL: "whatever", + Token: "token", + RefreshTokenFn: func() (string, error) { return "", nil }, + LinesPerRequest: MaxLinesPerRequest, + FlushTimeoutInSeconds: 1000000, + }) + + assert.Nil(t, backend) + assert.ErrorContains(t, err, "must be between 1 and 900") + }) +} + func Test__LogsArePushedToHTTPEndpoint(t *testing.T) { mockServer := testsupport.NewLoghubMockServer() mockServer.Init() - httpBackend, err := NewHTTPBackend(mockServer.URL(), "token", func() (string, error) { return "", nil }) + httpBackend, err := NewHTTPBackend(HTTPBackendConfig{ + URL: mockServer.URL(), + Token: "token", + RefreshTokenFn: func() (string, error) { return "", nil }, + LinesPerRequest: 20, + FlushTimeoutInSeconds: DefaultFlushTimeoutInSeconds, + }) + assert.Nil(t, err) assert.Nil(t, httpBackend.Open()) - timestamp := int(time.Now().Unix()) - assert.Nil(t, httpBackend.Write(&JobStartedEvent{Timestamp: timestamp, Event: "job_started"})) - assert.Nil(t, httpBackend.Write(&CommandStartedEvent{Timestamp: timestamp, Event: "cmd_started", Directive: "echo hello"})) - assert.Nil(t, httpBackend.Write(&CommandOutputEvent{Timestamp: timestamp, Event: "cmd_output", Output: "hello\n"})) - assert.Nil(t, httpBackend.Write(&CommandFinishedEvent{ - Timestamp: timestamp, - Event: "cmd_finished", - Directive: "echo hello", - ExitCode: 0, - StartedAt: timestamp, - FinishedAt: timestamp, - })) - assert.Nil(t, httpBackend.Write(&JobFinishedEvent{Timestamp: timestamp, Event: "job_finished", Result: "passed"})) - - // Wait until everything is pushed - time.Sleep(2 * time.Second) + generateLogEvents(t, 1, httpBackend) err = httpBackend.Close() assert.Nil(t, err) @@ -55,37 +122,111 @@ func Test__LogsArePushedToHTTPEndpoint(t *testing.T) { mockServer.Close() } -func Test__TokenIsRefreshed(t *testing.T) { +func Test__RequestsAreCappedAtLinesPerRequest(t *testing.T) { mockServer := testsupport.NewLoghubMockServer() mockServer.Init() - tokenWasRefreshed := false + httpBackend, err := NewHTTPBackend(HTTPBackendConfig{ + URL: mockServer.URL(), + Token: "token", + RefreshTokenFn: func() (string, error) { return "", nil }, + LinesPerRequest: 2, + FlushTimeoutInSeconds: DefaultFlushTimeoutInSeconds, + }) + + assert.Nil(t, err) + assert.Nil(t, httpBackend.Open()) + + generateLogEvents(t, 10, httpBackend) + _ = httpBackend.Close() + + // assert no more than 2 events were sent per batch + for _, batchSize := range mockServer.GetBatchSizesUsed() { + assert.LessOrEqual(t, batchSize, 2) + } + + eventObjects, err := TransformToObjects(mockServer.GetLogs()) + assert.Nil(t, err) + + simplifiedEvents, err := SimplifyLogEvents(eventObjects, true) + assert.Nil(t, err) + + assert.Equal(t, []string{ + "job_started", + + "directive: echo hello", + "hello\n", + "hello\n", + "hello\n", + "hello\n", + "hello\n", + "hello\n", + "hello\n", + "hello\n", + "hello\n", + "hello\n", + "Exit Code: 0", - httpBackend, err := NewHTTPBackend(mockServer.URL(), testsupport.ExpiredLogToken, func() (string, error) { - tokenWasRefreshed = true - return "some-new-and-shiny-valid-token", nil + "job_finished: passed", + }, simplifiedEvents) + + mockServer.Close() +} + +func Test__FlushingGivesUpAfterTimeout(t *testing.T) { + mockServer := testsupport.NewLoghubMockServer() + mockServer.Init() + + httpBackend, err := NewHTTPBackend(HTTPBackendConfig{ + URL: mockServer.URL(), + Token: "token", + RefreshTokenFn: func() (string, error) { return "", nil }, + LinesPerRequest: 2, + FlushTimeoutInSeconds: 10, }) assert.Nil(t, err) assert.Nil(t, httpBackend.Open()) - timestamp := int(time.Now().Unix()) - assert.Nil(t, httpBackend.Write(&JobStartedEvent{Timestamp: timestamp, Event: "job_started"})) - assert.Nil(t, httpBackend.Write(&CommandStartedEvent{Timestamp: timestamp, Event: "cmd_started", Directive: "echo hello"})) - assert.Nil(t, httpBackend.Write(&CommandOutputEvent{Timestamp: timestamp, Event: "cmd_output", Output: "hello\n"})) - assert.Nil(t, httpBackend.Write(&CommandFinishedEvent{ - Timestamp: timestamp, - Event: "cmd_finished", - Directive: "echo hello", - ExitCode: 0, - StartedAt: timestamp, - FinishedAt: timestamp, - })) - assert.Nil(t, httpBackend.Write(&JobFinishedEvent{Timestamp: timestamp, Event: "job_finished", Result: "passed"})) + // 1000+ log events at 2 per request + // would take more time to flush everything that the timeout we give it. + generateLogEvents(t, 1000, httpBackend) + + _ = httpBackend.Close() + + eventObjects, err := TransformToObjects(mockServer.GetLogs()) + assert.Nil(t, err) + + simplifiedEvents, err := SimplifyLogEvents(eventObjects, true) + assert.Nil(t, err) - // Wait until everything is pushed - time.Sleep(2 * time.Second) + // logs are incomplete + assert.NotContains(t, simplifiedEvents, "job_finished: passed") + mockServer.Close() +} + +func Test__TokenIsRefreshed(t *testing.T) { + mockServer := testsupport.NewLoghubMockServer() + mockServer.Init() + + tokenWasRefreshed := false + + httpBackend, err := NewHTTPBackend(HTTPBackendConfig{ + URL: mockServer.URL(), + Token: testsupport.ExpiredLogToken, + LinesPerRequest: 20, + FlushTimeoutInSeconds: DefaultFlushTimeoutInSeconds, + RefreshTokenFn: func() (string, error) { + tokenWasRefreshed = true + return "some-new-and-shiny-valid-token", nil + }, + }) + + assert.Nil(t, err) + assert.Nil(t, httpBackend.Open()) + + generateLogEvents(t, 1, httpBackend) _ = httpBackend.Close() assert.True(t, tokenWasRefreshed) @@ -107,3 +248,27 @@ func Test__TokenIsRefreshed(t *testing.T) { mockServer.Close() } + +func generateLogEvents(t *testing.T, outputEventsCount int, backend *HTTPBackend) { + timestamp := int(time.Now().Unix()) + + assert.Nil(t, backend.Write(&JobStartedEvent{Timestamp: timestamp, Event: "job_started"})) + assert.Nil(t, backend.Write(&CommandStartedEvent{Timestamp: timestamp, Event: "cmd_started", Directive: "echo hello"})) + + count := outputEventsCount + for count > 0 { + assert.Nil(t, backend.Write(&CommandOutputEvent{Timestamp: timestamp, Event: "cmd_output", Output: "hello\n"})) + count-- + } + + assert.Nil(t, backend.Write(&CommandFinishedEvent{ + Timestamp: timestamp, + Event: "cmd_finished", + Directive: "echo hello", + ExitCode: 0, + StartedAt: timestamp, + FinishedAt: timestamp, + })) + + assert.Nil(t, backend.Write(&JobFinishedEvent{Timestamp: timestamp, Event: "job_finished", Result: "passed"})) +} diff --git a/pkg/jobs/job.go b/pkg/jobs/job.go index 22c49f50..6b515eb8 100644 --- a/pkg/jobs/job.go +++ b/pkg/jobs/job.go @@ -327,15 +327,25 @@ func (job *Job) Stop() { func (job *Job) SendFinishedCallback(result string, retries int) error { payload := fmt.Sprintf(`{"result": "%s"}`, result) log.Infof("Sending finished callback: %+v", payload) - return retry.RetryWithConstantWait("Send finished callback", retries, time.Second, func() error { - return job.SendCallback(job.Request.Callbacks.Finished, payload) + return retry.RetryWithConstantWait(retry.RetryOptions{ + Task: "Send finished callback", + MaxAttempts: retries, + DelayBetweenAttempts: time.Second, + Fn: func() error { + return job.SendCallback(job.Request.Callbacks.Finished, payload) + }, }) } func (job *Job) SendTeardownFinishedCallback(retries int) error { log.Info("Sending teardown finished callback") - return retry.RetryWithConstantWait("Send teardown finished callback", retries, time.Second, func() error { - return job.SendCallback(job.Request.Callbacks.TeardownFinished, "{}") + return retry.RetryWithConstantWait(retry.RetryOptions{ + Task: "Send teardown finished callback", + MaxAttempts: retries, + DelayBetweenAttempts: time.Second, + Fn: func() error { + return job.SendCallback(job.Request.Callbacks.TeardownFinished, "{}") + }, }) } diff --git a/pkg/listener/job_processor.go b/pkg/listener/job_processor.go index d5d14b29..43cd1bba 100644 --- a/pkg/listener/job_processor.go +++ b/pkg/listener/job_processor.go @@ -213,14 +213,19 @@ func (p *JobProcessor) RunJob(jobID string) { func (p *JobProcessor) getJobWithRetries(jobID string) (*api.JobRequest, error) { var jobRequest *api.JobRequest - err := retry.RetryWithConstantWait("Get job", p.GetJobRetryAttempts, 3*time.Second, func() error { - job, err := p.APIClient.GetJob(jobID) - if err != nil { - return err - } + err := retry.RetryWithConstantWait(retry.RetryOptions{ + Task: "Get job", + MaxAttempts: p.GetJobRetryAttempts, + DelayBetweenAttempts: 3 * time.Second, + Fn: func() error { + job, err := p.APIClient.GetJob(jobID) + if err != nil { + return err + } - jobRequest = job - return nil + jobRequest = job + return nil + }, }) return jobRequest, err @@ -261,9 +266,14 @@ func (p *JobProcessor) disconnect() { p.StopSync = true log.Info("Disconnecting the Agent from Semaphore") - err := retry.RetryWithConstantWait("Disconnect", p.DisconnectRetryAttempts, time.Second, func() error { - _, err := p.APIClient.Disconnect() - return err + err := retry.RetryWithConstantWait(retry.RetryOptions{ + Task: "Disconnect", + MaxAttempts: p.DisconnectRetryAttempts, + DelayBetweenAttempts: time.Second, + Fn: func() error { + _, err := p.APIClient.Disconnect() + return err + }, }) if err != nil { diff --git a/pkg/listener/listener.go b/pkg/listener/listener.go index 004a4d3d..b13d514f 100644 --- a/pkg/listener/listener.go +++ b/pkg/listener/listener.go @@ -115,14 +115,19 @@ func (l *Listener) Register() error { Hostname: osinfo.Hostname(), } - err = retry.RetryWithConstantWait("Register", l.Config.RegisterRetryLimit, time.Second, func() error { - resp, err := l.Client.Register(req) - if err != nil { - return err - } - - l.Client.SetAccessToken(resp.Token) - return nil + err = retry.RetryWithConstantWait(retry.RetryOptions{ + Task: "Register", + MaxAttempts: l.Config.RegisterRetryLimit, + DelayBetweenAttempts: time.Second, + Fn: func() error { + resp, err := l.Client.Register(req) + if err != nil { + return err + } + + l.Client.SetAccessToken(resp.Token) + return nil + }, }) if err != nil { diff --git a/pkg/retry/retry.go b/pkg/retry/retry.go index 41fcb71d..69cd86f4 100644 --- a/pkg/retry/retry.go +++ b/pkg/retry/retry.go @@ -7,18 +7,39 @@ import ( log "github.com/sirupsen/logrus" ) -func RetryWithConstantWait(task string, maxAttempts int, wait time.Duration, f func() error) error { +type RetryOptions struct { + Task string + MaxAttempts int + DelayBetweenAttempts time.Duration + Fn func() error + HideError bool +} + +func RetryWithConstantWait(options RetryOptions) error { + if options.Fn == nil { + return fmt.Errorf("options.Fn cannot be nil") + } + for attempt := 1; ; attempt++ { - err := f() + err := options.Fn() if err == nil { return nil } - if attempt >= maxAttempts { - return fmt.Errorf("[%s] failed after [%d] attempts - giving up: %v", task, attempt, err) + if attempt >= options.MaxAttempts { + return fmt.Errorf("[%s] failed after [%d] attempts - giving up: %v", options.Task, attempt, err) + } + + if !options.HideError { + log.Errorf( + "[%s] attempt [%d] failed with [%v] - retrying in %s", + options.Task, + attempt, + err, + options.DelayBetweenAttempts, + ) } - log.Errorf("[%s] attempt [%d] failed with [%v] - retrying in %s", task, attempt, err, wait) - time.Sleep(wait) + time.Sleep(options.DelayBetweenAttempts) } } diff --git a/pkg/retry/retry_test.go b/pkg/retry/retry_test.go index f53b6967..d0a06868 100644 --- a/pkg/retry/retry_test.go +++ b/pkg/retry/retry_test.go @@ -10,20 +10,32 @@ import ( func Test__NoRetriesIfFirstAttemptIsSuccessful(t *testing.T) { attempts := 0 - err := RetryWithConstantWait("test", 5, 100*time.Millisecond, func() error { - attempts++ - return nil + err := RetryWithConstantWait(RetryOptions{ + Task: "test", + MaxAttempts: 5, + DelayBetweenAttempts: 100 * time.Millisecond, + Fn: func() error { + attempts++ + return nil + }, }) + assert.Equal(t, attempts, 1) assert.Nil(t, err) } func Test__GivesUpAfterMaxRetries(t *testing.T) { attempts := 0 - err := RetryWithConstantWait("test", 5, 100*time.Millisecond, func() error { - attempts++ - return errors.New("bad error") + err := RetryWithConstantWait(RetryOptions{ + Task: "test", + MaxAttempts: 5, + DelayBetweenAttempts: 100 * time.Millisecond, + Fn: func() error { + attempts++ + return errors.New("bad error") + }, }) + assert.Equal(t, attempts, 5) assert.NotNil(t, err) } diff --git a/pkg/server/server.go b/pkg/server/server.go index 64d9ace0..a2824313 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "io/ioutil" + "math" "net/http" "os" "path/filepath" @@ -129,7 +130,7 @@ func (s *Server) JobLogs(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, `{"message": "%s"}`, "Failed to open logfile") } - _, err = logFile.Stream(startFromLine, w) + _, err = logFile.Stream(startFromLine, math.MaxInt32, w) if err != nil { log.Errorf("Error while streaming logs: %v", err) diff --git a/test/support/hub.go b/test/support/hub.go index 354e750e..9de1f5d6 100644 --- a/test/support/hub.go +++ b/test/support/hub.go @@ -232,52 +232,77 @@ func (m *HubMockServer) Host() string { } func (m *HubMockServer) WaitUntilFailure(status string, attempts int, wait time.Duration) error { - return retry.RetryWithConstantWait("WaitUntilRunningJob", attempts, wait, func() error { - if m.FailureStatus != status { - return fmt.Errorf("still haven't failed with %s", status) - } - - return nil + return retry.RetryWithConstantWait(retry.RetryOptions{ + Task: "WaitUntilRunningJob", + MaxAttempts: attempts, + DelayBetweenAttempts: wait, + Fn: func() error { + if m.FailureStatus != status { + return fmt.Errorf("still haven't failed with %s", status) + } + + return nil + }, }) } func (m *HubMockServer) WaitUntilRunningJob(attempts int, wait time.Duration) error { - return retry.RetryWithConstantWait("WaitUntilRunningJob", attempts, wait, func() error { - if !m.RunningJob { - return fmt.Errorf("still not running job") - } - - return nil + return retry.RetryWithConstantWait(retry.RetryOptions{ + Task: "WaitUntilRunningJob", + MaxAttempts: attempts, + DelayBetweenAttempts: wait, + Fn: func() error { + if !m.RunningJob { + return fmt.Errorf("still not running job") + } + + return nil + }, }) } func (m *HubMockServer) WaitUntilFinishedJob(attempts int, wait time.Duration) error { - return retry.RetryWithConstantWait("WaitUntilFinishedJob", attempts, wait, func() error { - if !m.FinishedJob { - return fmt.Errorf("still not finished job") - } - - return nil + return retry.RetryWithConstantWait(retry.RetryOptions{ + Task: "WaitUntilFinishedJob", + MaxAttempts: attempts, + DelayBetweenAttempts: wait, + Fn: func() error { + if !m.FinishedJob { + return fmt.Errorf("still not finished job") + } + + return nil + }, }) } func (m *HubMockServer) WaitUntilDisconnected(attempts int, wait time.Duration) error { - return retry.RetryWithConstantWait("WaitUntilDisconnected", attempts, wait, func() error { - if !m.Disconnected { - return fmt.Errorf("still not disconnected") - } - - return nil + return retry.RetryWithConstantWait(retry.RetryOptions{ + Task: "WaitUntilDisconnected", + MaxAttempts: attempts, + DelayBetweenAttempts: wait, + Fn: func() error { + if !m.Disconnected { + return fmt.Errorf("still not disconnected") + } + + return nil + }, }) } func (m *HubMockServer) WaitUntilRegistered() error { - return retry.RetryWithConstantWait("WaitUntilRegistered", 10, time.Second, func() error { - if m.RegisterRequest == nil { - return fmt.Errorf("still not registered") - } - - return nil + return retry.RetryWithConstantWait(retry.RetryOptions{ + Task: "WaitUntilRegistered", + MaxAttempts: 10, + DelayBetweenAttempts: time.Second, + Fn: func() error { + if m.RegisterRequest == nil { + return fmt.Errorf("still not registered") + } + + return nil + }, }) } diff --git a/test/support/loghub.go b/test/support/loghub.go index 6f658a35..b3520e71 100644 --- a/test/support/loghub.go +++ b/test/support/loghub.go @@ -11,9 +11,11 @@ import ( const ExpiredLogToken = "expired-token" type LoghubMockServer struct { - Logs []string - Server *httptest.Server - Handler http.Handler + Logs []string + BatchSizesUsed []int + MaxSizeForLogs int + Server *httptest.Server + Handler http.Handler } func NewLoghubMockServer() *LoghubMockServer { @@ -27,12 +29,22 @@ func (m *LoghubMockServer) Init() { m.Server = mockServer } +func (m *LoghubMockServer) SetMaxSizeForLogs(maxSize int) { + m.MaxSizeForLogs = maxSize +} + func (m *LoghubMockServer) handler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } + // if max size is set, and is big enough, send 422. + if m.MaxSizeForLogs > 0 && len(m.Logs) >= m.MaxSizeForLogs { + w.WriteHeader(http.StatusUnprocessableEntity) + return + } + // just an easy way to mock the expired token scenario token, err := m.findToken(r) if err != nil || token == ExpiredLogToken { @@ -40,14 +52,17 @@ func (m *LoghubMockServer) handler(w http.ResponseWriter, r *http.Request) { return } - fmt.Println("[LOGHUB MOCK] Received logs") body, err := ioutil.ReadAll(r.Body) if err != nil { - fmt.Printf("Error reading body: %v\n", err) + fmt.Printf("[LOGHUB MOCK] Error reading body: %v\n", err) } - logs := strings.Split(string(body), "\n") - m.Logs = append(m.Logs, FilterEmpty(logs)...) + logs := FilterEmpty(strings.Split(string(body), "\n")) + fmt.Printf("[LOGHUB MOCK] Received %d log events\n", len(logs)) + + m.BatchSizesUsed = append(m.BatchSizesUsed, len(logs)) + m.Logs = append(m.Logs, logs...) + w.WriteHeader(200) } @@ -69,6 +84,10 @@ func (m *LoghubMockServer) GetLogs() []string { return m.Logs } +func (m *LoghubMockServer) GetBatchSizesUsed() []int { + return m.BatchSizesUsed +} + func (m *LoghubMockServer) URL() string { return m.Server.URL } From 5c2ad12caa9c31d7b1b24bbe1bf0034fce3e6769 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Wed, 20 Jul 2022 13:03:19 -0300 Subject: [PATCH 026/130] build: update dependencies (#157) --- go.mod | 16 ++++---- go.sum | 122 +++++++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 100 insertions(+), 38 deletions(-) diff --git a/go.mod b/go.mod index 91082b29..86436487 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,19 @@ module github.com/semaphoreci/agent require ( - github.com/creack/pty v1.1.17 + github.com/creack/pty v1.1.18 github.com/felixge/httpsnoop v1.0.2 // indirect - github.com/golang-jwt/jwt/v4 v4.1.0 + github.com/golang-jwt/jwt/v4 v4.4.2 github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 github.com/mitchellh/panicwrap v1.0.0 - github.com/renderedtext/go-watchman v0.0.0-20210809121718-0632d0d12b0a - github.com/sirupsen/logrus v1.8.1 + github.com/renderedtext/go-watchman v0.0.0-20220524201126-042727917d44 + github.com/sirupsen/logrus v1.9.0 github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.11.0 - github.com/stretchr/testify v1.7.1 - golang.org/x/sys v0.0.0-20220412211240-33da011f77ad - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b + github.com/spf13/viper v1.12.0 + github.com/stretchr/testify v1.8.0 + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 + gopkg.in/yaml.v3 v3.0.1 ) go 1.16 diff --git a/go.sum b/go.sum index 3ce53a11..0b9ace7d 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM7 cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= @@ -60,6 +62,7 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -86,14 +89,17 @@ github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XP github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= -github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -102,6 +108,7 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= @@ -109,22 +116,26 @@ github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYF github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= -github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.1.0 h1:XUgk2Ex5veyVFVeLm0xhusUTQybEbexJXrvPNOKkSY0= -github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= +github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -172,6 +183,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -200,11 +213,13 @@ github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pf github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= @@ -237,24 +252,30 @@ github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpT github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -276,8 +297,9 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/panicwrap v1.0.0 h1:67zIyVakCIvcs69A0FGfZjBdPleaonSgGlXRSRlb6fE= github.com/mitchellh/panicwrap v1.0.0/go.mod h1:pKvZHwWrZowLUzftuFq7coarnxbBXU4aQh3N0BJOeeA= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -286,12 +308,13 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= -github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.0-beta.8 h1:dy81yyLYJDwMTifq24Oi/IslOslRrDSb3jwDggjz3Z0= -github.com/pelletier/go-toml/v2 v2.0.0-beta.8/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= +github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -303,58 +326,70 @@ github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSg github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/renderedtext/go-watchman v0.0.0-20210809121718-0632d0d12b0a h1:pRX9qebwT+TMdBojMspqDtU1RFLIbH5VzI8aI9yMiyE= -github.com/renderedtext/go-watchman v0.0.0-20210809121718-0632d0d12b0a/go.mod h1:Z+qanDzSoUGCbcrTM7G6YCA9ST2KBdte7sCz+HQAp7I= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/renderedtext/go-watchman v0.0.0-20220524201126-042727917d44 h1:MahMOODBM7zV5F8ptFnnMw4qWsP96Y+ZmKc1AAAYG/o= +github.com/renderedtext/go-watchman v0.0.0-20220524201126-042727917d44/go.mod h1:Z+qanDzSoUGCbcrTM7G6YCA9ST2KBdte7sCz+HQAp7I= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sagikazarmark/crypt v0.5.0/go.mod h1:l+nzl7KWh51rpzp2h7t4MZWyiEWdhNpOAnclKvg+mdA= +github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= -github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= -github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.11.0 h1:7OX/1FS6n7jHD1zGrZTM7WtY13ZELRyosK4k93oPr44= -github.com/spf13/viper v1.11.0/go.mod h1:djo0X/bA5+tYVoCn+C7cAYJGcVn/qYLFTG8gdUsX7Zk= +github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= +github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= +github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.etcd.io/etcd/api/v3 v3.5.2/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= -go.etcd.io/etcd/client/pkg/v3 v3.5.2/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.2/go.mod h1:2D7ZejHVMIfog1221iLSYlQRzrtECw3kz4I4VAQm3qI= +go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= +go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= +go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -456,6 +491,8 @@ golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -487,6 +524,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -508,6 +546,7 @@ golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -522,6 +561,8 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -529,6 +570,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -541,6 +583,7 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -557,8 +600,11 @@ golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -634,6 +680,7 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -670,6 +717,9 @@ google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQ google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -749,6 +799,12 @@ google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2 google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -777,6 +833,8 @@ google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -808,11 +866,14 @@ gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -823,3 +884,4 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= From 40ed60b3d99beb955acbe45663458aca26abc808 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Tue, 26 Jul 2022 10:04:28 -0300 Subject: [PATCH 027/130] fix: race conditions on agent shutdown (#158) --- main.go | 3 +- pkg/jobs/job.go | 77 +++++++++----- pkg/listener/job_processor.go | 136 +++++++++---------------- pkg/listener/listener.go | 16 +-- pkg/listener/listener_test.go | 108 ++------------------ pkg/listener/selfhostedapi/register.go | 14 +-- pkg/listener/selfhostedapi/sync.go | 55 ++++++++-- pkg/listener/shutdown_reason.go | 24 ++++- test/e2e.rb | 4 - test/e2e_support/listener_mode.rb | 29 ++---- test/hub_reference/app.rb | 25 ++--- test/support/hub.go | 52 ++++++---- 12 files changed, 234 insertions(+), 309 deletions(-) diff --git a/main.go b/main.go index 0c03f42a..1e15442f 100644 --- a/main.go +++ b/main.go @@ -155,7 +155,6 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { log.Fatalf("Error parsing --files: %v", err) } - idleTimeout := viper.GetInt(config.DisconnectAfterIdleTimeout) config := listener.Config{ Endpoint: viper.GetString(config.Endpoint), Token: viper.GetString(config.Token), @@ -165,7 +164,7 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { Scheme: scheme, ShutdownHookPath: viper.GetString(config.ShutdownHookPath), DisconnectAfterJob: viper.GetBool(config.DisconnectAfterJob), - DisconnectAfterIdleTimeout: time.Duration(int64(idleTimeout) * int64(time.Second)), + DisconnectAfterIdleSeconds: viper.GetInt(config.DisconnectAfterIdleTimeout), EnvVars: hostEnvVars, FileInjections: fileInjections, FailOnMissingFiles: viper.GetBool(config.FailOnMissingFiles), diff --git a/pkg/jobs/job.go b/pkg/jobs/job.go index 6b515eb8..8749cf4e 100644 --- a/pkg/jobs/job.go +++ b/pkg/jobs/job.go @@ -12,6 +12,7 @@ import ( eventlogger "github.com/semaphoreci/agent/pkg/eventlogger" executors "github.com/semaphoreci/agent/pkg/executors" httputils "github.com/semaphoreci/agent/pkg/httputils" + "github.com/semaphoreci/agent/pkg/listener/selfhostedapi" "github.com/semaphoreci/agent/pkg/retry" log "github.com/sirupsen/logrus" ) @@ -88,6 +89,7 @@ func NewJobWithOptions(options *JobOptions) (*Job, error) { executor, err := CreateExecutor(options.Request, job.Logger, *options) if err != nil { + _ = job.Logger.Close() return nil, err } @@ -115,8 +117,7 @@ func CreateExecutor(request *api.JobRequest, logger *eventlogger.Logger, jobOpti type RunOptions struct { EnvVars []config.HostEnvVar FileInjections []config.FileInjection - OnSuccessfulTeardown func() - OnFailedTeardown func() + OnJobFinished func(selfhostedapi.JobResult) CallbackRetryAttempts int } @@ -124,8 +125,7 @@ func (job *Job) Run() { job.RunWithOptions(RunOptions{ EnvVars: []config.HostEnvVar{}, FileInjections: []config.FileInjection{}, - OnSuccessfulTeardown: nil, - OnFailedTeardown: nil, + OnJobFinished: nil, CallbackRetryAttempts: 60, }) } @@ -154,19 +154,20 @@ func (job *Job) RunWithOptions(options RunOptions) { } } - err := job.Teardown(result, options.CallbackRetryAttempts) + result, err := job.Teardown(result, options.CallbackRetryAttempts) if err != nil { - callFuncIfNotNull(options.OnFailedTeardown) - } else { - callFuncIfNotNull(options.OnSuccessfulTeardown) + log.Errorf("Error tearing down job: %v", err) } - job.Finished = true - // the executor is already stopped when the job is stopped, so there's no need to stop it again if !job.Stopped { job.Executor.Stop() } + + job.Finished = true + if options.OnJobFinished != nil { + options.OnJobFinished(selfhostedapi.JobResult(result)) + } } func (job *Job) PrepareEnvironment() int { @@ -269,12 +270,26 @@ func (job *Job) RunCommandsUntilFirstFailure(commands []api.Command) int { return lastExitCode } -func (job *Job) Teardown(result string, callbackRetryAttempts int) error { +func (job *Job) Teardown(result string, callbackRetryAttempts int) (string, error) { // if job was stopped during the epilogues, result should be stopped if job.Stopped { result = JobStopped } + if job.Request.Logger.Method == eventlogger.LoggerMethodPull { + return result, job.teardownWithCallbacks(result, callbackRetryAttempts) + } + + return result, job.teardownWithNoCallbacks(result) +} + +/* + * For hosted jobs, we use callbacks: + * 1. Send finished callback and log job_finished event + * 2. Wait for archivator to collect all the logs + * 3. Send teardown_finished callback and close the logger + */ +func (job *Job) teardownWithCallbacks(result string, callbackRetryAttempts int) error { err := job.SendFinishedCallback(result, callbackRetryAttempts) if err != nil { log.Errorf("Could not send finished callback: %v", err) @@ -282,21 +297,17 @@ func (job *Job) Teardown(result string, callbackRetryAttempts int) error { } job.Logger.LogJobFinished(result) + log.Debug("Waiting for archivator") - if job.Request.Logger.Method == eventlogger.LoggerMethodPull { - log.Debug("Waiting for archivator") - - for { - if job.JobLogArchived { - break - } else { - time.Sleep(1000 * time.Millisecond) - } + for { + if job.JobLogArchived { + break + } else { + time.Sleep(1000 * time.Millisecond) } - - log.Debug("Archivator finished") } + log.Debug("Archivator finished") err = job.Logger.Close() if err != nil { log.Errorf("Error closing logger: %+v", err) @@ -312,6 +323,22 @@ func (job *Job) Teardown(result string, callbackRetryAttempts int) error { return nil } +/* + * For self-hosted jobs, we don't use callbacks. + * The only thing we need to do is log the job_finished event and close the logger. + */ +func (job *Job) teardownWithNoCallbacks(result string) error { + job.Logger.LogJobFinished(result) + + err := job.Logger.Close() + if err != nil { + log.Errorf("Error closing logger: %+v", err) + } + + log.Info("Job teardown finished") + return nil +} + func (job *Job) Stop() { log.Info("Stopping job") @@ -367,9 +394,3 @@ func (job *Job) SendCallback(url string, payload string) error { return nil } - -func callFuncIfNotNull(function func()) { - if function != nil { - function() - } -} diff --git a/pkg/listener/job_processor.go b/pkg/listener/job_processor.go index 43cd1bba..0705d675 100644 --- a/pkg/listener/job_processor.go +++ b/pkg/listener/job_processor.go @@ -21,22 +21,19 @@ import ( func StartJobProcessor(httpClient *http.Client, apiClient *selfhostedapi.API, config Config) (*JobProcessor, error) { p := &JobProcessor{ - HTTPClient: httpClient, - APIClient: apiClient, - LastSuccessfulSync: time.Now(), - LastStateChangeAt: time.Now(), - State: selfhostedapi.AgentStateWaitingForJobs, - SyncInterval: 5 * time.Second, - DisconnectRetryAttempts: 100, - GetJobRetryAttempts: config.GetJobRetryLimit, - CallbackRetryAttempts: config.CallbackRetryLimit, - ShutdownHookPath: config.ShutdownHookPath, - DisconnectAfterJob: config.DisconnectAfterJob, - DisconnectAfterIdleTimeout: config.DisconnectAfterIdleTimeout, - EnvVars: config.EnvVars, - FileInjections: config.FileInjections, - FailOnMissingFiles: config.FailOnMissingFiles, - ExitOnShutdown: config.ExitOnShutdown, + HTTPClient: httpClient, + APIClient: apiClient, + LastSuccessfulSync: time.Now(), + State: selfhostedapi.AgentStateWaitingForJobs, + SyncInterval: 5 * time.Second, + DisconnectRetryAttempts: 100, + GetJobRetryAttempts: config.GetJobRetryLimit, + CallbackRetryAttempts: config.CallbackRetryLimit, + ShutdownHookPath: config.ShutdownHookPath, + EnvVars: config.EnvVars, + FileInjections: config.FileInjections, + FailOnMissingFiles: config.FailOnMissingFiles, + ExitOnShutdown: config.ExitOnShutdown, } go p.Start() @@ -47,27 +44,25 @@ func StartJobProcessor(httpClient *http.Client, apiClient *selfhostedapi.API, co } type JobProcessor struct { - HTTPClient *http.Client - APIClient *selfhostedapi.API - State selfhostedapi.AgentState - CurrentJobID string - CurrentJob *jobs.Job - SyncInterval time.Duration - LastSyncErrorAt *time.Time - LastSuccessfulSync time.Time - LastStateChangeAt time.Time - DisconnectRetryAttempts int - GetJobRetryAttempts int - CallbackRetryAttempts int - ShutdownHookPath string - StopSync bool - DisconnectAfterJob bool - DisconnectAfterIdleTimeout time.Duration - EnvVars []config.HostEnvVar - FileInjections []config.FileInjection - FailOnMissingFiles bool - ExitOnShutdown bool - ShutdownReason ShutdownReason + HTTPClient *http.Client + APIClient *selfhostedapi.API + State selfhostedapi.AgentState + CurrentJobID string + CurrentJobResult selfhostedapi.JobResult + CurrentJob *jobs.Job + SyncInterval time.Duration + LastSyncErrorAt *time.Time + LastSuccessfulSync time.Time + DisconnectRetryAttempts int + GetJobRetryAttempts int + CallbackRetryAttempts int + ShutdownHookPath string + StopSync bool + EnvVars []config.HostEnvVar + FileInjections []config.FileInjection + FailOnMissingFiles bool + ExitOnShutdown bool + ShutdownReason ShutdownReason } func (p *JobProcessor) Start() { @@ -85,37 +80,11 @@ func (p *JobProcessor) SyncLoop() { } } -func (p *JobProcessor) isIdle() bool { - return p.State == selfhostedapi.AgentStateWaitingForJobs -} - -func (p *JobProcessor) setState(newState selfhostedapi.AgentState) { - p.State = newState - p.LastStateChangeAt = time.Now() -} - -func (p *JobProcessor) shutdownIfIdle() { - if !p.isIdle() { - return - } - - if p.DisconnectAfterIdleTimeout == 0 { - return - } - - idleFor := time.Since(p.LastStateChangeAt) - if idleFor > p.DisconnectAfterIdleTimeout { - log.Infof("Agent has been idle for the past %v.", idleFor) - p.Shutdown(ShutdownReasonIdle, 0) - } -} - func (p *JobProcessor) Sync() { - p.shutdownIfIdle() - request := &selfhostedapi.SyncRequest{ - State: p.State, - JobID: p.CurrentJobID, + State: p.State, + JobID: p.CurrentJobID, + JobResult: p.CurrentJobResult, } response, err := p.APIClient.Sync(request) @@ -156,8 +125,8 @@ func (p *JobProcessor) ProcessSyncResponse(response *selfhostedapi.SyncResponse) return case selfhostedapi.AgentActionShutdown: - log.Info("Agent shutdown requested by Semaphore") - p.Shutdown(ShutdownReasonRequested, 0) + log.Infof("Agent shutdown requested by Semaphore due to: %s", response.ShutdownReason) + p.Shutdown(ShutdownReasonFromAPI(response.ShutdownReason), 0) case selfhostedapi.AgentActionWaitForJobs: p.WaitForJobs() @@ -165,13 +134,13 @@ func (p *JobProcessor) ProcessSyncResponse(response *selfhostedapi.SyncResponse) } func (p *JobProcessor) RunJob(jobID string) { - p.setState(selfhostedapi.AgentStateStartingJob) + p.State = selfhostedapi.AgentStateStartingJob p.CurrentJobID = jobID jobRequest, err := p.getJobWithRetries(p.CurrentJobID) if err != nil { log.Errorf("Could not get job %s: %v", jobID, err) - p.setState(selfhostedapi.AgentStateFailedToFetchJob) + p.JobFinished(selfhostedapi.JobResultFailed) return } @@ -189,25 +158,18 @@ func (p *JobProcessor) RunJob(jobID string) { if err != nil { log.Errorf("Could not construct job %s: %v", jobID, err) - p.setState(selfhostedapi.AgentStateFailedToConstructJob) + p.JobFinished(selfhostedapi.JobResultFailed) return } - p.setState(selfhostedapi.AgentStateRunningJob) + p.State = selfhostedapi.AgentStateRunningJob p.CurrentJob = job go job.RunWithOptions(jobs.RunOptions{ EnvVars: p.EnvVars, CallbackRetryAttempts: p.CallbackRetryAttempts, FileInjections: p.FileInjections, - OnSuccessfulTeardown: p.JobFinished, - OnFailedTeardown: func() { - if p.DisconnectAfterJob { - p.Shutdown(ShutdownReasonJobFinished, 1) - } else { - p.setState(selfhostedapi.AgentStateFailedToSendCallback) - } - }, + OnJobFinished: p.JobFinished, }) } @@ -233,23 +195,21 @@ func (p *JobProcessor) getJobWithRetries(jobID string) (*api.JobRequest, error) func (p *JobProcessor) StopJob(jobID string) { p.CurrentJobID = jobID - p.setState(selfhostedapi.AgentStateStoppingJob) + p.State = selfhostedapi.AgentStateStoppingJob p.CurrentJob.Stop() } -func (p *JobProcessor) JobFinished() { - if p.DisconnectAfterJob { - p.Shutdown(ShutdownReasonJobFinished, 0) - } else { - p.setState(selfhostedapi.AgentStateFinishedJob) - } +func (p *JobProcessor) JobFinished(result selfhostedapi.JobResult) { + p.State = selfhostedapi.AgentStateFinishedJob + p.CurrentJobResult = result } func (p *JobProcessor) WaitForJobs() { p.CurrentJobID = "" p.CurrentJob = nil - p.setState(selfhostedapi.AgentStateWaitingForJobs) + p.CurrentJobResult = "" + p.State = selfhostedapi.AgentStateWaitingForJobs } func (p *JobProcessor) SetupInterruptHandler() { diff --git a/pkg/listener/listener.go b/pkg/listener/listener.go index b13d514f..222683d9 100644 --- a/pkg/listener/listener.go +++ b/pkg/listener/listener.go @@ -30,7 +30,7 @@ type Config struct { Scheme string ShutdownHookPath string DisconnectAfterJob bool - DisconnectAfterIdleTimeout time.Duration + DisconnectAfterIdleSeconds int EnvVars []config.HostEnvVar FileInjections []config.FileInjection FailOnMissingFiles bool @@ -107,12 +107,14 @@ func (l *Listener) Register() error { } req := &selfhostedapi.RegisterRequest{ - Version: l.Config.AgentVersion, - Name: name, - PID: os.Getpid(), - OS: osinfo.Name(), - Arch: osinfo.Arch(), - Hostname: osinfo.Hostname(), + Version: l.Config.AgentVersion, + Name: name, + PID: os.Getpid(), + OS: osinfo.Name(), + Arch: osinfo.Arch(), + Hostname: osinfo.Hostname(), + SingleJob: l.Config.DisconnectAfterJob, + IdleTimeout: l.Config.DisconnectAfterIdleSeconds, } err = retry.RetryWithConstantWait(retry.RetryOptions{ diff --git a/pkg/listener/listener_test.go b/pkg/listener/listener_test.go index dc555a52..c679d1f8 100644 --- a/pkg/listener/listener_test.go +++ b/pkg/listener/listener_test.go @@ -296,7 +296,7 @@ func Test__ShutdownAfterIdleTimeout(t *testing.T) { config := Config{ ExitOnShutdown: false, - DisconnectAfterIdleTimeout: 15 * time.Second, + DisconnectAfterIdleSeconds: 15, Endpoint: hubMockServer.Host(), Token: "token", RegisterRetryLimit: 5, @@ -651,7 +651,7 @@ func Test__GetJobIsRetried(t *testing.T) { }, }) - assert.Nil(t, hubMockServer.WaitUntilDisconnected(10, 2*time.Second)) + assert.Nil(t, hubMockServer.WaitUntilDisconnected(20, 2*time.Second)) assert.Equal(t, listener.JobProcessor.ShutdownReason, ShutdownReasonJobFinished) assert.Equal(t, hubMockServer.GetJobAttempts, 5) @@ -671,7 +671,7 @@ func Test__ReportsFailedToFetchJob(t *testing.T) { hubMockServer.RejectGetJobAttempts(100) config := Config{ - DisconnectAfterJob: true, + DisconnectAfterJob: false, ExitOnShutdown: false, Endpoint: hubMockServer.Host(), Token: "token", @@ -700,7 +700,8 @@ func Test__ReportsFailedToFetchJob(t *testing.T) { }, }) - assert.Nil(t, hubMockServer.WaitUntilFailure(string(selfhostedapi.AgentStateFailedToFetchJob), 12, 5*time.Second)) + assert.Nil(t, hubMockServer.WaitUntilFinishedJob(12, 5*time.Second)) + assert.Equal(t, selfhostedapi.JobResult(selfhostedapi.JobResultFailed), hubMockServer.GetLastJobResult()) listener.Stop() hubMockServer.Close() @@ -718,7 +719,7 @@ func Test__ReportsFailedToConstructJob(t *testing.T) { hubMockServer.UseLogsURL(loghubMockServer.URL()) config := Config{ - DisconnectAfterJob: true, + DisconnectAfterJob: false, ExitOnShutdown: false, Endpoint: hubMockServer.Host(), Token: "token", @@ -748,101 +749,8 @@ func Test__ReportsFailedToConstructJob(t *testing.T) { }, }) - assert.Nil(t, hubMockServer.WaitUntilFailure(string(selfhostedapi.AgentStateFailedToConstructJob), 10, 2*time.Second)) - - listener.Stop() - hubMockServer.Close() - loghubMockServer.Close() -} - -func Test__ReportsFailedToSendFinishedCallback(t *testing.T) { - testsupport.SetupTestLogs() - - loghubMockServer := testsupport.NewLoghubMockServer() - loghubMockServer.Init() - - hubMockServer := testsupport.NewHubMockServer() - hubMockServer.Init() - hubMockServer.UseLogsURL(loghubMockServer.URL()) - - config := Config{ - ExitOnShutdown: false, - Endpoint: hubMockServer.Host(), - Token: "token", - RegisterRetryLimit: 5, - GetJobRetryLimit: 2, - CallbackRetryLimit: 5, - Scheme: "http", - EnvVars: []config.HostEnvVar{}, - FileInjections: []config.FileInjection{}, - AgentVersion: "0.0.7", - } - - listener, err := Start(http.DefaultClient, config) - assert.Nil(t, err) - - hubMockServer.AssignJob(&api.JobRequest{ - ID: "Test__ReportsFailedToSendFinishedCallback", - Commands: []api.Command{}, - Callbacks: api.Callbacks{ - Finished: "https://httpbin.org/status/500", - TeardownFinished: "https://httpbin.org/status/200", - }, - Logger: api.Logger{ - Method: eventlogger.LoggerMethodPush, - URL: loghubMockServer.URL(), - Token: "doesnotmatter", - }, - }) - - assert.Nil(t, hubMockServer.WaitUntilFailure(string(selfhostedapi.AgentStateFailedToSendCallback), 10, 2*time.Second)) - - listener.Stop() - hubMockServer.Close() - loghubMockServer.Close() -} - -func Test__ReportsFailedToSendTeardownFinishedCallback(t *testing.T) { - testsupport.SetupTestLogs() - - loghubMockServer := testsupport.NewLoghubMockServer() - loghubMockServer.Init() - - hubMockServer := testsupport.NewHubMockServer() - hubMockServer.Init() - hubMockServer.UseLogsURL(loghubMockServer.URL()) - - config := Config{ - ExitOnShutdown: false, - Endpoint: hubMockServer.Host(), - Token: "token", - RegisterRetryLimit: 5, - GetJobRetryLimit: 2, - CallbackRetryLimit: 5, - Scheme: "http", - EnvVars: []config.HostEnvVar{}, - FileInjections: []config.FileInjection{}, - AgentVersion: "0.0.7", - } - - listener, err := Start(http.DefaultClient, config) - assert.Nil(t, err) - - hubMockServer.AssignJob(&api.JobRequest{ - ID: "Test__ReportsFailedToSendTeardownFinishedCallback", - Commands: []api.Command{}, - Callbacks: api.Callbacks{ - Finished: "https://httpbin.org/status/200", - TeardownFinished: "https://httpbin.org/status/500", - }, - Logger: api.Logger{ - Method: eventlogger.LoggerMethodPush, - URL: loghubMockServer.URL(), - Token: "doesnotmatter", - }, - }) - - assert.Nil(t, hubMockServer.WaitUntilFailure(string(selfhostedapi.AgentStateFailedToSendCallback), 10, 2*time.Second)) + assert.Nil(t, hubMockServer.WaitUntilFinishedJob(10, 2*time.Second)) + assert.Equal(t, selfhostedapi.JobResult(selfhostedapi.JobResultFailed), hubMockServer.GetLastJobResult()) listener.Stop() hubMockServer.Close() diff --git a/pkg/listener/selfhostedapi/register.go b/pkg/listener/selfhostedapi/register.go index b79aba0b..c36a7f1a 100644 --- a/pkg/listener/selfhostedapi/register.go +++ b/pkg/listener/selfhostedapi/register.go @@ -12,12 +12,14 @@ import ( ) type RegisterRequest struct { - Name string `json:"name"` - Version string `json:"version"` - PID int `json:"pid"` - OS string `json:"os"` - Arch string `json:"arch"` - Hostname string `json:"hostname"` + Name string `json:"name"` + Version string `json:"version"` + PID int `json:"pid"` + OS string `json:"os"` + Arch string `json:"arch"` + Hostname string `json:"hostname"` + SingleJob bool `json:"single_job"` + IdleTimeout int `json:"idle_timeout"` } type RegisterResponse struct { diff --git a/pkg/listener/selfhostedapi/sync.go b/pkg/listener/selfhostedapi/sync.go index 868659fc..aa707d0e 100644 --- a/pkg/listener/selfhostedapi/sync.go +++ b/pkg/listener/selfhostedapi/sync.go @@ -12,15 +12,14 @@ import ( type AgentState string type AgentAction string +type JobResult string +type ShutdownReason string const AgentStateWaitingForJobs = "waiting-for-jobs" const AgentStateStartingJob = "starting-job" const AgentStateRunningJob = "running-job" const AgentStateStoppingJob = "stopping-job" const AgentStateFinishedJob = "finished-job" -const AgentStateFailedToFetchJob = "failed-to-fetch-job" -const AgentStateFailedToConstructJob = "failed-to-construct-job" -const AgentStateFailedToSendCallback = "failed-to-send-callback" const AgentActionWaitForJobs = "wait-for-jobs" const AgentActionRunJob = "run-job" @@ -28,14 +27,24 @@ const AgentActionStopJob = "stop-job" const AgentActionShutdown = "shutdown" const AgentActionContinue = "continue" +const JobResultStopped = "stopped" +const JobResultFailed = "failed" +const JobResultPassed = "passed" + +const ShutdownReasonIdle = "idle" +const ShutdownReasonJobFinished = "job-finished" +const ShutdownReasonRequested = "requested" + type SyncRequest struct { - State AgentState `json:"state"` - JobID string `json:"job_id"` + State AgentState `json:"state"` + JobID string `json:"job_id"` + JobResult JobResult `json:"job_result"` } type SyncResponse struct { - Action AgentAction `json:"action"` - JobID string `json:"job_id"` + Action AgentAction `json:"action"` + JobID string `json:"job_id"` + ShutdownReason ShutdownReason `json:"shutdown_reason"` } func (a *API) SyncPath() string { @@ -48,8 +57,7 @@ func (a *API) Sync(req *SyncRequest) (*SyncResponse, error) { return nil, err } - log.Infof("SYNC request (state: %s, job: %s)", req.State, req.JobID) - + a.logSyncRequest(req) r, err := http.NewRequest("POST", a.SyncPath(), bytes.NewBuffer(b)) if err != nil { return nil, err @@ -77,7 +85,32 @@ func (a *API) Sync(req *SyncRequest) (*SyncResponse, error) { return nil, err } - log.Infof("SYNC response (action: %s, job: %s)", response.Action, response.JobID) - + a.logSyncResponse(response) return response, nil } + +func (a *API) logSyncRequest(req *SyncRequest) { + switch req.State { + case AgentStateWaitingForJobs: + log.Infof("SYNC request (state: %s)", req.State) + case AgentStateStoppingJob, AgentStateStartingJob, AgentStateRunningJob: + log.Infof("SYNC request (state: %s, job: %s)", req.State, req.JobID) + case AgentStateFinishedJob: + log.Infof("SYNC request (state: %s, job: %s, result: %s)", req.State, req.JobID, req.JobResult) + default: + log.Infof("SYNC request: %v", req) + } +} + +func (a *API) logSyncResponse(response *SyncResponse) { + switch response.Action { + case AgentActionContinue, AgentActionWaitForJobs: + log.Infof("SYNC response (action: %s)", response.Action) + case AgentActionRunJob, AgentActionStopJob: + log.Infof("SYNC response (action: %s, job: %s)", response.Action, response.JobID) + case AgentActionShutdown: + log.Infof("SYNC response (action: %s, reason: %s)", response.Action, response.ShutdownReason) + default: + log.Infof("SYNC response: %v", response) + } +} diff --git a/pkg/listener/shutdown_reason.go b/pkg/listener/shutdown_reason.go index 5ad7ea52..440d9b8e 100644 --- a/pkg/listener/shutdown_reason.go +++ b/pkg/listener/shutdown_reason.go @@ -1,15 +1,37 @@ package listener +import "github.com/semaphoreci/agent/pkg/listener/selfhostedapi" + type ShutdownReason int64 const ( + + // When the agent shuts down due to these reasons, + // the Semaphore API requested it to do so. ShutdownReasonIdle ShutdownReason = iota ShutdownReasonJobFinished - ShutdownReasonUnableToSync ShutdownReasonRequested + ShutdownReasonUnknown + + // When the agent shuts down due to these reasons, + // the agent decides to do so. + ShutdownReasonUnableToSync ShutdownReasonInterrupted ) +func ShutdownReasonFromAPI(reasonFromAPI selfhostedapi.ShutdownReason) ShutdownReason { + switch reasonFromAPI { + case selfhostedapi.ShutdownReasonIdle: + return ShutdownReasonIdle + case selfhostedapi.ShutdownReasonJobFinished: + return ShutdownReasonJobFinished + case selfhostedapi.ShutdownReasonRequested: + return ShutdownReasonRequested + } + + return ShutdownReasonUnknown +} + func (s ShutdownReason) String() string { switch s { case ShutdownReasonIdle: diff --git a/test/e2e.rb b/test/e2e.rb index 9041fe77..de1c05af 100644 --- a/test/e2e.rb +++ b/test/e2e.rb @@ -77,10 +77,6 @@ def teardown_callback_url $strategy.teardown_callback_url end -def wait_for_job_to_get_stuck - $strategy.wait_for_job_to_get_stuck -end - def shutdown_agent $strategy.shutdown_agent end diff --git a/test/e2e_support/listener_mode.rb b/test/e2e_support/listener_mode.rb index 3fa11612..ab0d778a 100644 --- a/test/e2e_support/listener_mode.rb +++ b/test/e2e_support/listener_mode.rb @@ -40,26 +40,6 @@ def wait_for_command_to_start(cmd) end end - def wait_for_job_to_get_stuck - puts "" - puts "Waiting for job to get stuck" - - loop do - response = `curl -s --fail -X GET -k "#{HUB_ENDPOINT}/api/v1/self_hosted_agents/jobs/#{$JOB_ID}/status"`.strip - puts "Job state #{response}" - - if response == "stuck" - return - else - sleep 1 - end - end - - sleep 5 - - puts - end - def wait_for_agent_to_shutdown puts "" puts "Waiting for agent to shutdown" @@ -83,6 +63,7 @@ def wait_for_agent_to_shutdown def wait_for_job_to_finish puts "" puts "Waiting for job to finish" + attempts = 0 loop do response = `curl -s --fail -X GET -k "#{HUB_ENDPOINT}/api/v1/self_hosted_agents/jobs/#{$JOB_ID}/status"`.strip @@ -91,6 +72,10 @@ def wait_for_job_to_finish if response == "finished" return else + attempts += 1 + if attempts > 600 + abort "Job did not finish in 10 minutes - giving up" + end sleep 1 end end @@ -203,11 +188,11 @@ def assert_job_log(expected_log) end def finished_callback_url - "http://hub:4567/jobs/#{$JOB_ID}/callbacks/finished" + "" end def teardown_callback_url - "http://hub:4567/jobs/#{$JOB_ID}/callbacks/finished" + "" end private diff --git a/test/hub_reference/app.rb b/test/hub_reference/app.rb index ccb08ea7..e5ff10bf 100644 --- a/test/hub_reference/app.rb +++ b/test/hub_reference/app.rb @@ -15,7 +15,6 @@ $payloads = {} $job_states = {} $finished = {} -$teardown = {} $logs = [] before do @@ -65,27 +64,21 @@ when "running-job" job_id = @json_request["job_id"] if $should_shutdown || $job_states[job_id] == "stopping" - {"action" => "stop-job"} + {"action" => "stop-job", "job_id" => job_id} else {"action" => "continue"} end when "stopping-job" {"action" => "continue"} when "finished-job" - $should_shutdown ? {"action" => "shutdown"} : {"action" => "wait-for-jobs"} + $job_states[@json_request["job_id"]] = "finished" + if $should_shutdown + {"action" => "shutdown"} + else + {"action" => "wait-for-jobs"} + end when "starting-job" {"action" => "continue"} - when "failed-to-send-callback" - job_id = @json_request["job_id"] - $job_states[job_id] = "stuck" - $should_shutdown ? {"action" => "shutdown"} : {"action" => "continue"} - when "failed-to-fetch-job" - job_id = @json_request["job_id"] - $job_states[job_id] = "stuck" - $should_shutdown ? {"action" => "shutdown"} : {"action" => "continue"} - when "failed-to-construct-job" - $job_states[job_id] = "stuck" - $should_shutdown ? {"action" => "shutdown"} : {"action" => "continue"} else raise "unknown state" end @@ -126,10 +119,6 @@ $job_states[params["id"]] = "finished" end -post "/jobs/:id/callbacks/teardown" do - $teardown[params["id"]] = true -end - # # Private APIs. Only needed to contoll the flow # of e2e tests in the Agent. diff --git a/test/support/hub.go b/test/support/hub.go index 9de1f5d6..f3cdb7eb 100644 --- a/test/support/hub.go +++ b/test/support/hub.go @@ -29,12 +29,16 @@ type HubMockServer struct { RunningJob bool FinishedJob bool TokenIsRefreshed bool - FailureStatus string + JobResult selfhostedapi.JobResult + LastState selfhostedapi.AgentState + LastStateChange *time.Time } func NewHubMockServer() *HubMockServer { + now := time.Now() return &HubMockServer{ RegisterAttempts: -1, + LastStateChange: &now, } } @@ -113,6 +117,7 @@ func (m *HubMockServer) handleRegisterRequest(w http.ResponseWriter, r *http.Req return } + m.LastState = selfhostedapi.AgentActionWaitForJobs _, _ = w.Write(response) } @@ -142,6 +147,15 @@ func (m *HubMockServer) handleSyncRequest(w http.ResponseWriter, r *http.Request case selfhostedapi.AgentStateWaitingForJobs: if m.ShouldShutdown { syncResponse.Action = selfhostedapi.AgentActionShutdown + syncResponse.ShutdownReason = selfhostedapi.ShutdownReasonRequested + } + + if m.RegisterRequest.IdleTimeout > 0 { + lastStateChange := int(time.Since(*m.LastStateChange) / time.Second) + if lastStateChange > m.RegisterRequest.IdleTimeout { + syncResponse.Action = selfhostedapi.AgentActionShutdown + syncResponse.ShutdownReason = selfhostedapi.ShutdownReasonIdle + } } if m.JobRequest != nil { @@ -160,18 +174,17 @@ func (m *HubMockServer) handleSyncRequest(w http.ResponseWriter, r *http.Request case selfhostedapi.AgentStateFinishedJob: m.JobRequest = nil m.FinishedJob = true + m.JobResult = request.JobResult if m.ShouldShutdown { syncResponse.Action = selfhostedapi.AgentActionShutdown + syncResponse.ShutdownReason = selfhostedapi.ShutdownReasonRequested + } else if m.RegisterRequest.SingleJob { + syncResponse.Action = selfhostedapi.AgentActionShutdown + syncResponse.ShutdownReason = selfhostedapi.ShutdownReasonJobFinished } else { syncResponse.Action = selfhostedapi.AgentActionWaitForJobs } - - case selfhostedapi.AgentStateFailedToFetchJob, - selfhostedapi.AgentStateFailedToConstructJob, - selfhostedapi.AgentStateFailedToSendCallback: - m.FailureStatus = string(request.State) - syncResponse.Action = selfhostedapi.AgentActionWaitForJobs } response, err := json.Marshal(syncResponse) @@ -181,6 +194,12 @@ func (m *HubMockServer) handleSyncRequest(w http.ResponseWriter, r *http.Request return } + if request.State != m.LastState { + now := time.Now() + m.LastStateChange = &now + } + + m.LastState = request.State _, _ = w.Write(response) } @@ -231,21 +250,6 @@ func (m *HubMockServer) Host() string { return m.Server.Listener.Addr().String() } -func (m *HubMockServer) WaitUntilFailure(status string, attempts int, wait time.Duration) error { - return retry.RetryWithConstantWait(retry.RetryOptions{ - Task: "WaitUntilRunningJob", - MaxAttempts: attempts, - DelayBetweenAttempts: wait, - Fn: func() error { - if m.FailureStatus != status { - return fmt.Errorf("still haven't failed with %s", status) - } - - return nil - }, - }) -} - func (m *HubMockServer) WaitUntilRunningJob(attempts int, wait time.Duration) error { return retry.RetryWithConstantWait(retry.RetryOptions{ Task: "WaitUntilRunningJob", @@ -306,6 +310,10 @@ func (m *HubMockServer) WaitUntilRegistered() error { }) } +func (m *HubMockServer) GetLastJobResult() selfhostedapi.JobResult { + return m.JobResult +} + func (m *HubMockServer) GetRegisterRequest() *selfhostedapi.RegisterRequest { return m.RegisterRequest } From 643e68a0d767c6f0c9f1eb1ae9534326ca8ed994 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Thu, 4 Aug 2022 11:07:58 -0300 Subject: [PATCH 028/130] build: update Go to 1.18 (#160) --- .github/workflows/test.yml | 2 +- .semaphore/release.yml | 2 +- .semaphore/semaphore.yml | 12 +- go.mod | 23 ++- go.sum | 391 +------------------------------------ 5 files changed, 30 insertions(+), 400 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 46a68647..6dc31327 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ jobs: - name: Install Go uses: actions/setup-go@v2 with: - go-version: 1.16.x + go-version: 1.18.x - name: Check out repository code uses: actions/checkout@v2 - name: Install gotestsum diff --git a/.semaphore/release.yml b/.semaphore/release.yml index ab09c062..9a1c3a53 100644 --- a/.semaphore/release.yml +++ b/.semaphore/release.yml @@ -14,7 +14,7 @@ blocks: - name: github-release-bot-agent prologue: commands: - - sem-version go 1.16 + - sem-version go 1.18 - "export GOPATH=~/go" - "export PATH=/home/semaphore/go/bin:$PATH" - checkout diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index f329a9e7..144cb8f3 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -22,13 +22,13 @@ blocks: prologue: commands: - - sem-version go 1.16 + - sem-version go 1.18 - checkout jobs: - name: Lint commands: - - go get -u github.com/mgechev/revive + - go install github.com/mgechev/revive@latest - make lint - name: "Security checks" @@ -58,7 +58,7 @@ blocks: prologue: commands: - - sem-version go 1.16 + - sem-version go 1.18 - checkout - go version - go get @@ -90,7 +90,7 @@ blocks: prologue: commands: - - sem-version go 1.16 + - sem-version go 1.18 - checkout - go version - go get @@ -154,7 +154,7 @@ blocks: prologue: commands: - - sem-version go 1.16 + - sem-version go 1.18 - checkout - go version - go get @@ -181,7 +181,7 @@ blocks: prologue: commands: - - sem-version go 1.16 + - sem-version go 1.18 - checkout - go version - go get diff --git a/go.mod b/go.mod index 86436487..cf82df50 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,6 @@ module github.com/semaphoreci/agent require ( github.com/creack/pty v1.1.18 - github.com/felixge/httpsnoop v1.0.2 // indirect github.com/golang-jwt/jwt/v4 v4.4.2 github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 @@ -16,4 +15,24 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) -go 1.16 +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/felixge/httpsnoop v1.0.2 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/magiconair/properties v1.8.6 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/afero v1.8.2 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/subosito/gotenv v1.3.0 // indirect + golang.org/x/text v0.3.7 // indirect + gopkg.in/alexcesaro/statsd.v2 v2.0.0 // indirect + gopkg.in/ini.v1 v1.66.4 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) + +go 1.18 diff --git a/go.sum b/go.sum index 0b9ace7d..f7c78f33 100644 --- a/go.sum +++ b/go.sum @@ -17,32 +17,14 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= -cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= -cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= -cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= -cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= -cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= -cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= -cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= -cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= -cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= -cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= -cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -56,91 +38,40 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= -github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -148,8 +79,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -164,10 +93,6 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -178,18 +103,11 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -200,162 +118,51 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= -github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= -github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= -github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= -github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= -github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= -github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= -github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/panicwrap v1.0.0 h1:67zIyVakCIvcs69A0FGfZjBdPleaonSgGlXRSRlb6fE= github.com/mitchellh/panicwrap v1.0.0/go.mod h1:pKvZHwWrZowLUzftuFq7coarnxbBXU4aQh3N0BJOeeA= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/renderedtext/go-watchman v0.0.0-20220524201126-042727917d44 h1:MahMOODBM7zV5F8ptFnnMw4qWsP96Y+ZmKc1AAAYG/o= github.com/renderedtext/go-watchman v0.0.0-20220524201126-042727917d44/go.mod h1:Z+qanDzSoUGCbcrTM7G6YCA9ST2KBdte7sCz+HQAp7I= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= @@ -367,50 +174,33 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= -github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= -go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= -go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -434,7 +224,6 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -445,10 +234,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -456,11 +243,9 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -477,22 +262,9 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -502,17 +274,6 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -523,34 +284,20 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -561,8 +308,6 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -570,51 +315,19 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -633,7 +346,6 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -658,7 +370,6 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= @@ -667,20 +378,12 @@ golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -700,26 +403,6 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513 google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= -google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= -google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= -google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= -google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= -google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= -google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= -google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= -google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= -google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= -google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= -google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -750,7 +433,6 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= @@ -763,48 +445,7 @@ google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= -google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= -google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -818,24 +459,9 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -846,32 +472,18 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/statsd.v2 v2.0.0 h1:FXkZSCZIH17vLCO5sO2UucTHsH9pc+17F6pl3JVCwMc= gopkg.in/alexcesaro/statsd.v2 v2.0.0/go.mod h1:i0ubccKGzBVNBpdGV5MocxyA/XlLUJzA7SLonnE4drU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -884,4 +496,3 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= From 3fa555a80f8968cba6cfbed26cb0a55e5ed938ed Mon Sep 17 00:00:00 2001 From: Luke Young <91491244+lyoung-confluent@users.noreply.github.com> Date: Fri, 5 Aug 2022 07:17:06 -0700 Subject: [PATCH 029/130] fix: avoid unnecessary Write in FileBackend (#159) --- pkg/eventlogger/filebackend.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/pkg/eventlogger/filebackend.go b/pkg/eventlogger/filebackend.go index 4067d124..f8ca622b 100644 --- a/pkg/eventlogger/filebackend.go +++ b/pkg/eventlogger/filebackend.go @@ -31,22 +31,18 @@ func (l *FileBackend) Open() error { } func (l *FileBackend) Write(event interface{}) error { - jsonString, err := json.Marshal(event) + jsonBytes, err := json.Marshal(event) if err != nil { return err } + jsonBytes = append(jsonBytes, '\n') - _, err = l.file.Write([]byte(jsonString)) + _, err = l.file.Write(jsonBytes) if err != nil { return err } - _, err = l.file.Write([]byte("\n")) - if err != nil { - return err - } - - log.Debugf("%s", jsonString) + log.Debugf("%s", jsonBytes) return nil } From 75b7c6194773ce3754270bae4fcbe4e72e08d3bf Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Tue, 9 Aug 2022 12:00:22 -0300 Subject: [PATCH 030/130] fix: disable package-comments lint rule (#163) --- lint.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lint.toml b/lint.toml index 34bf477f..f28910b6 100644 --- a/lint.toml +++ b/lint.toml @@ -16,10 +16,12 @@ warningCode = 1 [rule.increment-decrement] [rule.var-naming] [rule.var-declaration] -[rule.package-comments] [rule.range] [rule.receiver-naming] [rule.time-naming] [rule.unexported-return] [rule.indent-error-flow] [rule.errorf] + +[rule.package-comments] + Disabled = true From 64614fc2d736487e4c69687a9e80ccd6535c9ffd Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Wed, 10 Aug 2022 11:19:44 -0300 Subject: [PATCH 031/130] fix: use random delay for sync requests (#162) --- pkg/eventlogger/httpbackend.go | 18 +++++------------- pkg/listener/job_processor.go | 8 +++++--- pkg/random/random.go | 21 +++++++++++++++++++++ pkg/random/random_test.go | 29 +++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+), 16 deletions(-) create mode 100644 pkg/random/random.go create mode 100644 pkg/random/random_test.go diff --git a/pkg/eventlogger/httpbackend.go b/pkg/eventlogger/httpbackend.go index a04d08db..7887b0f6 100644 --- a/pkg/eventlogger/httpbackend.go +++ b/pkg/eventlogger/httpbackend.go @@ -4,12 +4,12 @@ import ( "bytes" "errors" "fmt" - "math/rand" "net/http" "os" "path/filepath" "time" + "github.com/semaphoreci/agent/pkg/random" "github.com/semaphoreci/agent/pkg/retry" log "github.com/sirupsen/logrus" ) @@ -122,24 +122,16 @@ func (l *HTTPBackend) delay() time.Duration { * we use a tighter range of 500ms - 1000ms. */ if l.flush { - min := 500 - max := 1000 - - // #nosec - interval := rand.Intn(max-min) + min - return time.Duration(interval) * time.Millisecond + delay, _ := random.DurationInRange(500, 1000) + return *delay } /* * if we are not flushing, * we use a wider range of 1500ms - 3000ms. */ - min := 1500 - max := 3000 - - // #nosec - interval := rand.Intn(max-min) + min - return time.Duration(interval) * time.Millisecond + delay, _ := random.DurationInRange(1500, 3000) + return *delay } func (l *HTTPBackend) newRequest() error { diff --git a/pkg/listener/job_processor.go b/pkg/listener/job_processor.go index 0705d675..9cf23859 100644 --- a/pkg/listener/job_processor.go +++ b/pkg/listener/job_processor.go @@ -14,6 +14,7 @@ import ( "github.com/semaphoreci/agent/pkg/config" jobs "github.com/semaphoreci/agent/pkg/jobs" selfhostedapi "github.com/semaphoreci/agent/pkg/listener/selfhostedapi" + "github.com/semaphoreci/agent/pkg/random" "github.com/semaphoreci/agent/pkg/retry" "github.com/semaphoreci/agent/pkg/shell" log "github.com/sirupsen/logrus" @@ -25,7 +26,6 @@ func StartJobProcessor(httpClient *http.Client, apiClient *selfhostedapi.API, co APIClient: apiClient, LastSuccessfulSync: time.Now(), State: selfhostedapi.AgentStateWaitingForJobs, - SyncInterval: 5 * time.Second, DisconnectRetryAttempts: 100, GetJobRetryAttempts: config.GetJobRetryLimit, CallbackRetryAttempts: config.CallbackRetryLimit, @@ -50,7 +50,6 @@ type JobProcessor struct { CurrentJobID string CurrentJobResult selfhostedapi.JobResult CurrentJob *jobs.Job - SyncInterval time.Duration LastSyncErrorAt *time.Time LastSuccessfulSync time.Time DisconnectRetryAttempts int @@ -76,7 +75,10 @@ func (p *JobProcessor) SyncLoop() { } p.Sync() - time.Sleep(p.SyncInterval) + + delay, _ := random.DurationInRange(3000, 6000) + log.Infof("Waiting %v for next sync...", delay) + time.Sleep(*delay) } } diff --git a/pkg/random/random.go b/pkg/random/random.go new file mode 100644 index 00000000..ad888e7d --- /dev/null +++ b/pkg/random/random.go @@ -0,0 +1,21 @@ +package random + +import ( + "fmt" + "math/rand" + "time" +) + +func DurationInRange(minMillis, maxMillis int) (*time.Duration, error) { + if minMillis <= 0 { + return nil, fmt.Errorf("min cannot be less than or equal to zero") + } + + if minMillis >= maxMillis { + return nil, fmt.Errorf("max cannot be greater than or equal to zero") + } + + interval := rand.Intn(maxMillis-minMillis) + minMillis + duration := time.Duration(interval) * time.Millisecond + return &duration, nil +} diff --git a/pkg/random/random_test.go b/pkg/random/random_test.go new file mode 100644 index 00000000..c0e3b918 --- /dev/null +++ b/pkg/random/random_test.go @@ -0,0 +1,29 @@ +package random + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func Test__RandomDuration(t *testing.T) { + t.Run("min can't be zero or negative", func(t *testing.T) { + duration, err := DurationInRange(-1, 0) + assert.Nil(t, duration) + assert.ErrorContains(t, err, "min cannot be less than or equal to zero") + }) + + t.Run("max cannot be below or equal to min", func(t *testing.T) { + duration, err := DurationInRange(100, 50) + assert.Nil(t, duration) + assert.ErrorContains(t, err, "max cannot be greater than or equal to zero") + }) + + t.Run("duration is in range", func(t *testing.T) { + duration, err := DurationInRange(50, 100) + assert.Nil(t, err) + assert.GreaterOrEqual(t, int(*duration/time.Millisecond), 50) + assert.LessOrEqual(t, int(*duration/time.Millisecond), 100) + }) +} From ae278448d60b95021aa3d021572e2bdbb5467aac Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Wed, 10 Aug 2022 11:46:47 -0300 Subject: [PATCH 032/130] feat: add pre-job hook (#161) --- main.go | 4 + pkg/config/config.go | 4 + pkg/executors/docker_compose_executor.go | 31 +++- pkg/executors/executor.go | 8 + pkg/executors/shell_executor.go | 31 +++- pkg/jobs/job.go | 69 +++++++- pkg/jobs/job_test.go | 203 +++++++++++++++++++++++ pkg/listener/job_processor.go | 6 + pkg/listener/listener.go | 2 + pkg/listener/listener_test.go | 23 +-- test/support/commands.go | 5 +- test/support/files.go | 25 +++ 12 files changed, 368 insertions(+), 43 deletions(-) create mode 100644 test/support/files.go diff --git a/main.go b/main.go index 1e15442f..5f567a74 100644 --- a/main.go +++ b/main.go @@ -109,11 +109,13 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { _ = pflag.String(config.Token, "", "Registration token") _ = pflag.Bool(config.NoHTTPS, false, "Use http for communication") _ = pflag.String(config.ShutdownHookPath, "", "Shutdown hook path") + _ = pflag.String(config.PreJobHookPath, "", "Pre-job hook path") _ = pflag.Bool(config.DisconnectAfterJob, false, "Disconnect after job") _ = pflag.Int(config.DisconnectAfterIdleTimeout, 0, "Disconnect after idle timeout, in seconds") _ = pflag.StringSlice(config.EnvVars, []string{}, "Export environment variables in jobs") _ = pflag.StringSlice(config.Files, []string{}, "Inject files into container, when using docker compose executor") _ = pflag.Bool(config.FailOnMissingFiles, false, "Fail job if files specified using --files are missing") + _ = pflag.Bool(config.FailOnPreJobHookError, false, "Fail job if pre-job hook fails") pflag.Parse() @@ -163,11 +165,13 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { CallbackRetryLimit: 60, Scheme: scheme, ShutdownHookPath: viper.GetString(config.ShutdownHookPath), + PreJobHookPath: viper.GetString(config.PreJobHookPath), DisconnectAfterJob: viper.GetBool(config.DisconnectAfterJob), DisconnectAfterIdleSeconds: viper.GetInt(config.DisconnectAfterIdleTimeout), EnvVars: hostEnvVars, FileInjections: fileInjections, FailOnMissingFiles: viper.GetBool(config.FailOnMissingFiles), + FailOnPreJobHookError: viper.GetBool(config.FailOnPreJobHookError), AgentVersion: VERSION, ExitOnShutdown: true, } diff --git a/pkg/config/config.go b/pkg/config/config.go index 09d881fb..d563c748 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -8,11 +8,13 @@ const ( Token = "token" NoHTTPS = "no-https" ShutdownHookPath = "shutdown-hook-path" + PreJobHookPath = "pre-job-hook-path" DisconnectAfterJob = "disconnect-after-job" DisconnectAfterIdleTimeout = "disconnect-after-idle-timeout" EnvVars = "env-vars" Files = "files" FailOnMissingFiles = "fail-on-missing-files" + FailOnPreJobHookError = "fail-on-pre-job-hook-error" ) var ValidConfigKeys = []string{ @@ -21,11 +23,13 @@ var ValidConfigKeys = []string{ Token, NoHTTPS, ShutdownHookPath, + PreJobHookPath, DisconnectAfterJob, DisconnectAfterIdleTimeout, EnvVars, Files, FailOnMissingFiles, + FailOnPreJobHookError, } type HostEnvVar struct { diff --git a/pkg/executors/docker_compose_executor.go b/pkg/executors/docker_compose_executor.go index aaa63785..0243907a 100644 --- a/pkg/executors/docker_compose_executor.go +++ b/pkg/executors/docker_compose_executor.go @@ -695,30 +695,43 @@ func (e *DockerComposeExecutor) InjectFiles(files []api.File) int { } func (e *DockerComposeExecutor) RunCommand(command string, silent bool, alias string) int { - directive := command - if alias != "" { - directive = alias + return e.RunCommandWithOptions(CommandOptions{ + Command: command, + Silent: silent, + Alias: alias, + Warning: "", + }) +} + +func (e *DockerComposeExecutor) RunCommandWithOptions(options CommandOptions) int { + directive := options.Command + if options.Alias != "" { + directive = options.Alias } - p := e.Shell.NewProcess(command) + p := e.Shell.NewProcess(options.Command) - if !silent { + if !options.Silent { e.Logger.LogCommandStarted(directive) - if alias != "" { - e.Logger.LogCommandOutput(fmt.Sprintf("Running: %s\n", command)) + if options.Alias != "" { + e.Logger.LogCommandOutput(fmt.Sprintf("Running: %s\n", options.Command)) + } + + if options.Warning != "" { + e.Logger.LogCommandOutput(fmt.Sprintf("Warning: %s\n", options.Warning)) } } p.OnStdout(func(output string) { - if !silent { + if !options.Silent { e.Logger.LogCommandOutput(output) } }) p.Run() - if !silent { + if !options.Silent { e.Logger.LogCommandFinished(directive, p.ExitCode, p.StartedAt, p.FinishedAt) } diff --git a/pkg/executors/executor.go b/pkg/executors/executor.go index ef3b5009..2eabca49 100644 --- a/pkg/executors/executor.go +++ b/pkg/executors/executor.go @@ -11,9 +11,17 @@ type Executor interface { ExportEnvVars([]api.EnvVar, []config.HostEnvVar) int InjectFiles([]api.File) int RunCommand(string, bool, string) int + RunCommandWithOptions(options CommandOptions) int Stop() int Cleanup() int } +type CommandOptions struct { + Command string + Silent bool + Alias string + Warning string +} + const ExecutorTypeShell = "shell" const ExecutorTypeDockerCompose = "dockercompose" diff --git a/pkg/executors/shell_executor.go b/pkg/executors/shell_executor.go index 7ad80ecc..e58117f4 100644 --- a/pkg/executors/shell_executor.go +++ b/pkg/executors/shell_executor.go @@ -227,30 +227,43 @@ func (e *ShellExecutor) InjectFiles(files []api.File) int { } func (e *ShellExecutor) RunCommand(command string, silent bool, alias string) int { - directive := command - if alias != "" { - directive = alias + return e.RunCommandWithOptions(CommandOptions{ + Command: command, + Silent: silent, + Alias: alias, + Warning: "", + }) +} + +func (e *ShellExecutor) RunCommandWithOptions(options CommandOptions) int { + directive := options.Command + if options.Alias != "" { + directive = options.Alias } - p := e.Shell.NewProcess(command) + p := e.Shell.NewProcess(options.Command) - if !silent { + if !options.Silent { e.Logger.LogCommandStarted(directive) - if alias != "" { - e.Logger.LogCommandOutput(fmt.Sprintf("Running: %s\n", command)) + if options.Alias != "" { + e.Logger.LogCommandOutput(fmt.Sprintf("Running: %s\n", options.Command)) + } + + if options.Warning != "" { + e.Logger.LogCommandOutput(fmt.Sprintf("Warning: %s\n", options.Warning)) } } p.OnStdout(func(output string) { - if !silent { + if !options.Silent { e.Logger.LogCommandOutput(output) } }) p.Run() - if !silent { + if !options.Silent { e.Logger.LogCommandFinished(directive, p.ExitCode, p.StartedAt, p.FinishedAt) } diff --git a/pkg/jobs/job.go b/pkg/jobs/job.go index 8749cf4e..68256701 100644 --- a/pkg/jobs/job.go +++ b/pkg/jobs/job.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "fmt" "net/http" + "runtime" "time" api "github.com/semaphoreci/agent/pkg/api" @@ -117,14 +118,43 @@ func CreateExecutor(request *api.JobRequest, logger *eventlogger.Logger, jobOpti type RunOptions struct { EnvVars []config.HostEnvVar FileInjections []config.FileInjection + PreJobHookPath string + FailOnPreJobHookError bool OnJobFinished func(selfhostedapi.JobResult) CallbackRetryAttempts int } +func (o *RunOptions) GetPreJobHookWarning() string { + if o.PreJobHookPath == "" { + return "" + } + + if o.FailOnPreJobHookError { + return `The agent is configured to fail the job if the pre-job hook fails.` + } + + return `The agent is configured to proceed with the job even if the pre-job hook fails.` +} + +func (o *RunOptions) GetPreJobHookCommand() string { + + /* + * If we are dealing with PowerShell, we make sure to just call the script directly, + * without creating a new powershell process. If we did, people would need to set + * $ErrorActionPreference to "STOP" in order for errors to propagate properly. + */ + if runtime.GOOS == "windows" { + return o.PreJobHookPath + } + + return fmt.Sprintf("bash %s", o.PreJobHookPath) +} + func (job *Job) Run() { job.RunWithOptions(RunOptions{ EnvVars: []config.HostEnvVar{}, FileInjections: []config.FileInjection{}, + PreJobHookPath: "", OnJobFinished: nil, CallbackRetryAttempts: 60, }) @@ -145,7 +175,7 @@ func (job *Job) RunWithOptions(options RunOptions) { } if executorRunning { - result = job.RunRegularCommands(options.EnvVars) + result = job.RunRegularCommands(options) log.Debug("Exporting job result") if result != JobStopped { @@ -186,8 +216,8 @@ func (job *Job) PrepareEnvironment() int { return 0 } -func (job *Job) RunRegularCommands(hostEnvVars []config.HostEnvVar) string { - exitCode := job.Executor.ExportEnvVars(job.Request.EnvVars, hostEnvVars) +func (job *Job) RunRegularCommands(options RunOptions) string { + exitCode := job.Executor.ExportEnvVars(job.Request.EnvVars, options.EnvVars) if exitCode != 0 { log.Error("Failed to export env vars") @@ -201,6 +231,11 @@ func (job *Job) RunRegularCommands(hostEnvVars []config.HostEnvVar) string { return JobFailed } + shouldProceed := job.runPreJobHook(options) + if !shouldProceed { + return JobFailed + } + if len(job.Request.Commands) == 0 { exitCode = 0 } else { @@ -219,6 +254,34 @@ func (job *Job) RunRegularCommands(hostEnvVars []config.HostEnvVar) string { } } +func (job *Job) runPreJobHook(options RunOptions) bool { + if options.PreJobHookPath == "" { + log.Info("No pre-job hook configured.") + return true + } + + log.Infof("Executing pre-job hook at %s", options.PreJobHookPath) + exitCode := job.Executor.RunCommandWithOptions(executors.CommandOptions{ + Command: options.GetPreJobHookCommand(), + Silent: false, + Alias: "Running the pre-job hook configured in the agent", + Warning: options.GetPreJobHookWarning(), + }) + + if exitCode == 0 { + log.Info("Pre-job hook executed successfully.") + return true + } + + if options.FailOnPreJobHookError { + log.Error("Error executing pre-job hook - failing job") + return false + } + + log.Error("Error executing pre-job hook - proceeding") + return true +} + func (job *Job) handleEpilogues(result string) { envVars := []api.EnvVar{ {Name: "SEMAPHORE_JOB_RESULT", Value: base64.RawStdEncoding.EncodeToString([]byte(result))}, diff --git a/pkg/jobs/job_test.go b/pkg/jobs/job_test.go index 2446454b..55304884 100644 --- a/pkg/jobs/job_test.go +++ b/pkg/jobs/job_test.go @@ -3,13 +3,17 @@ package jobs import ( "encoding/base64" "fmt" + "io/ioutil" "net/http" + "os" "os/exec" "runtime" + "strings" "testing" "time" "github.com/semaphoreci/agent/pkg/api" + "github.com/semaphoreci/agent/pkg/config" eventlogger "github.com/semaphoreci/agent/pkg/eventlogger" testsupport "github.com/semaphoreci/agent/test/support" "github.com/stretchr/testify/assert" @@ -951,3 +955,202 @@ func Test__BashSetPipefail(t *testing.T) { "job_finished: failed", }) } + +func Test__UsePreJobHook(t *testing.T) { + testLogger, testLoggerBackend := eventlogger.DefaultTestLogger() + request := &api.JobRequest{ + EnvVars: []api.EnvVar{}, + Commands: []api.Command{ + {Directive: testsupport.Output("hello")}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + }, + } + + job, err := NewJobWithOptions(&JobOptions{Request: request, Client: http.DefaultClient, Logger: testLogger}) + assert.Nil(t, err) + + hook, _ := testsupport.TempFileWithExtension() + _ = ioutil.WriteFile(hook, []byte(testsupport.Output("hello from pre-job hook")), 0777) + + job.RunWithOptions(RunOptions{ + EnvVars: []config.HostEnvVar{}, + FileInjections: []config.FileInjection{}, + PreJobHookPath: hook, + OnJobFinished: nil, + CallbackRetryAttempts: 1, + }) + + assert.True(t, job.Finished) + + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + testsupport.AssertSimplifiedJobLogs(t, simplifiedEvents, []string{ + "job_started", + + "directive: Exporting environment variables", + "Exit Code: 0", + + "directive: Injecting Files", + "Exit Code: 0", + + "directive: Running the pre-job hook configured in the agent", + "*** IGNORE SINGLE LINE ***", // we are using a temp file, it's hard to assert its path, just ignore it + "Warning: The agent is configured to proceed with the job even if the pre-job hook fails.\n", + "hello from pre-job hook", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.Output("hello")), + "hello", + "Exit Code: 0", + + "directive: Exporting environment variables", + "Exporting SEMAPHORE_JOB_RESULT\n", + "Exit Code: 0", + + "job_finished: passed", + }) + + os.Remove(hook) +} + +func Test__PreJobHookHasAccessToEnvVars(t *testing.T) { + testLogger, testLoggerBackend := eventlogger.DefaultTestLogger() + request := &api.JobRequest{ + EnvVars: []api.EnvVar{ + {Name: "A", Value: base64.StdEncoding.EncodeToString([]byte("VALUE_A"))}, + {Name: "B", Value: base64.StdEncoding.EncodeToString([]byte("VALUE_B"))}, + }, + Commands: []api.Command{ + {Directive: testsupport.Output("hello")}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + }, + } + + job, err := NewJobWithOptions(&JobOptions{Request: request, Client: http.DefaultClient, Logger: testLogger}) + assert.Nil(t, err) + + hook, _ := testsupport.TempFileWithExtension() + hookContent := []string{ + testsupport.EchoEnvVar("A"), + testsupport.Output(" - "), + testsupport.EchoEnvVar("B"), + } + + _ = ioutil.WriteFile(hook, []byte(strings.Join(hookContent, "\n")), 0777) + job.RunWithOptions(RunOptions{ + EnvVars: []config.HostEnvVar{}, + FileInjections: []config.FileInjection{}, + PreJobHookPath: hook, + OnJobFinished: nil, + CallbackRetryAttempts: 1, + }) + + assert.True(t, job.Finished) + + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + testsupport.AssertSimplifiedJobLogs(t, simplifiedEvents, []string{ + "job_started", + + "directive: Exporting environment variables", + "Exporting A\n", + "Exporting B\n", + "Exit Code: 0", + + "directive: Injecting Files", + "Exit Code: 0", + + "directive: Running the pre-job hook configured in the agent", + "*** IGNORE SINGLE LINE ***", // we are using a temp file, it's hard to assert its path, just ignore it + "Warning: The agent is configured to proceed with the job even if the pre-job hook fails.\n", + "VALUE_A - VALUE_B", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.Output("hello")), + "hello", + "Exit Code: 0", + + "directive: Exporting environment variables", + "Exporting SEMAPHORE_JOB_RESULT\n", + "Exit Code: 0", + + "job_finished: passed", + }) + + os.Remove(hook) +} + +func Test__UsePreJobHookAndFailOnError(t *testing.T) { + testLogger, testLoggerBackend := eventlogger.DefaultTestLogger() + request := &api.JobRequest{ + EnvVars: []api.EnvVar{}, + Commands: []api.Command{ + {Directive: testsupport.Output("hello")}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + }, + } + + job, err := NewJobWithOptions(&JobOptions{Request: request, Client: http.DefaultClient, Logger: testLogger}) + assert.Nil(t, err) + + hook, _ := testsupport.TempFileWithExtension() + _ = ioutil.WriteFile(hook, []byte("badcommand"), 0777) + + job.RunWithOptions(RunOptions{ + EnvVars: []config.HostEnvVar{}, + FileInjections: []config.FileInjection{}, + PreJobHookPath: hook, + FailOnPreJobHookError: true, + OnJobFinished: nil, + CallbackRetryAttempts: 1, + }) + + assert.True(t, job.Finished) + + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + assert.Nil(t, err) + + testsupport.AssertSimplifiedJobLogs(t, simplifiedEvents, []string{ + "job_started", + + "directive: Exporting environment variables", + "Exit Code: 0", + + "directive: Injecting Files", + "Exit Code: 0", + + "directive: Running the pre-job hook configured in the agent", + "*** IGNORE SINGLE LINE ***", // we are using a temp file, it's hard to assert its path, just ignore it + "Warning: The agent is configured to fail the job if the pre-job hook fails.\n", + "*** IGNORE LINES UNTIL EXIT CODE ***", // also hard to assert the actual error message, just ignore it + fmt.Sprintf("Exit Code: %d", testsupport.UnknownCommandExitCode()), + + "directive: Exporting environment variables", + "Exporting SEMAPHORE_JOB_RESULT\n", + "Exit Code: 0", + + "job_finished: failed", + }) + + os.Remove(hook) +} diff --git a/pkg/listener/job_processor.go b/pkg/listener/job_processor.go index 9cf23859..c0831ca3 100644 --- a/pkg/listener/job_processor.go +++ b/pkg/listener/job_processor.go @@ -30,9 +30,11 @@ func StartJobProcessor(httpClient *http.Client, apiClient *selfhostedapi.API, co GetJobRetryAttempts: config.GetJobRetryLimit, CallbackRetryAttempts: config.CallbackRetryLimit, ShutdownHookPath: config.ShutdownHookPath, + PreJobHookPath: config.PreJobHookPath, EnvVars: config.EnvVars, FileInjections: config.FileInjections, FailOnMissingFiles: config.FailOnMissingFiles, + FailOnPreJobHookError: config.FailOnPreJobHookError, ExitOnShutdown: config.ExitOnShutdown, } @@ -56,10 +58,12 @@ type JobProcessor struct { GetJobRetryAttempts int CallbackRetryAttempts int ShutdownHookPath string + PreJobHookPath string StopSync bool EnvVars []config.HostEnvVar FileInjections []config.FileInjection FailOnMissingFiles bool + FailOnPreJobHookError bool ExitOnShutdown bool ShutdownReason ShutdownReason } @@ -169,6 +173,8 @@ func (p *JobProcessor) RunJob(jobID string) { go job.RunWithOptions(jobs.RunOptions{ EnvVars: p.EnvVars, + PreJobHookPath: p.PreJobHookPath, + FailOnPreJobHookError: p.FailOnPreJobHookError, CallbackRetryAttempts: p.CallbackRetryAttempts, FileInjections: p.FileInjections, OnJobFinished: p.JobFinished, diff --git a/pkg/listener/listener.go b/pkg/listener/listener.go index 222683d9..b48d26d2 100644 --- a/pkg/listener/listener.go +++ b/pkg/listener/listener.go @@ -29,11 +29,13 @@ type Config struct { Token string Scheme string ShutdownHookPath string + PreJobHookPath string DisconnectAfterJob bool DisconnectAfterIdleSeconds int EnvVars []config.HostEnvVar FileInjections []config.FileInjection FailOnMissingFiles bool + FailOnPreJobHookError bool ExitOnShutdown bool AgentVersion string } diff --git a/pkg/listener/listener_test.go b/pkg/listener/listener_test.go index c679d1f8..cd646830 100644 --- a/pkg/listener/listener_test.go +++ b/pkg/listener/listener_test.go @@ -5,7 +5,6 @@ import ( "io/ioutil" "net/http" "os" - "runtime" "strings" "testing" "time" @@ -138,7 +137,7 @@ func Test__ShutdownHookIsExecuted(t *testing.T) { hubMockServer.Init() hubMockServer.UseLogsURL(loghubMockServer.URL()) - hook, err := tempFileWithExtension() + hook, err := testsupport.TempFileWithExtension() assert.Nil(t, err) /* @@ -191,7 +190,7 @@ func Test__ShutdownHookCanSeeShutdownReason(t *testing.T) { hubMockServer.Init() hubMockServer.UseLogsURL(loghubMockServer.URL()) - hook, err := tempFileWithExtension() + hook, err := testsupport.TempFileWithExtension() assert.Nil(t, err) /* @@ -756,21 +755,3 @@ func Test__ReportsFailedToConstructJob(t *testing.T) { hubMockServer.Close() loghubMockServer.Close() } - -func tempFileWithExtension() (string, error) { - tmpFile, err := ioutil.TempFile("", fmt.Sprintf("file*.%s", extension())) - if err != nil { - return "", err - } - - tmpFile.Close() - return tmpFile.Name(), nil -} - -func extension() string { - if runtime.GOOS == "windows" { - return "ps1" - } - - return "sh" -} diff --git a/test/support/commands.go b/test/support/commands.go index 7af1a117..18661d8f 100644 --- a/test/support/commands.go +++ b/test/support/commands.go @@ -19,12 +19,15 @@ func AssertSimplifiedJobLogs(t *testing.T, actual, expected []string) { actualLine := actual[actualIndex] expectedLine := expected[expectedIndex] - if expectedLine == "*** OUTPUT ***" { + if expectedLine == "*** OUTPUT ***" || expectedLine == "*** IGNORE LINES UNTIL EXIT CODE ***" { if strings.HasPrefix(actualLine, "Exit Code: ") { expectedIndex++ } else { actualIndex++ } + } else if expectedLine == "*** IGNORE SINGLE LINE ***" { + actualIndex++ + expectedIndex++ } else { if !assert.Equal(t, actualLine, expectedLine) { break diff --git a/test/support/files.go b/test/support/files.go new file mode 100644 index 00000000..30359cd0 --- /dev/null +++ b/test/support/files.go @@ -0,0 +1,25 @@ +package testsupport + +import ( + "fmt" + "io/ioutil" + "runtime" +) + +func TempFileWithExtension() (string, error) { + tmpFile, err := ioutil.TempFile("", fmt.Sprintf("file*.%s", extension())) + if err != nil { + return "", err + } + + tmpFile.Close() + return tmpFile.Name(), nil +} + +func extension() string { + if runtime.GOOS == "windows" { + return "ps1" + } + + return "sh" +} From 3bb946cc9549e73de7e62ca156c4a3a5edaf415d Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Fri, 12 Aug 2022 11:28:43 -0300 Subject: [PATCH 033/130] fix: always include agent name in logs (#165) --- main.go | 24 ++++++++++++++++++++++++ pkg/eventlogger/formatter.go | 28 ++++++++++++++++++++++++++-- pkg/listener/listener.go | 29 ++++------------------------- pkg/listener/listener_test.go | 15 +++++++++++++++ 4 files changed, 69 insertions(+), 27 deletions(-) diff --git a/main.go b/main.go index 5f567a74..0accbbcf 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "encoding/base64" "fmt" "io" "math/rand" @@ -157,7 +158,16 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { log.Fatalf("Error parsing --files: %v", err) } + agentName, err := randomName() + if err != nil { + log.Fatalf("Error generating name for agent: %v", err) + } + + formatter := eventlogger.CustomFormatter{AgentName: agentName} + log.SetFormatter(&formatter) + config := listener.Config{ + AgentName: agentName, Endpoint: viper.GetString(config.Endpoint), Token: viper.GetString(config.Token), RegisterRetryLimit: 30, @@ -312,3 +322,17 @@ func panicHandler(output string) { log.Printf("Child agent process panicked:\n\n%s\n", output) os.Exit(1) } + +// base64 gives you 4 chars every 3 bytes, we want 20 chars, so 15 bytes +const nameLength = 15 + +func randomName() (string, error) { + buffer := make([]byte, nameLength) + _, err := rand.Read(buffer) + + if err != nil { + return "", err + } + + return base64.URLEncoding.EncodeToString(buffer), nil +} diff --git a/pkg/eventlogger/formatter.go b/pkg/eventlogger/formatter.go index f049564a..2f60dc02 100644 --- a/pkg/eventlogger/formatter.go +++ b/pkg/eventlogger/formatter.go @@ -2,15 +2,39 @@ package eventlogger import ( "fmt" + "strings" "time" log "github.com/sirupsen/logrus" ) type CustomFormatter struct { + AgentName string } func (f *CustomFormatter) Format(entry *log.Entry) ([]byte, error) { - log := fmt.Sprintf("%-20s: %s\n", entry.Time.UTC().Format(time.StampMilli), entry.Message) - return []byte(log), nil + parts := []string{} + parts = append(parts, entry.Time.UTC().Format(time.StampMilli)) + + if f.AgentName != "" { + parts = append(parts, f.AgentName) + } + + extraFields := f.formatFields(entry.Data) + if extraFields != "" { + parts = append(parts, extraFields) + } + + parts = append(parts, ":") + parts = append(parts, fmt.Sprintf("%s\n", entry.Message)) + return []byte(strings.Join(parts, " ")), nil +} + +func (f *CustomFormatter) formatFields(fields log.Fields) string { + result := []string{} + for key, value := range fields { + result = append(result, fmt.Sprintf("%s=%s", key, value)) + } + + return strings.Join(result, " ") } diff --git a/pkg/listener/listener.go b/pkg/listener/listener.go index b48d26d2..fedb12e6 100644 --- a/pkg/listener/listener.go +++ b/pkg/listener/listener.go @@ -1,8 +1,6 @@ package listener import ( - "crypto/rand" - "encoding/base64" "fmt" "net/http" "os" @@ -38,6 +36,7 @@ type Config struct { FailOnPreJobHookError bool ExitOnShutdown bool AgentVersion string + AgentName string } func Start(httpClient *http.Client, config Config) (*Listener, error) { @@ -50,7 +49,7 @@ func Start(httpClient *http.Client, config Config) (*Listener, error) { log.Info("Starting Agent") log.Info("Registering Agent") - err := listener.Register() + err := listener.Register(config.AgentName) if err != nil { return listener, err } @@ -87,27 +86,7 @@ func (l *Listener) DisplayHelloMessage() { fmt.Println(" ") } -// base64 gives you 4 chars every 3 bytes, we want 20 chars, so 15 bytes -const nameLength = 15 - -func (l *Listener) Name() (string, error) { - buffer := make([]byte, nameLength) - _, err := rand.Read(buffer) - - if err != nil { - return "", err - } - - return base64.URLEncoding.EncodeToString(buffer), nil -} - -func (l *Listener) Register() error { - name, err := l.Name() - if err != nil { - log.Errorf("Error generating name for agent: %v", err) - return err - } - +func (l *Listener) Register(name string) error { req := &selfhostedapi.RegisterRequest{ Version: l.Config.AgentVersion, Name: name, @@ -119,7 +98,7 @@ func (l *Listener) Register() error { IdleTimeout: l.Config.DisconnectAfterIdleSeconds, } - err = retry.RetryWithConstantWait(retry.RetryOptions{ + err := retry.RetryWithConstantWait(retry.RetryOptions{ Task: "Register", MaxAttempts: l.Config.RegisterRetryLimit, DelayBetweenAttempts: time.Second, diff --git a/pkg/listener/listener_test.go b/pkg/listener/listener_test.go index cd646830..97aa25f5 100644 --- a/pkg/listener/listener_test.go +++ b/pkg/listener/listener_test.go @@ -3,6 +3,7 @@ package listener import ( "fmt" "io/ioutil" + "math/rand" "net/http" "os" "strings" @@ -28,6 +29,7 @@ func Test__Register(t *testing.T) { hubMockServer.UseLogsURL(loghubMockServer.URL()) config := Config{ + AgentName: fmt.Sprintf("agent-name-%d", rand.Intn(10000000)), ExitOnShutdown: false, Endpoint: hubMockServer.Host(), Token: "token", @@ -68,6 +70,7 @@ func Test__RegisterRequestIsRetried(t *testing.T) { hubMockServer.RejectRegisterAttempts(3) config := Config{ + AgentName: fmt.Sprintf("agent-name-%d", rand.Intn(10000000)), ExitOnShutdown: false, Endpoint: hubMockServer.Host(), Token: "token", @@ -109,6 +112,7 @@ func Test__RegistrationFails(t *testing.T) { hubMockServer.RejectRegisterAttempts(10) config := Config{ + AgentName: fmt.Sprintf("agent-name-%d", rand.Intn(10000000)), ExitOnShutdown: false, Endpoint: hubMockServer.Host(), Token: "token", @@ -151,6 +155,7 @@ func Test__ShutdownHookIsExecuted(t *testing.T) { assert.Nil(t, err) config := Config{ + AgentName: fmt.Sprintf("agent-name-%d", rand.Intn(10000000)), ExitOnShutdown: false, Endpoint: hubMockServer.Host(), Token: "token", @@ -202,6 +207,7 @@ func Test__ShutdownHookCanSeeShutdownReason(t *testing.T) { assert.Nil(t, err) config := Config{ + AgentName: fmt.Sprintf("agent-name-%d", rand.Intn(10000000)), ExitOnShutdown: false, Endpoint: hubMockServer.Host(), Token: "token", @@ -246,6 +252,7 @@ func Test__ShutdownAfterJobFinished(t *testing.T) { hubMockServer.UseLogsURL(loghubMockServer.URL()) config := Config{ + AgentName: fmt.Sprintf("agent-name-%d", rand.Intn(10000000)), ExitOnShutdown: false, DisconnectAfterJob: true, Endpoint: hubMockServer.Host(), @@ -294,6 +301,7 @@ func Test__ShutdownAfterIdleTimeout(t *testing.T) { hubMockServer.UseLogsURL(loghubMockServer.URL()) config := Config{ + AgentName: fmt.Sprintf("agent-name-%d", rand.Intn(10000000)), ExitOnShutdown: false, DisconnectAfterIdleSeconds: 15, Endpoint: hubMockServer.Host(), @@ -325,6 +333,7 @@ func Test__ShutdownFromUpstreamWhileWaiting(t *testing.T) { hubMockServer.UseLogsURL(loghubMockServer.URL()) config := Config{ + AgentName: fmt.Sprintf("agent-name-%d", rand.Intn(10000000)), ExitOnShutdown: false, Endpoint: hubMockServer.Host(), Token: "token", @@ -359,6 +368,7 @@ func Test__ShutdownFromUpstreamWhileRunningJob(t *testing.T) { hubMockServer.UseLogsURL(loghubMockServer.URL()) config := Config{ + AgentName: fmt.Sprintf("agent-name-%d", rand.Intn(10000000)), ExitOnShutdown: false, Endpoint: hubMockServer.Host(), Token: "token", @@ -409,6 +419,7 @@ func Test__HostEnvVarsAreExposedToJob(t *testing.T) { hubMockServer.UseLogsURL(loghubMockServer.URL()) config := Config{ + AgentName: fmt.Sprintf("agent-name-%d", rand.Intn(10000000)), ExitOnShutdown: false, Endpoint: hubMockServer.Host(), Token: "token", @@ -543,6 +554,7 @@ func Test__LogTokenIsRefreshed(t *testing.T) { hubMockServer.UseLogsURL(loghubMockServer.URL()) config := Config{ + AgentName: fmt.Sprintf("agent-name-%d", rand.Intn(10000000)), ExitOnShutdown: false, Endpoint: hubMockServer.Host(), Token: "token", @@ -619,6 +631,7 @@ func Test__GetJobIsRetried(t *testing.T) { hubMockServer.RejectGetJobAttempts(5) config := Config{ + AgentName: fmt.Sprintf("agent-name-%d", rand.Intn(10000000)), DisconnectAfterJob: true, ExitOnShutdown: false, Endpoint: hubMockServer.Host(), @@ -670,6 +683,7 @@ func Test__ReportsFailedToFetchJob(t *testing.T) { hubMockServer.RejectGetJobAttempts(100) config := Config{ + AgentName: fmt.Sprintf("agent-name-%d", rand.Intn(10000000)), DisconnectAfterJob: false, ExitOnShutdown: false, Endpoint: hubMockServer.Host(), @@ -718,6 +732,7 @@ func Test__ReportsFailedToConstructJob(t *testing.T) { hubMockServer.UseLogsURL(loghubMockServer.URL()) config := Config{ + AgentName: fmt.Sprintf("agent-name-%d", rand.Intn(10000000)), DisconnectAfterJob: false, ExitOnShutdown: false, Endpoint: hubMockServer.Host(), From 7f8174b39fa0494e333726c972e04bea5a3c26e9 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Tue, 16 Aug 2022 12:11:14 -0300 Subject: [PATCH 034/130] feat: set ECR registry (#166) --- .semaphore/semaphore.yml | 7 +- Dockerfile.ecr | 7 ++ Makefile | 8 ++ go.mod | 1 + go.sum | 2 + pkg/aws/aws.go | 100 ++++++++++++++++++ pkg/executors/docker_compose_executor.go | 7 +- test/e2e/docker/docker_private_image_ecr.rb | 5 +- .../docker_private_image_ecr_account_id.rb | 88 +++++++++++++++ .../docker_private_image_ecr_account_id_v2.rb | 92 ++++++++++++++++ .../e2e/docker/docker_private_image_ecr_v2.rb | 91 ++++++++++++++++ 11 files changed, 403 insertions(+), 5 deletions(-) create mode 100644 Dockerfile.ecr create mode 100644 pkg/aws/aws.go create mode 100644 test/e2e/docker/docker_private_image_ecr_account_id.rb create mode 100644 test/e2e/docker/docker_private_image_ecr_account_id_v2.rb create mode 100644 test/e2e/docker/docker_private_image_ecr_v2.rb diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 144cb8f3..831fc07d 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -79,7 +79,7 @@ blocks: dependencies: [] task: secrets: - - name: aws-ecr-payground + - name: aws-ecr-agent-e2e-secret - name: gcr-test-secret - name: docker-registry-test-secret env_vars: @@ -129,7 +129,10 @@ blocks: - container_options - dockerhub_private_image - docker_registry_private_image - # - docker_private_image_ecr + - docker_private_image_ecr + - docker_private_image_ecr_v2 + - docker_private_image_ecr_account_id + - docker_private_image_ecr_account_id_v2 - docker_private_image_gcr - dockerhub_private_image_bad_creds - docker_registry_private_image_bad_creds diff --git a/Dockerfile.ecr b/Dockerfile.ecr new file mode 100644 index 00000000..add7cd0b --- /dev/null +++ b/Dockerfile.ecr @@ -0,0 +1,7 @@ +FROM python:3 + +RUN apt-get update && \ + apt-get install curl -y && \ + curl -sSL https://get.docker.com/ | sh && \ + apt-get install -y ssh && \ + pip install docker-compose awscli diff --git a/Makefile b/Makefile index 18ac7bdf..b1ad2517 100644 --- a/Makefile +++ b/Makefile @@ -62,6 +62,14 @@ empty.ubuntu.machine: empty.ubuntu.machine.build: docker build -f Dockerfile.empty_ubuntu -t empty-ubuntu-self-hosted-agent . +ecr.test.build: + docker build -f Dockerfile.ecr -t agent-testing . + +ecr.test.push: + aws ecr get-login-password --region $(AWS_REGION) | docker login --username AWS --password-stdin $(AWS_ECR_REGISTRY) + docker tag agent-testing:latest $(AWS_ECR_REGISTRY)/agent-testing:latest + docker push $(AWS_ECR_REGISTRY)/agent-testing:latest + # # Docker Release # diff --git a/go.mod b/go.mod index cf82df50..5b4d5f3f 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ require ( github.com/golang-jwt/jwt/v4 v4.4.2 github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 + github.com/hashicorp/go-version v1.6.0 github.com/mitchellh/panicwrap v1.0.0 github.com/renderedtext/go-watchman v0.0.0-20220524201126-042727917d44 github.com/sirupsen/logrus v1.9.0 diff --git a/go.sum b/go.sum index f7c78f33..48963158 100644 --- a/go.sum +++ b/go.sum @@ -127,6 +127,8 @@ github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= diff --git a/pkg/aws/aws.go b/pkg/aws/aws.go new file mode 100644 index 00000000..284a76da --- /dev/null +++ b/pkg/aws/aws.go @@ -0,0 +1,100 @@ +package aws + +import ( + "fmt" + "os/exec" + "strings" + + versions "github.com/hashicorp/go-version" + log "github.com/sirupsen/logrus" +) + +func GetECRLoginCmd(envs []string) (string, error) { + awsV2, _ := versions.NewVersion("2.0.0") + awsCLIVersion, err := findAWSCLIVersion() + if err != nil { + return "", err + } + + if awsCLIVersion.GreaterThanOrEqual(awsV2) { + accountID := getAccountIDFromVars(envs) + if accountID == "" { + accountID, err = getAccountIDFromSTS(envs) + if err != nil { + return "", err + } + } + + /* + * get-login-password was added in v1.17.10 and is the only command available in v2. + * That command doesn't generate a docker login command by itself, only the password. + * So we need to pipe that into the docker login command ourselves. + * See: https://docs.aws.amazon.com/cli/latest/reference/ecr/get-login-password.html. + * The only difference here is that we need to determine the AWS account id for ourselves. + */ + return fmt.Sprintf( + `aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin %s.dkr.ecr.$AWS_REGION.amazonaws.com`, + accountID, + ), nil + } + + /* + * get-login is only available in AWS CLI v1. + * The way it works is it generates a token, and then prints the + * docker login command to actually login. Note the extra $() around it. + * This is to make sure we execute the output of that command as well. + * See: https://docs.aws.amazon.com/cli/latest/reference/ecr/get-login.html + */ + accountID := getAccountIDFromVars(envs) + if accountID == "" { + return `$(aws ecr get-login --no-include-email --region $AWS_REGION)`, nil + } + + /* + * If AWS_ACCOUNT_ID is specified in the env vars, the registry is + * possibly living in a separate AWS account, so we set --registry-ids. + */ + return fmt.Sprintf(`$(aws ecr get-login --no-include-email --region $AWS_REGION --registry-ids %s)`, accountID), nil +} + +func getAccountIDFromVars(envs []string) string { + for _, envVar := range envs { + parts := strings.Split(envVar, "=") + if parts[0] == "AWS_ACCOUNT_ID" { + return parts[1] + } + } + + return "" +} + +func getAccountIDFromSTS(envs []string) (string, error) { + cmd := exec.Command("bash", "-c", "aws sts get-caller-identity --query Account --output text") + cmd.Env = envs + + output, err := cmd.CombinedOutput() + if err != nil { + log.Errorf("Error finding AWS account ID: Output: %s - Error: %v", string(output), err) + return "", err + } + + return strings.TrimSuffix(string(output), "\n"), nil +} + +func findAWSCLIVersion() (*versions.Version, error) { + cmd := exec.Command("bash", "-c", `aws --version 2>&1 | awk -F'[/ ]' '{print $2}'`) + output, err := cmd.CombinedOutput() + if err != nil { + log.Errorf("Error determing AWS CLI version: Output '%s' - Error: %v", string(output), err) + return nil, err + } + + versionAsString := strings.TrimSuffix(string(output), "\n") + version, err := versions.NewVersion(versionAsString) + if err != nil { + log.Errorf("Error parsing AWS CLI version from '%s': %v", versionAsString, err) + return nil, err + } + + return version, nil +} diff --git a/pkg/executors/docker_compose_executor.go b/pkg/executors/docker_compose_executor.go index 0243907a..59c67b31 100644 --- a/pkg/executors/docker_compose_executor.go +++ b/pkg/executors/docker_compose_executor.go @@ -14,6 +14,7 @@ import ( watchman "github.com/renderedtext/go-watchman" api "github.com/semaphoreci/agent/pkg/api" + aws "github.com/semaphoreci/agent/pkg/aws" "github.com/semaphoreci/agent/pkg/config" eventlogger "github.com/semaphoreci/agent/pkg/eventlogger" shell "github.com/semaphoreci/agent/pkg/shell" @@ -392,7 +393,11 @@ func (e *DockerComposeExecutor) injectImagePullSecretsForECR(envVars []api.EnvVa envs = append(envs, fmt.Sprintf("%s=%s", name, string(value))) } - loginCmd := `$(aws ecr get-login --no-include-email --region $AWS_REGION)` + loginCmd, err := aws.GetECRLoginCmd(envs) + if err != nil { + e.Logger.LogCommandOutput(fmt.Sprintf("Failed to determine docker login command: %v\n", err)) + return 1 + } e.Logger.LogCommandOutput(loginCmd + "\n") diff --git a/test/e2e/docker/docker_private_image_ecr.rb b/test/e2e/docker/docker_private_image_ecr.rb index 4a072794..d5cf72ec 100644 --- a/test/e2e/docker/docker_private_image_ecr.rb +++ b/test/e2e/docker/docker_private_image_ecr.rb @@ -78,7 +78,8 @@ {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello World"} {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello World","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed"} - {"event":"cmd_finished", "timestamp":"*", "directive":"export SEMAPHORE_JOB_RESULT=passed","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} {"event":"job_finished", "timestamp":"*", "result":"passed"} LOG diff --git a/test/e2e/docker/docker_private_image_ecr_account_id.rb b/test/e2e/docker/docker_private_image_ecr_account_id.rb new file mode 100644 index 00000000..e278a4bf --- /dev/null +++ b/test/e2e/docker/docker_private_image_ecr_account_id.rb @@ -0,0 +1,88 @@ +#!/bin/ruby +# rubocop:disable all + +require_relative '../../e2e' + +aws_account_id = ENV['AWS_ACCOUNT_ID'] + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + + "executor": "dockercompose", + + "compose": { + "containers": [ + { + "name": "main", + "image": "#{ENV['AWS_IMAGE']}" + } + ], + + "image_pull_credentials": [ + { + "env_vars": [ + { "name": "DOCKER_CREDENTIAL_TYPE", "value": "#{Base64.strict_encode64("AWS_ECR")}" }, + { "name": "AWS_REGION", "value": "#{Base64.strict_encode64(ENV['AWS_REGION'])}" }, + { "name": "AWS_ACCESS_KEY_ID", "value": "#{Base64.strict_encode64(ENV['AWS_ACCESS_KEY_ID'])}" }, + { "name": "AWS_SECRET_ACCESS_KEY", "value": "#{Base64.strict_encode64(ENV['AWS_SECRET_ACCESS_KEY'])}" }, + { "name": "AWS_ACCOUNT_ID", "value": "#{Base64.strict_encode64(aws_account_id)}" } + ] + } + ] + }, + + "env_vars": [], + + "files": [], + + "commands": [ + { "directive": "echo Hello World" } + ], + + "epilogue_always_commands": [], + + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Setting up image pull credentials"} + {"event":"cmd_output", "timestamp":"*", "output":"Setting up credentials for ECR\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"$(aws ecr get-login --no-include-email --region $AWS_REGION --registry-ids #{aws_account_id})\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"WARNING! Using --password via the CLI is insecure. Use --password-stdin.\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"WARNING! Your password will be stored unencrypted in /root/.docker/config.json.\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Configure a credential helper to remove this warning. See\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"https://docs.docker.com/engine/reference/commandline/login/#credentials-store\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Login Succeeded\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Setting up image pull credentials", "exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Pulling docker images..."} + *** LONG_OUTPUT *** + {"event":"cmd_finished", "timestamp":"*", "directive":"Pulling docker images...", "exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Starting the docker image..."} + {"event":"cmd_output", "timestamp":"*", "output":"Starting a new bash session.\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Starting the docker image...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello World"} + {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello World","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} +LOG diff --git a/test/e2e/docker/docker_private_image_ecr_account_id_v2.rb b/test/e2e/docker/docker_private_image_ecr_account_id_v2.rb new file mode 100644 index 00000000..cee313ea --- /dev/null +++ b/test/e2e/docker/docker_private_image_ecr_account_id_v2.rb @@ -0,0 +1,92 @@ +#!/bin/ruby +# rubocop:disable all + +require_relative '../../e2e' + +aws_account_id = ENV['AWS_ACCOUNT_ID'] + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + + "executor": "dockercompose", + + "compose": { + "containers": [ + { + "name": "main", + "image": "#{ENV['AWS_IMAGE']}" + } + ], + + "image_pull_credentials": [ + { + "env_vars": [ + { "name": "DOCKER_CREDENTIAL_TYPE", "value": "#{Base64.strict_encode64("AWS_ECR")}" }, + { "name": "AWS_REGION", "value": "#{Base64.strict_encode64(ENV['AWS_REGION'])}" }, + { "name": "AWS_ACCESS_KEY_ID", "value": "#{Base64.strict_encode64(ENV['AWS_ACCESS_KEY_ID'])}" }, + { "name": "AWS_SECRET_ACCESS_KEY", "value": "#{Base64.strict_encode64(ENV['AWS_SECRET_ACCESS_KEY'])}" }, + { "name": "AWS_ACCOUNT_ID", "value": "#{Base64.strict_encode64(aws_account_id)}" } + ] + } + ], + "host_setup_commands": [ + { "directive": "curl 'https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip' -o 'awscliv2.zip'" }, + { "directive": "unzip awscliv2.zip" }, + { "directive": "./aws/install" } + ] + }, + + "env_vars": [], + + "files": [], + + "commands": [ + { "directive": "echo Hello World" } + ], + + "epilogue_always_commands": [], + + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Setting up image pull credentials"} + {"event":"cmd_output", "timestamp":"*", "output":"Setting up credentials for ECR\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin #{aws_account_id}.dkr.ecr.$AWS_REGION.amazonaws.com\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"WARNING! Your password will be stored unencrypted in /root/.docker/config.json.\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Configure a credential helper to remove this warning. See\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"https://docs.docker.com/engine/reference/commandline/login/#credentials-store\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Login Succeeded\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Setting up image pull credentials", "exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Pulling docker images..."} + *** LONG_OUTPUT *** + {"event":"cmd_finished", "timestamp":"*", "directive":"Pulling docker images...", "exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Starting the docker image..."} + {"event":"cmd_output", "timestamp":"*", "output":"Starting a new bash session.\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Starting the docker image...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello World"} + {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello World","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} +LOG diff --git a/test/e2e/docker/docker_private_image_ecr_v2.rb b/test/e2e/docker/docker_private_image_ecr_v2.rb new file mode 100644 index 00000000..50eec913 --- /dev/null +++ b/test/e2e/docker/docker_private_image_ecr_v2.rb @@ -0,0 +1,91 @@ +#!/bin/ruby +# rubocop:disable all + +require_relative '../../e2e' + +aws_account_id = ENV['AWS_ACCOUNT_ID'] + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + + "executor": "dockercompose", + + "compose": { + "containers": [ + { + "name": "main", + "image": "#{ENV['AWS_IMAGE']}" + } + ], + + "image_pull_credentials": [ + { + "env_vars": [ + { "name": "DOCKER_CREDENTIAL_TYPE", "value": "#{Base64.strict_encode64("AWS_ECR")}" }, + { "name": "AWS_REGION", "value": "#{Base64.strict_encode64(ENV['AWS_REGION'])}" }, + { "name": "AWS_ACCESS_KEY_ID", "value": "#{Base64.strict_encode64(ENV['AWS_ACCESS_KEY_ID'])}" }, + { "name": "AWS_SECRET_ACCESS_KEY", "value": "#{Base64.strict_encode64(ENV['AWS_SECRET_ACCESS_KEY'])}" } + ] + } + ], + "host_setup_commands": [ + { "directive": "curl 'https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip' -o 'awscliv2.zip'" }, + { "directive": "unzip awscliv2.zip" }, + { "directive": "./aws/install" } + ] + }, + + "env_vars": [], + + "files": [], + + "commands": [ + { "directive": "echo Hello World" } + ], + + "epilogue_always_commands": [], + + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Setting up image pull credentials"} + {"event":"cmd_output", "timestamp":"*", "output":"Setting up credentials for ECR\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin #{aws_account_id}.dkr.ecr.$AWS_REGION.amazonaws.com\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"WARNING! Your password will be stored unencrypted in /root/.docker/config.json.\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Configure a credential helper to remove this warning. See\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"https://docs.docker.com/engine/reference/commandline/login/#credentials-store\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Login Succeeded\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Setting up image pull credentials", "exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Pulling docker images..."} + *** LONG_OUTPUT *** + {"event":"cmd_finished", "timestamp":"*", "directive":"Pulling docker images...", "exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Starting the docker image..."} + {"event":"cmd_output", "timestamp":"*", "output":"Starting a new bash session.\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Starting the docker image...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello World"} + {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello World","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} +LOG From c5842b9a838acfc679b3b016118722160a927306 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Wed, 17 Aug 2022 14:36:54 -0300 Subject: [PATCH 035/130] feat: configure systemd restart options during installation (#168) --- install.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index d15eb5c4..550b997e 100755 --- a/install.sh +++ b/install.sh @@ -90,6 +90,9 @@ echo "Creating agent config file at $AGENT_CONFIG_PATH..." echo "$AGENT_CONFIG" > $AGENT_CONFIG_PATH sudo chown $SEMAPHORE_AGENT_INSTALLATION_USER:$SEMAPHORE_AGENT_INSTALLATION_USER $AGENT_CONFIG_PATH +SEMAPHORE_AGENT_SYSTEMD_RESTART=${SEMAPHORE_AGENT_SYSTEMD_RESTART:-always} +SEMAPHORE_AGENT_SYSTEMD_RESTART_SEC=${SEMAPHORE_AGENT_SYSTEMD_RESTART_SEC:-60} + # # Create systemd service # @@ -101,8 +104,8 @@ StartLimitIntervalSec=0 [Service] Type=simple -Restart=always -RestartSec=5 +Restart=$SEMAPHORE_AGENT_SYSTEMD_RESTART +RestartSec=$SEMAPHORE_AGENT_SYSTEMD_RESTART_SEC User=$SEMAPHORE_AGENT_INSTALLATION_USER WorkingDirectory=$AGENT_INSTALLATION_DIRECTORY ExecStart=$AGENT_INSTALLATION_DIRECTORY/agent start --config-file $AGENT_CONFIG_PATH From 937d96c70eb64b337dcf5113dcf853f9a6e7e632 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Wed, 17 Aug 2022 18:04:01 -0300 Subject: [PATCH 036/130] fix: always use /tmp for SSH jump point (#169) --- pkg/executors/shell_executor_test.go | 20 ++++++++++++++++++-- pkg/executors/ssh_jump_point.go | 16 ++++++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/pkg/executors/shell_executor_test.go b/pkg/executors/shell_executor_test.go index 4540032b..b873eab0 100644 --- a/pkg/executors/shell_executor_test.go +++ b/pkg/executors/shell_executor_test.go @@ -21,15 +21,31 @@ var UnicodeOutput1 = `特定の伝説に拠る物語の由来については諸 var UnicodeOutput2 = `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━` func Test__ShellExecutor__SSHJumpPointIsCreatedForHosted(t *testing.T) { - sshJumpPointPath := filepath.Join(os.TempDir(), "ssh_jump_point") + if runtime.GOOS == "windows" { + t.Skip() + } + + sshJumpPointPath := "/tmp/ssh_jump_point" os.Remove(sshJumpPointPath) _, _ = setupShellExecutor(t, false) assert.FileExists(t, sshJumpPointPath) os.Remove(sshJumpPointPath) } +func Test__ShellExecutor__SSHJumpPointIsNotCreatedForWindows(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip() + } + + sshJumpPointPath := "/tmp/ssh_jump_point" + os.Remove(sshJumpPointPath) + _, _ = setupShellExecutor(t, false) + assert.NoFileExists(t, sshJumpPointPath) + os.Remove(sshJumpPointPath) +} + func Test__ShellExecutor__SSHJumpPointIsNotCreatedForSelfHosted(t *testing.T) { - sshJumpPointPath := filepath.Join(os.TempDir(), "ssh_jump_point") + sshJumpPointPath := "/tmp/ssh_jump_point" os.Remove(sshJumpPointPath) _, _ = setupShellExecutor(t, true) assert.NoFileExists(t, sshJumpPointPath) diff --git a/pkg/executors/ssh_jump_point.go b/pkg/executors/ssh_jump_point.go index 5494b559..e62d1360 100644 --- a/pkg/executors/ssh_jump_point.go +++ b/pkg/executors/ssh_jump_point.go @@ -2,11 +2,23 @@ package executors import ( "os" - "path/filepath" + "runtime" + + log "github.com/sirupsen/logrus" ) func SetUpSSHJumpPoint(script string) error { - path := filepath.Join(os.TempDir(), "ssh_jump_point") + if runtime.GOOS == "windows" { + log.Warn("Debug sessions are not supported in Windows - skipping") + return nil + } + + /* + * We can't use os.TempDir() here, because on macOS, + * $TMPDIR resolves to something like /var/folders/rg/92ky7bj54xj6pcv5l24g6l_00000gn/T/, + * and the sem CLI needs a /tmp/ssh_jump_point file. + */ + path := "/tmp/ssh_jump_point" // #nosec f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0644) From d086a01cfefe2efb4bdf0cfb59a355c0f966434d Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Mon, 22 Aug 2022 09:26:25 -0300 Subject: [PATCH 037/130] feat: launchd support in installation script (#167) --- install.sh | 205 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 156 insertions(+), 49 deletions(-) diff --git a/install.sh b/install.sh index 550b997e..d5dc51f0 100755 --- a/install.sh +++ b/install.sh @@ -3,6 +3,130 @@ set -e set -o pipefail +# +# Creates a 'semaphore-agent' systemd service. +# If it already exists, it will be overriden. +# SEMAPHORE_AGENT_START controls whether the service will be started as well. +# +create_systemd_service() { + SYSTEMD_SERVICE=$(cat <<-END +[Unit] +Description=Semaphore agent +After=network.target +StartLimitIntervalSec=0 + +[Service] +Type=simple +Restart=$SEMAPHORE_AGENT_SYSTEMD_RESTART +RestartSec=$SEMAPHORE_AGENT_SYSTEMD_RESTART_SEC +User=$SEMAPHORE_AGENT_INSTALLATION_USER +WorkingDirectory=$AGENT_INSTALLATION_DIRECTORY +ExecStart=$AGENT_INSTALLATION_DIRECTORY/agent start --config-file $AGENT_CONFIG_PATH + +[Install] +WantedBy=multi-user.target +END + ) + + SYSTEMD_PATH=/etc/systemd/system + SERVICE_NAME=semaphore-agent + SYSTEMD_SERVICE_PATH=$SYSTEMD_PATH/$SERVICE_NAME.service + + echo "Creating $SYSTEMD_SERVICE_PATH..." + + if [[ -f "$SYSTEMD_SERVICE_PATH" ]]; then + echo "systemd service already exists at $SYSTEMD_SERVICE_PATH. Overriding it..." + echo "$SYSTEMD_SERVICE" > $SYSTEMD_SERVICE_PATH + systemctl daemon-reload + if [[ "$SEMAPHORE_AGENT_START" == "false" ]]; then + echo "Not restarting agent." + else + echo "Restarting semaphore-agent service..." + systemctl restart semaphore-agent + fi + else + echo "$SYSTEMD_SERVICE" > $SYSTEMD_SERVICE_PATH + if [[ "$SEMAPHORE_AGENT_START" == "false" ]]; then + echo "Not starting agent." + else + echo "Starting semaphore-agent service..." + systemctl start semaphore-agent + fi + fi +} + +# +# Creates a 'com.semaphoreci.agent' launchd daemon, at /Library/LaunchDaemons. +# If it already exists, it will be overriden. SEMAPHORE_AGENT_START controls +# whether the daemon will be started as well. +# +create_launchd_daemon() { + LAUNCHD_DAEMON_LABEL=com.semaphoreci.agent + LAUNCHD_DAEMON=$(cat <<-END + + + + + Label + $LAUNCHD_DAEMON_LABEL + ProgramArguments + + $AGENT_INSTALLATION_DIRECTORY/agent + start + --config-file + $AGENT_CONFIG_PATH + + RunAtLoad + + KeepAlive + + Crashed + + + UserName + $SEMAPHORE_AGENT_INSTALLATION_USER + WorkingDirectory + $AGENT_INSTALLATION_DIRECTORY + + +END + ) + + LAUNCHD_PATH=/Library/LaunchDaemons + LAUNCHD_DAEMON_PATH=$LAUNCHD_PATH/$LAUNCHD_DAEMON_LABEL.plist + + echo "Creating $LAUNCHD_DAEMON_PATH..." + + if [[ -f "$LAUNCHD_DAEMON_PATH" ]]; then + echo "launchd daemon already exists at $LAUNCHD_DAEMON_PATH. Overriding and reloading it..." + launchctl unload $LAUNCHD_DAEMON_PATH + echo "$LAUNCHD_DAEMON" > $LAUNCHD_DAEMON_PATH + launchctl load $LAUNCHD_DAEMON_PATH + + if [[ "$SEMAPHORE_AGENT_START" == "false" ]]; then + echo "Not restarting $LAUNCHD_DAEMON_LABEL." + else + echo "Restarting $LAUNCHD_DAEMON_LABEL service..." + launchctl start $LAUNCHD_DAEMON_LABEL + fi + else + echo "$LAUNCHD_DAEMON" > $LAUNCHD_DAEMON_PATH + launchctl load $LAUNCHD_DAEMON_PATH + + if [[ "$SEMAPHORE_AGENT_START" == "false" ]]; then + echo "Not starting $LAUNCHD_DAEMON_LABEL." + else + echo "Starting $LAUNCHD_DAEMON_LABEL service..." + launchctl start $LAUNCHD_DAEMON_LABEL + fi + fi +} + +# +# Main script +# + +DIST=$(uname) AGENT_INSTALLATION_DIRECTORY="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" if [[ "$EUID" -ne 0 ]]; then @@ -61,7 +185,15 @@ fi tar -xf toolbox.tar mv toolbox $TOOLBOX_DIRECTORY -sudo chown -R $SEMAPHORE_AGENT_INSTALLATION_USER:$SEMAPHORE_AGENT_INSTALLATION_USER $TOOLBOX_DIRECTORY + +case $DIST in + Darwin) + sudo chown -R $SEMAPHORE_AGENT_INSTALLATION_USER $TOOLBOX_DIRECTORY + ;; + Linux) + sudo chown -R $SEMAPHORE_AGENT_INSTALLATION_USER:$SEMAPHORE_AGENT_INSTALLATION_USER $TOOLBOX_DIRECTORY + ;; +esac sudo -u $SEMAPHORE_AGENT_INSTALLATION_USER -H bash $TOOLBOX_DIRECTORY/install-toolbox echo "source ~/.toolbox/toolbox" >> $USER_HOME_DIRECTORY/.bash_profile @@ -88,57 +220,32 @@ END AGENT_CONFIG_PATH="$AGENT_INSTALLATION_DIRECTORY/config.yaml" echo "Creating agent config file at $AGENT_CONFIG_PATH..." echo "$AGENT_CONFIG" > $AGENT_CONFIG_PATH -sudo chown $SEMAPHORE_AGENT_INSTALLATION_USER:$SEMAPHORE_AGENT_INSTALLATION_USER $AGENT_CONFIG_PATH +case $DIST in + Darwin) + sudo chown $SEMAPHORE_AGENT_INSTALLATION_USER $AGENT_CONFIG_PATH + ;; + Linux) + sudo chown $SEMAPHORE_AGENT_INSTALLATION_USER:$SEMAPHORE_AGENT_INSTALLATION_USER $AGENT_CONFIG_PATH + ;; +esac SEMAPHORE_AGENT_SYSTEMD_RESTART=${SEMAPHORE_AGENT_SYSTEMD_RESTART:-always} SEMAPHORE_AGENT_SYSTEMD_RESTART_SEC=${SEMAPHORE_AGENT_SYSTEMD_RESTART_SEC:-60} # -# Create systemd service +# Check if we can use some kind of service manager to run the agent. +# We use systemd for Linux, and launchd for MacOS. # -SYSTEMD_SERVICE=$(cat <<-END -[Unit] -Description=Semaphore agent -After=network.target -StartLimitIntervalSec=0 - -[Service] -Type=simple -Restart=$SEMAPHORE_AGENT_SYSTEMD_RESTART -RestartSec=$SEMAPHORE_AGENT_SYSTEMD_RESTART_SEC -User=$SEMAPHORE_AGENT_INSTALLATION_USER -WorkingDirectory=$AGENT_INSTALLATION_DIRECTORY -ExecStart=$AGENT_INSTALLATION_DIRECTORY/agent start --config-file $AGENT_CONFIG_PATH - -[Install] -WantedBy=multi-user.target -END -) - -SYSTEMD_PATH=/etc/systemd/system -SERVICE_NAME=semaphore-agent -SYSTEMD_SERVICE_PATH=$SYSTEMD_PATH/$SERVICE_NAME.service - -echo "Creating $SYSTEMD_SERVICE_PATH..." - -if [[ -f "$SYSTEMD_SERVICE_PATH" ]]; then - echo "systemd service already exists at $SYSTEMD_SERVICE_PATH. Overriding it..." - echo "$SYSTEMD_SERVICE" > $SYSTEMD_SERVICE_PATH - systemctl daemon-reload - if [[ "$SEMAPHORE_AGENT_START" == "false" ]]; then - echo "Not restarting agent." - else - echo "Restarting semaphore-agent service..." - systemctl restart semaphore-agent - fi -else - echo "$SYSTEMD_SERVICE" > $SYSTEMD_SERVICE_PATH - if [[ "$SEMAPHORE_AGENT_START" == "false" ]]; then - echo "Not starting agent." - else - echo "Starting semaphore-agent service..." - systemctl start semaphore-agent - fi -fi - -echo "Done." \ No newline at end of file +case $DIST in + Darwin) + create_launchd_daemon + ;; + Linux) + create_systemd_service + ;; + *) + echo "$DIST is not supported. You can still start the agent with '$AGENT_INSTALLATION_DIRECTORY/agent start --config $AGENT_CONFIG_PATH'." + ;; +esac + +echo "Done." From fc094fecd7110c2c913cd9e1810d4022be305dc7 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Mon, 22 Aug 2022 10:09:58 -0300 Subject: [PATCH 038/130] feat: allow name to be specified (#170) --- main.go | 26 +++++++++++++++++++++----- pkg/config/config.go | 2 ++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index 0accbbcf..a77d5e64 100644 --- a/main.go +++ b/main.go @@ -106,6 +106,7 @@ func getLogFilePath() string { func RunListener(httpClient *http.Client, logfile io.Writer) { configFile := pflag.String(config.ConfigFile, "", "Config file") + _ = pflag.String(config.Name, "", "Name to use for the agent. If not set, a default random one is used.") _ = pflag.String(config.Endpoint, "", "Endpoint where agents are registered") _ = pflag.String(config.Token, "", "Registration token") _ = pflag.Bool(config.NoHTTPS, false, "Use http for communication") @@ -158,11 +159,7 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { log.Fatalf("Error parsing --files: %v", err) } - agentName, err := randomName() - if err != nil { - log.Fatalf("Error generating name for agent: %v", err) - } - + agentName := getAgentName() formatter := eventlogger.CustomFormatter{AgentName: agentName} log.SetFormatter(&formatter) @@ -225,6 +222,25 @@ func validateConfiguration() { } } +func getAgentName() string { + agentName := viper.GetString(config.Name) + if agentName != "" { + if len(agentName) < 8 || len(agentName) > 64 { + log.Fatalf("The agent name should have between 8 and 64 characters. '%s' has %d.", agentName, len(agentName)) + } + + return agentName + } + + log.Infof("Agent name was not assigned - using a random one.") + randomName, err := randomName() + if err != nil { + log.Fatalf("Error generating name for agent: %v", err) + } + + return randomName +} + func ParseEnvVars() ([]config.HostEnvVar, error) { vars := []config.HostEnvVar{} for _, envVar := range viper.GetStringSlice(config.EnvVars) { diff --git a/pkg/config/config.go b/pkg/config/config.go index d563c748..cc148591 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -4,6 +4,7 @@ import "os" const ( ConfigFile = "config-file" + Name = "name" Endpoint = "endpoint" Token = "token" NoHTTPS = "no-https" @@ -19,6 +20,7 @@ const ( var ValidConfigKeys = []string{ ConfigFile, + Name, Endpoint, Token, NoHTTPS, From b0f19d81e20384b32ab371468b6050576ce8e2fc Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Mon, 22 Aug 2022 15:32:17 -0300 Subject: [PATCH 039/130] feat: upload trimmed logs as artifact (#164) --- main.go | 12 +++++ pkg/config/config.go | 16 ++++++ pkg/eventlogger/backend.go | 8 +++ pkg/eventlogger/filebackend.go | 6 ++- pkg/eventlogger/httpbackend.go | 30 ++++++----- pkg/eventlogger/httpbackend_test.go | 31 ++++++++++- pkg/eventlogger/inmemorybackend.go | 9 ++++ pkg/eventlogger/logger.go | 80 +++++++++++++++++++++++++++++ pkg/eventlogger/logger_test.go | 47 +++++++++++++++++ pkg/jobs/job.go | 43 +++++++++++++++- pkg/listener/job_processor.go | 3 ++ pkg/listener/listener.go | 1 + pkg/server/server.go | 11 +--- 13 files changed, 272 insertions(+), 25 deletions(-) create mode 100644 pkg/eventlogger/logger_test.go diff --git a/main.go b/main.go index a77d5e64..1e25ac51 100644 --- a/main.go +++ b/main.go @@ -117,6 +117,7 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { _ = pflag.StringSlice(config.EnvVars, []string{}, "Export environment variables in jobs") _ = pflag.StringSlice(config.Files, []string{}, "Inject files into container, when using docker compose executor") _ = pflag.Bool(config.FailOnMissingFiles, false, "Fail job if files specified using --files are missing") + _ = pflag.String(config.UploadJobLogs, config.UploadJobLogsConditionNever, "When should the agent upload the job logs as a job artifact. Default is never.") _ = pflag.Bool(config.FailOnPreJobHookError, false, "Fail job if pre-job hook fails") pflag.Parse() @@ -178,6 +179,7 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { EnvVars: hostEnvVars, FileInjections: fileInjections, FailOnMissingFiles: viper.GetBool(config.FailOnMissingFiles), + UploadJobLogs: viper.GetString(config.UploadJobLogs), FailOnPreJobHookError: viper.GetBool(config.FailOnPreJobHookError), AgentVersion: VERSION, ExitOnShutdown: true, @@ -220,6 +222,16 @@ func validateConfiguration() { log.Fatalf("Unrecognized option '%s'. Exiting...", key) } } + + uploadJobLogs := viper.GetString(config.UploadJobLogs) + if !contains(config.ValidUploadJobLogsCondition, uploadJobLogs) { + log.Fatalf( + "Unsupported value '%s' for '%s'. Allowed values are: %v. Exiting...", + uploadJobLogs, + config.UploadJobLogs, + config.ValidUploadJobLogsCondition, + ) + } } func getAgentName() string { diff --git a/pkg/config/config.go b/pkg/config/config.go index cc148591..787c5b2f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -15,9 +15,24 @@ const ( EnvVars = "env-vars" Files = "files" FailOnMissingFiles = "fail-on-missing-files" + UploadJobLogs = "upload-job-logs" FailOnPreJobHookError = "fail-on-pre-job-hook-error" ) +type UploadJobLogsCondition string + +const ( + UploadJobLogsConditionNever = "never" + UploadJobLogsConditionAlways = "always" + UploadJobLogsConditionWhenTrimmed = "when-trimmed" +) + +var ValidUploadJobLogsCondition = []string{ + UploadJobLogsConditionNever, + UploadJobLogsConditionAlways, + UploadJobLogsConditionWhenTrimmed, +} + var ValidConfigKeys = []string{ ConfigFile, Name, @@ -31,6 +46,7 @@ var ValidConfigKeys = []string{ EnvVars, Files, FailOnMissingFiles, + UploadJobLogs, FailOnPreJobHookError, } diff --git a/pkg/eventlogger/backend.go b/pkg/eventlogger/backend.go index 392361b6..65ea36da 100644 --- a/pkg/eventlogger/backend.go +++ b/pkg/eventlogger/backend.go @@ -1,9 +1,17 @@ package eventlogger +import "io" + type Backend interface { Open() error Write(interface{}) error + Read(startFrom, maxLines int, writer io.Writer) (int, error) Close() error + CloseWithOptions(CloseOptions) error +} + +type CloseOptions struct { + OnClose func(bool) } var _ Backend = (*FileBackend)(nil) diff --git a/pkg/eventlogger/filebackend.go b/pkg/eventlogger/filebackend.go index f8ca622b..35a2603b 100644 --- a/pkg/eventlogger/filebackend.go +++ b/pkg/eventlogger/filebackend.go @@ -48,6 +48,10 @@ func (l *FileBackend) Write(event interface{}) error { } func (l *FileBackend) Close() error { + return l.CloseWithOptions(CloseOptions{}) +} + +func (l *FileBackend) CloseWithOptions(options CloseOptions) error { err := l.file.Close() if err != nil { log.Errorf("Error closing file %s: %v\n", l.file.Name(), err) @@ -63,7 +67,7 @@ func (l *FileBackend) Close() error { return nil } -func (l *FileBackend) Stream(startingLineNumber, maxLines int, writer io.Writer) (int, error) { +func (l *FileBackend) Read(startingLineNumber, maxLines int, writer io.Writer) (int, error) { fd, err := os.OpenFile(l.path, os.O_RDONLY, os.ModePerm) if err != nil { return startingLineNumber, err diff --git a/pkg/eventlogger/httpbackend.go b/pkg/eventlogger/httpbackend.go index 7887b0f6..79b411f4 100644 --- a/pkg/eventlogger/httpbackend.go +++ b/pkg/eventlogger/httpbackend.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" "fmt" + "io" "net/http" "os" "path/filepath" @@ -27,6 +28,7 @@ type HTTPBackend struct { config HTTPBackendConfig stop bool flush bool + useArtifact bool } type HTTPBackendConfig struct { @@ -72,6 +74,10 @@ func (l *HTTPBackend) Write(event interface{}) error { return l.fileBackend.Write(event) } +func (l *HTTPBackend) Read(startFrom, maxLines int, writer io.Writer) (int, error) { + return l.fileBackend.Read(startFrom, maxLines, writer) +} + func (l *HTTPBackend) push() { log.Infof("Logs will be pushed to %s", l.config.URL) @@ -136,7 +142,7 @@ func (l *HTTPBackend) delay() time.Duration { func (l *HTTPBackend) newRequest() error { buffer := bytes.NewBuffer([]byte{}) - nextStartFrom, err := l.fileBackend.Stream(l.startFrom, l.config.LinesPerRequest, buffer) + nextStartFrom, err := l.fileBackend.Read(l.startFrom, l.config.LinesPerRequest, buffer) if err != nil { return err } @@ -183,6 +189,7 @@ func (l *HTTPBackend) newRequest() error { // The API will keep rejecting the requests if we keep sending them, so just stop. case http.StatusUnprocessableEntity: l.stop = true + l.useArtifact = true return errors.New("no more space available for logs - stopping") // The token issued for the agent expired. @@ -203,18 +210,9 @@ func (l *HTTPBackend) newRequest() error { } } -func (l *HTTPBackend) Close() error { - +func (l *HTTPBackend) CloseWithOptions(options CloseOptions) error { /* - * If we have already stopped pushing logs - * due to no more space available, we just proceed. - */ - if l.stop { - return l.fileBackend.Close() - } - - /* - * If not, we try to flush all the remaining logs. + * Try to flush all the remaining logs. * We wait for them to be flushed for a period of time (60s). * If they are not yet completely flushed after that period of time, we give up. */ @@ -235,6 +233,10 @@ func (l *HTTPBackend) Close() error { }, }) + if options.OnClose != nil { + options.OnClose(l.useArtifact) + } + if err != nil { log.Errorf("Could not push all logs to %s - giving up", l.config.URL) } @@ -242,3 +244,7 @@ func (l *HTTPBackend) Close() error { l.stop = true return l.fileBackend.Close() } + +func (l *HTTPBackend) Close() error { + return l.CloseWithOptions(CloseOptions{}) +} diff --git a/pkg/eventlogger/httpbackend_test.go b/pkg/eventlogger/httpbackend_test.go index 8943615c..9c7a9caf 100644 --- a/pkg/eventlogger/httpbackend_test.go +++ b/pkg/eventlogger/httpbackend_test.go @@ -206,6 +206,35 @@ func Test__FlushingGivesUpAfterTimeout(t *testing.T) { mockServer.Close() } +func Test__ExecutesOnCloseCallback(t *testing.T) { + mockServer := testsupport.NewLoghubMockServer() + mockServer.Init() + mockServer.SetMaxSizeForLogs(30) + + httpBackend, err := NewHTTPBackend(HTTPBackendConfig{ + URL: mockServer.URL(), + Token: "token", + RefreshTokenFn: func() (string, error) { return "", nil }, + LinesPerRequest: 10, + FlushTimeoutInSeconds: 10, + }) + + assert.Nil(t, err) + assert.Nil(t, httpBackend.Open()) + + // 1000+ log events at 10 per request would take 4 requests + // to go over the max size of 30 events. + generateLogEvents(t, 1000, httpBackend) + + callbackExecuted := false + _ = httpBackend.CloseWithOptions(CloseOptions{OnClose: func(trimmed bool) { + callbackExecuted = true + }}) + + assert.True(t, callbackExecuted) + mockServer.Close() +} + func Test__TokenIsRefreshed(t *testing.T) { mockServer := testsupport.NewLoghubMockServer() mockServer.Init() @@ -249,7 +278,7 @@ func Test__TokenIsRefreshed(t *testing.T) { mockServer.Close() } -func generateLogEvents(t *testing.T, outputEventsCount int, backend *HTTPBackend) { +func generateLogEvents(t *testing.T, outputEventsCount int, backend Backend) { timestamp := int(time.Now().Unix()) assert.Nil(t, backend.Write(&JobStartedEvent{Timestamp: timestamp, Event: "job_started"})) diff --git a/pkg/eventlogger/inmemorybackend.go b/pkg/eventlogger/inmemorybackend.go index 010a27c4..e1a5a19a 100644 --- a/pkg/eventlogger/inmemorybackend.go +++ b/pkg/eventlogger/inmemorybackend.go @@ -1,6 +1,7 @@ package eventlogger import ( + "io" "strings" ) @@ -16,6 +17,10 @@ func (l *InMemoryBackend) Open() error { return nil } +func (l *InMemoryBackend) Read(startFrom, maxLines int, writer io.Writer) (int, error) { + return 0, nil +} + func (l *InMemoryBackend) Write(event interface{}) error { l.Events = append(l.Events, event) @@ -26,6 +31,10 @@ func (l *InMemoryBackend) Close() error { return nil } +func (l *InMemoryBackend) CloseWithOptions(options CloseOptions) error { + return nil +} + func (l *InMemoryBackend) SimplifiedEvents(includeOutput bool) ([]string, error) { return SimplifyLogEvents(l.Events, includeOutput) } diff --git a/pkg/eventlogger/logger.go b/pkg/eventlogger/logger.go index 922ad314..843b4c84 100644 --- a/pkg/eventlogger/logger.go +++ b/pkg/eventlogger/logger.go @@ -1,6 +1,10 @@ package eventlogger import ( + "bytes" + "encoding/json" + "io/ioutil" + "strings" "time" log "github.com/sirupsen/logrus" @@ -22,6 +26,82 @@ func (l *Logger) Close() error { return l.Backend.Close() } +func (l *Logger) CloseWithOptions(options CloseOptions) error { + return l.Backend.CloseWithOptions(options) +} + +/* + * Convert the JSON logs file into a plain text one. + * Note: the caller must delete the generated plain text file after it's done with it. + */ +func (l *Logger) GeneratePlainTextFile() (string, error) { + tmpFile, err := ioutil.TempFile("", "*.txt") + if err != nil { + return "", err + } + + defer tmpFile.Close() + + /* + * Since we are only doing this for possibly very big files, + * we read/write things in chunks to avoid keeping a lot of things in memory. + */ + startFrom := 0 + var buf bytes.Buffer + for { + nextStartFrom, err := l.Backend.Read(startFrom, 20000, &buf) + if err != nil { + return "", err + } + + if nextStartFrom == startFrom { + break + } + + startFrom = nextStartFrom + logEvents := strings.Split(buf.String(), "\n") + logs, err := l.eventsToPlainLogLines(logEvents) + if err != nil { + return "", err + } + + newLines := []byte(strings.Join(logs, "")) + err = ioutil.WriteFile(tmpFile.Name(), newLines, 0755) + if err != nil { + return "", err + } + } + + return tmpFile.Name(), nil +} + +func (l *Logger) eventsToPlainLogLines(logEvents []string) ([]string, error) { + lines := []string{} + var object map[string]interface{} + + for _, logEvent := range logEvents { + if logEvent == "" { + continue + } + + err := json.Unmarshal([]byte(logEvent), &object) + if err != nil { + return []string{}, err + } + + switch eventType := object["event"].(string); { + case eventType == "cmd_started": + lines = append(lines, object["directive"].(string)+"\n") + case eventType == "cmd_output": + lines = append(lines, object["output"].(string)) + default: + // We can ignore all the other event types here + } + } + + return lines, nil +} + func (l *Logger) LogJobStarted() { event := &JobStartedEvent{ Timestamp: int(time.Now().Unix()), diff --git a/pkg/eventlogger/logger_test.go b/pkg/eventlogger/logger_test.go new file mode 100644 index 00000000..ea034cef --- /dev/null +++ b/pkg/eventlogger/logger_test.go @@ -0,0 +1,47 @@ +package eventlogger + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func Test__GeneratePlainLogs(t *testing.T) { + tmpFileName := filepath.Join(os.TempDir(), fmt.Sprintf("logs_%d.json", time.Now().UnixNano())) + backend, _ := NewFileBackend(tmpFileName) + assert.Nil(t, backend.Open()) + logger, _ := NewLogger(backend) + generateLogEvents(t, 10, backend) + + file, err := logger.GeneratePlainTextFile() + assert.NoError(t, err) + assert.FileExists(t, file) + + bytes, err := ioutil.ReadFile(file) + assert.NoError(t, err) + + lines := strings.Split(string(bytes), "\n") + assert.Equal(t, []string{ + "echo hello", + "hello", + "hello", + "hello", + "hello", + "hello", + "hello", + "hello", + "hello", + "hello", + "hello", + "", + }, lines) + + assert.NoError(t, logger.Close()) + os.Remove(file) +} diff --git a/pkg/jobs/job.go b/pkg/jobs/job.go index 68256701..2a16eef1 100644 --- a/pkg/jobs/job.go +++ b/pkg/jobs/job.go @@ -5,7 +5,9 @@ import ( "encoding/base64" "fmt" "net/http" + "os" "runtime" + "strings" "time" api "github.com/semaphoreci/agent/pkg/api" @@ -32,6 +34,7 @@ type Job struct { JobLogArchived bool Stopped bool Finished bool + UploadJobLogs string } type JobOptions struct { @@ -42,6 +45,7 @@ type JobOptions struct { FileInjections []config.FileInjection FailOnMissingFiles bool SelfHosted bool + UploadJobLogs string RefreshTokenFn func() (string, error) } @@ -75,6 +79,7 @@ func NewJobWithOptions(options *JobOptions) (*Job, error) { Request: options.Request, JobLogArchived: false, Stopped: false, + UploadJobLogs: options.UploadJobLogs, } if options.Logger != nil { @@ -393,7 +398,13 @@ func (job *Job) teardownWithCallbacks(result string, callbackRetryAttempts int) func (job *Job) teardownWithNoCallbacks(result string) error { job.Logger.LogJobFinished(result) - err := job.Logger.Close() + // The job already finished, but executor is still open. + // We use the open executor to upload the job logs as an artifact, + // in case it has been trimmed during streaming. + err := job.Logger.CloseWithOptions(eventlogger.CloseOptions{ + OnClose: job.uploadLogsAsArtifact, + }) + if err != nil { log.Errorf("Error closing logger: %+v", err) } @@ -402,6 +413,36 @@ func (job *Job) teardownWithNoCallbacks(result string) error { return nil } +func (job *Job) uploadLogsAsArtifact(trimmed bool) { + if job.UploadJobLogs == config.UploadJobLogsConditionNever { + log.Infof("upload-job-logs=never - not uploading job logs as job artifact.") + return + } + + if job.UploadJobLogs == config.UploadJobLogsConditionWhenTrimmed && !trimmed { + log.Infof("upload-job-logs=when-trimmed - logs were not trimmed, not uploading job logs as job artifact.") + return + } + + log.Infof("Uploading job logs as job artifact...") + file, err := job.Logger.GeneratePlainTextFile() + if err != nil { + log.Errorf("Error converting '%s' to plain text: %v", file, err) + return + } + + defer os.Remove(file) + + cmd := []string{"artifact", "push", "job", file, "-d", "agent/job_logs.txt"} + exitCode := job.Executor.RunCommand(strings.Join(cmd, " "), true, "") + if exitCode != 0 { + log.Errorf("Error uploading job logs as artifact") + return + } + + log.Info("Successfully uploaded job logs as artifact.") +} + func (job *Job) Stop() { log.Info("Stopping job") diff --git a/pkg/listener/job_processor.go b/pkg/listener/job_processor.go index c0831ca3..0018b79d 100644 --- a/pkg/listener/job_processor.go +++ b/pkg/listener/job_processor.go @@ -34,6 +34,7 @@ func StartJobProcessor(httpClient *http.Client, apiClient *selfhostedapi.API, co EnvVars: config.EnvVars, FileInjections: config.FileInjections, FailOnMissingFiles: config.FailOnMissingFiles, + UploadJobLogs: config.UploadJobLogs, FailOnPreJobHookError: config.FailOnPreJobHookError, ExitOnShutdown: config.ExitOnShutdown, } @@ -63,6 +64,7 @@ type JobProcessor struct { EnvVars []config.HostEnvVar FileInjections []config.FileInjection FailOnMissingFiles bool + UploadJobLogs string FailOnPreJobHookError bool ExitOnShutdown bool ShutdownReason ShutdownReason @@ -157,6 +159,7 @@ func (p *JobProcessor) RunJob(jobID string) { FileInjections: p.FileInjections, FailOnMissingFiles: p.FailOnMissingFiles, SelfHosted: true, + UploadJobLogs: p.UploadJobLogs, RefreshTokenFn: func() (string, error) { return p.APIClient.RefreshToken() }, diff --git a/pkg/listener/listener.go b/pkg/listener/listener.go index fedb12e6..3bbea26a 100644 --- a/pkg/listener/listener.go +++ b/pkg/listener/listener.go @@ -33,6 +33,7 @@ type Config struct { EnvVars []config.HostEnvVar FileInjections []config.FileInjection FailOnMissingFiles bool + UploadJobLogs string FailOnPreJobHookError bool ExitOnShutdown bool AgentVersion string diff --git a/pkg/server/server.go b/pkg/server/server.go index a2824313..fe047b5f 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -16,7 +16,6 @@ import ( api "github.com/semaphoreci/agent/pkg/api" "github.com/semaphoreci/agent/pkg/config" - eventlogger "github.com/semaphoreci/agent/pkg/eventlogger" jobs "github.com/semaphoreci/agent/pkg/jobs" log "github.com/sirupsen/logrus" ) @@ -122,15 +121,7 @@ func (s *Server) JobLogs(w http.ResponseWriter, r *http.Request) { startFromLine = 0 } - logFile, ok := s.ActiveJob.Logger.Backend.(*eventlogger.FileBackend) - if !ok { - log.Error("Failed to stream job logs") - - http.Error(w, err.Error(), 500) - fmt.Fprintf(w, `{"message": "%s"}`, "Failed to open logfile") - } - - _, err = logFile.Stream(startFromLine, math.MaxInt32, w) + _, err = s.ActiveJob.Logger.Backend.Read(startFromLine, math.MaxInt32, w) if err != nil { log.Errorf("Error while streaming logs: %v", err) From 7454a856e7a48d3858bb2d5b243f48b73cd806a4 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Wed, 24 Aug 2022 12:35:30 -0300 Subject: [PATCH 040/130] fix: install toolbox based on os/arch (#171) --- install.sh | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/install.sh b/install.sh index d5dc51f0..2e5e8eb4 100755 --- a/install.sh +++ b/install.sh @@ -122,11 +122,28 @@ END fi } +# Find the toolbox URL based on operating system (linux/darwin) and architecture. +# It also considers SEMAPHORE_TOOLBOX_VERSION. If not set, it uses the latest version. +find_toolbox_url() { + local os=$(echo $DIST | tr '[:upper:]' '[:lower:]') + local tarball_name="self-hosted-${os}.tar" + if [[ ${ARCH} =~ "arm" || ${ARCH} == "aarch64" ]]; then + tarball_name="self-hosted-${os}-arm.tar" + fi + + if [[ -z "${SEMAPHORE_TOOLBOX_VERSION}" ]]; then + echo "https://github.com/semaphoreci/toolbox/releases/latest/download/${tarball_name}" + else + echo "https://github.com/semaphoreci/toolbox/releases/download/${SEMAPHORE_TOOLBOX_VERSION}/${tarball_name}" + fi +} + # # Main script # DIST=$(uname) +ARCH=$(uname -m) AGENT_INSTALLATION_DIRECTORY="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" if [[ "$EUID" -ne 0 ]]; then @@ -175,13 +192,9 @@ if [[ -d "$TOOLBOX_DIRECTORY" ]]; then rm -rf "$TOOLBOX_DIRECTORY" fi -if [[ -z "${SEMAPHORE_TOOLBOX_VERSION}" ]]; then - echo "SEMAPHORE_TOOLBOX_VERSION is not set. Installing latest toolbox..." - curl -sL "https://github.com/semaphoreci/toolbox/releases/latest/download/self-hosted-linux.tar" -o toolbox.tar -else - echo "Installing ${SEMAPHORE_TOOLBOX_VERSION} toolbox..." - curl -sL "https://github.com/semaphoreci/toolbox/releases/download/${SEMAPHORE_TOOLBOX_VERSION}/self-hosted-linux.tar" -o toolbox.tar -fi +toolbox_url=$(find_toolbox_url) +echo "Downloading toolbox from ${toolbox_url}..." +curl -sL ${toolbox_url} -o toolbox.tar tar -xf toolbox.tar mv toolbox $TOOLBOX_DIRECTORY From 4b8b7b144ceefe8527a00583bb8627354224de05 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Mon, 12 Sep 2022 10:02:11 -0300 Subject: [PATCH 041/130] fix: include response body in registration error log (#172) --- pkg/listener/selfhostedapi/register.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/listener/selfhostedapi/register.go b/pkg/listener/selfhostedapi/register.go index c36a7f1a..683e0d91 100644 --- a/pkg/listener/selfhostedapi/register.go +++ b/pkg/listener/selfhostedapi/register.go @@ -56,7 +56,7 @@ func (a *API) Register(req *RegisterRequest) (*RegisterResponse, error) { } if !httputils.IsSuccessfulCode(resp.StatusCode) { - return nil, fmt.Errorf("register request to %s got HTTP %d", a.RegisterPath(), resp.StatusCode) + return nil, fmt.Errorf("register request to %s got HTTP %d: %s", a.RegisterPath(), resp.StatusCode, body) } response := &RegisterResponse{} From cd3ebe021cba20ddd2c18c50b14e85a97c741ce8 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Fri, 16 Sep 2022 16:47:51 -0300 Subject: [PATCH 042/130] fix: do not block output flushing on long empty TTY read (#173) --- go.mod | 1 + go.sum | 2 + pkg/executors/docker_compose_executor.go | 12 +- pkg/executors/shell_executor.go | 12 +- pkg/shell/output_buffer.go | 214 +++++++++++++++++------ pkg/shell/output_buffer_test.go | 145 ++++++--------- pkg/shell/process.go | 69 ++------ pkg/shell/shell.go | 10 ++ pkg/shell/shell_test.go | 20 +-- 9 files changed, 257 insertions(+), 228 deletions(-) diff --git a/go.mod b/go.mod index 5b4d5f3f..f243d387 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,7 @@ module github.com/semaphoreci/agent require ( + github.com/cenkalti/backoff/v4 v4.1.3 github.com/creack/pty v1.1.18 github.com/golang-jwt/jwt/v4 v4.4.2 github.com/gorilla/handlers v1.5.1 diff --git a/go.sum b/go.sum index 48963158..075e8f14 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= +github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= diff --git a/pkg/executors/docker_compose_executor.go b/pkg/executors/docker_compose_executor.go index 59c67b31..8111a58d 100644 --- a/pkg/executors/docker_compose_executor.go +++ b/pkg/executors/docker_compose_executor.go @@ -714,7 +714,11 @@ func (e *DockerComposeExecutor) RunCommandWithOptions(options CommandOptions) in directive = options.Alias } - p := e.Shell.NewProcess(options.Command) + p := e.Shell.NewProcessWithOutput(options.Command, func(output string) { + if !options.Silent { + e.Logger.LogCommandOutput(output) + } + }) if !options.Silent { e.Logger.LogCommandStarted(directive) @@ -728,12 +732,6 @@ func (e *DockerComposeExecutor) RunCommandWithOptions(options CommandOptions) in } } - p.OnStdout(func(output string) { - if !options.Silent { - e.Logger.LogCommandOutput(output) - } - }) - p.Run() if !options.Silent { diff --git a/pkg/executors/shell_executor.go b/pkg/executors/shell_executor.go index e58117f4..c52613c6 100644 --- a/pkg/executors/shell_executor.go +++ b/pkg/executors/shell_executor.go @@ -241,7 +241,11 @@ func (e *ShellExecutor) RunCommandWithOptions(options CommandOptions) int { directive = options.Alias } - p := e.Shell.NewProcess(options.Command) + p := e.Shell.NewProcessWithOutput(options.Command, func(output string) { + if !options.Silent { + e.Logger.LogCommandOutput(output) + } + }) if !options.Silent { e.Logger.LogCommandStarted(directive) @@ -255,12 +259,6 @@ func (e *ShellExecutor) RunCommandWithOptions(options CommandOptions) int { } } - p.OnStdout(func(output string) { - if !options.Silent { - e.Logger.LogCommandOutput(output) - } - }) - p.Run() if !options.Silent { diff --git a/pkg/shell/output_buffer.go b/pkg/shell/output_buffer.go index b4ab9c7a..a0835645 100644 --- a/pkg/shell/output_buffer.go +++ b/pkg/shell/output_buffer.go @@ -1,10 +1,14 @@ package shell import ( + "fmt" "strings" + "sync" "time" "unicode/utf8" + backoff "github.com/cenkalti/backoff/v4" + "github.com/semaphoreci/agent/pkg/retry" log "github.com/sirupsen/logrus" ) @@ -19,9 +23,7 @@ import ( // - If there is more than 100 characters in the buffer // // - If there is less than 100 characters in the buffer, but they were in -// the buffer for more than 100 milisecond. The reasoning here is that -// it should take no more than 100 milliseconds for the TTY to flush its -// output. +// the buffer for more than 100 milliseconds. // // - If the UTF-8 sequence is complete. Cutting the UTF-8 sequence in half // leads to undefined (?) characters in the UI. @@ -31,19 +33,34 @@ const OutputBufferMaxTimeSinceLastAppend = 100 * time.Millisecond const OutputBufferDefaultCutLength = 100 type OutputBuffer struct { - bytes []byte - + Consumer func(string) + bytes []byte + mu sync.Mutex + done bool lastAppend *time.Time } -func NewOutputBuffer() *OutputBuffer { - return &OutputBuffer{bytes: []byte{}} +func NewOutputBuffer(consumer func(string)) (*OutputBuffer, error) { + if consumer == nil { + return nil, fmt.Errorf("output buffer requires a consumer") + } + + b := &OutputBuffer{ + Consumer: consumer, + bytes: []byte{}, + } + + go b.Flush() + + return b, nil } func (b *OutputBuffer) Append(bytes []byte) { + b.mu.Lock() + defer b.mu.Unlock() + now := time.Now() b.lastAppend = &now - b.bytes = append(b.bytes, bytes...) } @@ -51,64 +68,87 @@ func (b *OutputBuffer) IsEmpty() bool { return len(b.bytes) == 0 } -func (b *OutputBuffer) Flush() (string, bool) { - if b.IsEmpty() { - return "", false - } +func (b *OutputBuffer) Flush() { + backoffStrategy := b.exponentialBackoff() - timeSinceLastAppend := 1 * time.Millisecond - if b.lastAppend != nil { - timeSinceLastAppend = time.Since(*b.lastAppend) + for { + if b.done { + log.Debugf("The output buffer was closed - stopping") + break + } + + /* + * The exponential backoff strategy for ticks is only used + * when the buffer is empty, to make sure we don't continuously + * check the buffer if it has been empty for a while. + */ + if b.IsEmpty() { + delay := backoffStrategy.NextBackOff() + log.Debugf("Empty buffer - waiting %v until next tick", delay) + time.Sleep(delay) + continue + } + + b.flush() + backoffStrategy.Reset() } +} - log.Debugf("Flushing. %d bytes in the buffer", len(b.bytes)) +func (b *OutputBuffer) flush() { + b.mu.Lock() + defer b.mu.Unlock() - // We don't want to flush too often. - // - // We either: - // - // - wait till there is enough in the buffer - // - wait till the dat sitting in the buffer is old enough + timeSinceLastAppend := b.timeSinceLastAppend() + /* + * If there's recent, but not enough data in the buffer, we don't yet flush. + * Here, we don't want to use the exponential backoff strategy while waiting, + * because we should respect the maximum of 100ms for data in the buffer. + */ if len(b.bytes) < OutputBufferDefaultCutLength && timeSinceLastAppend < OutputBufferMaxTimeSinceLastAppend { - return "", false + log.Debugf("The output buffer has only %d bytes and the flush was %v ago - waiting...", len(b.bytes), timeSinceLastAppend) + time.Sleep(10 * time.Millisecond) + return } - // - // First we determine how much to cut. - // - // We don't want to flush too much in any iteration, but neither we want to - // flush too little. - // - // Starting from the default cut lenght, and decreasing the lenght until we - // are ready to flush. - // - + log.Debugf("%d bytes in the buffer - flushing...", len(b.bytes)) + + /* + * First we determine how much to cut. + * + * We don't want to flush too much in any iteration, but neither we want to + * flush too little. + * + * Starting from the default cut lenght, and decreasing the lenght until we + * are ready to flush. + */ cutLength := OutputBufferDefaultCutLength - // // We can't cut more than we have in the buffer. - // if len(b.bytes) < cutLength { cutLength = len(b.bytes) } - // - // Now comes the tricky part. - // - // We don't want to cut in the middle of an UTF-8 sequence. - // - // In the below loop, we are cutting of the last 3 charactes in case - // they are marked as the unicode continuation characters. - // - // An unicode sequence can't be longer than 4 bytes - // - // - // If there is only broken bytes in the buffer, we don't want to wait - // indefinetily. We only run this check if the last insert was recent enough. - // - - if timeSinceLastAppend < OutputBufferMaxTimeSinceLastAppend { + /* + * Now comes the tricky part. + * + * We don't want to cut in the middle of an UTF-8 sequence. + * + * In the loop below, we are cutting off the last 3 charactes in case + * they are marked as the unicode continuation characters, + * since an unicode sequence can't be longer than 4 bytes. + * + * We do this unicode-based adjustment in two scenarios: + * 1 - The output buffer was not yet closed. + * In this case, we always check for incomplete UTF-8 sequences, + * because the buffer might not yet received them from the TTY. + * 2 - The output buffer was closed, but the data in there doesn't fit + * in one chunk. Since in this case we know that no more bytes are coming + * from the TTY, the only reason we'd have an incomplete UTF-8 sequence + * is because we are cutting it here to fit it into the chunk. + */ + + if !b.done || (b.done && cutLength == OutputBufferDefaultCutLength) { for i := 0; i < 4; i++ { if utf8.Valid(b.bytes[0:cutLength]) { break @@ -118,11 +158,75 @@ func (b *OutputBuffer) Flush() (string, bool) { } } - // Flushing... + if cutLength <= 0 { + return + } - output := make([]byte, cutLength) - copy(output, b.bytes[0:cutLength]) + bytes := make([]byte, cutLength) + copy(bytes, b.bytes[0:cutLength]) b.bytes = b.bytes[cutLength:] - return strings.Replace(string(output), "\r\n", "\n", -1), true + // Make sure we normalize newline sequences, and flush the output to the consumer. + output := strings.Replace(string(bytes), "\r\n", "\n", -1) + log.Debugf("%d bytes flushed: %s", len(bytes), output) + b.Consumer(output) +} + +func (b *OutputBuffer) exponentialBackoff() *backoff.ExponentialBackOff { + e := backoff.NewExponentialBackOff() + + /* + * We start with a 10ms interval between ticks, + * but if there’s nothing in the buffer for a while, + * 10ms is too little an interval. + */ + e.InitialInterval = 10 * time.Millisecond + + /* + * If there's no data in the buffer, we increase the delay. + * But, we also cap that delay to 1s, to make sure we don’t go too + * long without checking if a command goes too long without producing any output. + */ + e.MaxInterval = time.Second + + // We don't ever want the strategy to return backoff.Stop, so we don't set this. + e.MaxElapsedTime = 0 + + // We need to call Reset() before using it. + e.Reset() + + return e +} + +func (b *OutputBuffer) timeSinceLastAppend() time.Duration { + if b.lastAppend != nil { + return time.Since(*b.lastAppend) + } + + return time.Millisecond +} + +func (b *OutputBuffer) Close() { + b.done = true + + // wait until buffer is empty, for at most 1s. + log.Debugf("Waiting for buffer to be completely flushed...") + err := retry.RetryWithConstantWait(retry.RetryOptions{ + Task: "wait for all output to be flushed", + MaxAttempts: 100, + DelayBetweenAttempts: 10 * time.Millisecond, + HideError: true, + Fn: func() error { + if b.IsEmpty() { + return nil + } + + b.flush() + return fmt.Errorf("not fully flushed") + }, + }) + + if err != nil { + log.Error("Could not flush all the output in the buffer") + } } diff --git a/pkg/shell/output_buffer_test.go b/pkg/shell/output_buffer_test.go index 04d07689..10f4167e 100644 --- a/pkg/shell/output_buffer_test.go +++ b/pkg/shell/output_buffer_test.go @@ -1,17 +1,25 @@ package shell import ( + "strings" "testing" "time" assert "github.com/stretchr/testify/assert" ) +func Test__OutputBuffer__RequiresConsumer(t *testing.T) { + buffer, err := NewOutputBuffer(nil) + assert.Error(t, err) + assert.Nil(t, buffer) +} + func Test__OutputBuffer__SimpleAscii(t *testing.T) { - buffer := NewOutputBuffer() + output := []string{} + buffer, _ := NewOutputBuffer(func(s string) { output = append(output, s) }) // - // Making sure that the input is long enough to the flushed immidiately + // Making sure that the input is long enough to the flushed immediately // input := []byte{} for i := 0; i < OutputBufferDefaultCutLength; i++ { @@ -19,32 +27,29 @@ func Test__OutputBuffer__SimpleAscii(t *testing.T) { } buffer.Append(input) - flushed, ok := buffer.Flush() - - assert.Equal(t, ok, true) - assert.Equal(t, flushed, string(input)) + buffer.Close() + assert.Equal(t, strings.Join(output, ""), string(input)) } func Test__OutputBuffer__SimpleAscii__ShorterThanMinimalCutLength(t *testing.T) { - buffer := NewOutputBuffer() + output := []string{} + buffer, _ := NewOutputBuffer(func(s string) { output = append(output, s) }) input := []byte("aaa") - buffer.Append(input) - _, ok := buffer.Flush() - // We need to wait a bit before flushing, the buffer is still too short - assert.Equal(t, ok, false) - - time.Sleep(OutputBufferMaxTimeSinceLastAppend) + // output is too short, so it will only be flushed + // when the max delay is reached. + assert.Len(t, output, 0) - flushed, ok := buffer.Flush() - assert.Equal(t, ok, true) - assert.Equal(t, flushed, string(input)) + // We need to wait a bit before flushing, the buffer is still too short + assert.Eventually(t, func() bool { return strings.Join(output, "") == string(input) }, time.Second, 100*time.Millisecond) + buffer.Close() } func Test__OutputBuffer__SimpleAscii__LongerThanMinimalCutLength(t *testing.T) { - buffer := NewOutputBuffer() + output := []string{} + buffer, _ := NewOutputBuffer(func(s string) { output = append(output, s) }) // // Making sure that the input is long enough to have to be flushed two times. @@ -56,20 +61,16 @@ func Test__OutputBuffer__SimpleAscii__LongerThanMinimalCutLength(t *testing.T) { buffer.Append(input) - flushed1, ok := buffer.Flush() - assert.Equal(t, ok, true) - assert.Equal(t, flushed1, string(input[:OutputBufferDefaultCutLength])) - - // We need to wait a bit before flushing, the buffer is still too short - time.Sleep(OutputBufferMaxTimeSinceLastAppend) - - flushed2, ok := buffer.Flush() - assert.Equal(t, ok, true) - assert.Equal(t, flushed2, string(input[OutputBufferDefaultCutLength:])) + buffer.Close() + if assert.Len(t, output, 2) { + assert.Equal(t, output[0], string(input[:OutputBufferDefaultCutLength])) + assert.Equal(t, output[1], string(input[OutputBufferDefaultCutLength:])) + } } func Test__OutputBuffer__UTF8_Sequence__Simple(t *testing.T) { - buffer := NewOutputBuffer() + output := []string{} + buffer, _ := NewOutputBuffer(func(s string) { output = append(output, s) }) // // Making sure that the input is long enough to the flushed immidiately @@ -80,52 +81,26 @@ func Test__OutputBuffer__UTF8_Sequence__Simple(t *testing.T) { } buffer.Append(input) - - out := "" - - for !buffer.IsEmpty() { - flushed, ok := buffer.Flush() - - if ok { - out += flushed - } else { - time.Sleep(OutputBufferMaxTimeSinceLastAppend) - } - } - - assert.Equal(t, out, string(input)) + buffer.Close() + assert.Equal(t, strings.Join(output, ""), string(input)) } func Test__OutputBuffer__UTF8_Sequence__Short(t *testing.T) { - buffer := NewOutputBuffer() + output := []string{} + buffer, _ := NewOutputBuffer(func(s string) { output = append(output, s) }) - // - // Making sure that the input is long enough to the flushed immidiately - // input := []byte("特特特") - buffer.Append(input) - - out := "" - - for !buffer.IsEmpty() { - flushed, ok := buffer.Flush() - - if ok { - out += flushed - } else { - time.Sleep(OutputBufferMaxTimeSinceLastAppend) - } - } - - assert.Equal(t, out, string(input)) + buffer.Close() + assert.Equal(t, strings.Join(output, ""), string(input)) } func Test__OutputBuffer__InvalidUTF8_Sequence(t *testing.T) { - buffer := NewOutputBuffer() + output := []string{} + buffer, _ := NewOutputBuffer(func(s string) { output = append(output, s) }) // - // Making sure that the input is long enough to the flushed immidiately + // Making sure that the input is long enough to the flushed immediately // input := []byte{} for len(input) <= OutputBufferDefaultCutLength { @@ -133,20 +108,8 @@ func Test__OutputBuffer__InvalidUTF8_Sequence(t *testing.T) { } buffer.Append(input) - - out := "" - - for !buffer.IsEmpty() { - flushed, ok := buffer.Flush() - - if ok { - out += flushed - } else { - time.Sleep(OutputBufferMaxTimeSinceLastAppend) - } - } - - assert.Equal(t, out, string(input)) + buffer.Close() + assert.Equal(t, strings.Join(output, ""), string(input)) } func Test__OutputBuffer__FlushIgnoresCharactersThatAreNotUtf8Valid(t *testing.T) { @@ -155,8 +118,8 @@ func Test__OutputBuffer__FlushIgnoresCharactersThatAreNotUtf8Valid(t *testing.T) // // The first 99 bytes will come from the 3-byte long kanji character, while // the last byte will be a broken character - - buffer := NewOutputBuffer() + output := []string{} + buffer, _ := NewOutputBuffer(func(s string) { output = append(output, s) }) input := "" for i := 0; i < 33; i++ { @@ -169,12 +132,10 @@ func Test__OutputBuffer__FlushIgnoresCharactersThatAreNotUtf8Valid(t *testing.T) buffer.Append([]byte(input)) buffer.Append(nonUtf8Chars) - // In the output, we expect that the last broken byte is not returned. - - out, ok := buffer.Flush() - - assert.Equal(t, ok, true) - assert.Equal(t, out, input) + // In the output, we expect that the last broken byte is not returned initially. + time.Sleep(10 * time.Millisecond) + assert.Equal(t, strings.Join(output, ""), input) + buffer.Close() } func Test__OutputBuffer__FlushReturnsBytesThatAreBrokenAndSitInTheBufferForTooLong(t *testing.T) { @@ -184,8 +145,8 @@ func Test__OutputBuffer__FlushReturnsBytesThatAreBrokenAndSitInTheBufferForTooLo // The first 99 bytes will come from the 3-byte long kanji character, while // the last byte will be a broken character // - - buffer := NewOutputBuffer() + output := []string{} + buffer, _ := NewOutputBuffer(func(s string) { output = append(output, s) }) input := []byte{} for i := 0; i < 33; i++ { @@ -194,12 +155,6 @@ func Test__OutputBuffer__FlushReturnsBytesThatAreBrokenAndSitInTheBufferForTooLo input = append(input, []byte("特")[0]) buffer.Append(input) - - // We wait for a while, and let the broken become bocome stale. - // Stale characters are forced out of the buffer, even if they are not valid. - time.Sleep(200 * time.Millisecond) - - out, ok := buffer.Flush() - assert.Equal(t, ok, true) - assert.Equal(t, out, string(input)) + buffer.Close() + assert.Equal(t, strings.Join(output, ""), string(input)) } diff --git a/pkg/shell/process.go b/pkg/shell/process.go index 2c58e1ec..7cb7c6a7 100644 --- a/pkg/shell/process.go +++ b/pkg/shell/process.go @@ -38,23 +38,23 @@ type Config struct { Shell *Shell StoragePath string Command string + OnOutput func(string) } type Process struct { - Command string - Shell *Shell - StoragePath string - StartedAt int - FinishedAt int - ExitCode int - OnStdoutCallback func(string) - Pid int - startMark string - endMark string - commandEndRegex *regexp.Regexp - inputBuffer []byte - outputBuffer *OutputBuffer - SysProcAttr *syscall.SysProcAttr + Command string + Shell *Shell + StoragePath string + StartedAt int + FinishedAt int + ExitCode int + Pid int + startMark string + endMark string + commandEndRegex *regexp.Regexp + inputBuffer []byte + outputBuffer *OutputBuffer + SysProcAttr *syscall.SysProcAttr } func randomMagicMark() string { @@ -64,8 +64,8 @@ func randomMagicMark() string { func NewProcess(config Config) *Process { startMark := randomMagicMark() + "-start" endMark := randomMagicMark() + "-end" - commandEndRegex := regexp.MustCompile(endMark + " " + `(\d+)` + "[\r\n]+") + outputBuffer, _ := NewOutputBuffer(config.OnOutput) return &Process{ Shell: config.Shell, @@ -75,7 +75,7 @@ func NewProcess(config Config) *Process { startMark: startMark, endMark: endMark, commandEndRegex: commandEndRegex, - outputBuffer: NewOutputBuffer(), + outputBuffer: outputBuffer, } } @@ -87,33 +87,6 @@ func (p *Process) EnvironmentFilePath() string { return fmt.Sprintf("%s.env.after", p.CmdFilePath()) } -func (p *Process) OnStdout(callback func(string)) { - p.OnStdoutCallback = callback -} - -func (p *Process) StreamToStdout() { - for { - data, ok := p.outputBuffer.Flush() - if !ok { - break - } - - log.Debugf("Stream to stdout: %#v", data) - - p.OnStdoutCallback(data) - } -} - -func (p *Process) flushOutputBuffer() { - for !p.outputBuffer.IsEmpty() { - p.StreamToStdout() - - if !p.outputBuffer.IsEmpty() { - time.Sleep(10 * time.Millisecond) - } - } -} - func (p *Process) flushInputAll() { p.flushInputBufferTill(len(p.inputBuffer)) } @@ -376,11 +349,10 @@ func (p *Process) readNonPTY(reader *io.PipeReader, done chan bool) { p.inputBuffer = append(p.inputBuffer, buffer[0:n]...) log.Debugf("reading data from command. Input buffer: %#v", string(p.inputBuffer)) p.flushInputAll() - p.StreamToStdout() if err == io.EOF { log.Debug("Finished reading") - p.flushOutputBuffer() + p.outputBuffer.Close() break } } @@ -487,21 +459,18 @@ func (p *Process) scan() error { p.flushInputAll() } - p.StreamToStdout() - err := p.read() if err != nil { // Reading failed. The most likely cause is that the bash process // died. For example, running an `exit 1` command has killed it. - // Flushing all remaining data in the buffer and exiting. - p.flushOutputBuffer() + p.outputBuffer.Close() return err } } - p.flushOutputBuffer() + p.outputBuffer.Close() log.Debug("Command output finished") log.Debugf("Parsing exit code %s", exitCode) diff --git a/pkg/shell/shell.go b/pkg/shell/shell.go index ded0767e..34e4d75b 100644 --- a/pkg/shell/shell.go +++ b/pkg/shell/shell.go @@ -224,6 +224,16 @@ func (s *Shell) NewProcess(command string) *Process { }) } +func (s *Shell) NewProcessWithOutput(command string, outputConsumer func(string)) *Process { + return NewProcess( + Config{ + Command: command, + Shell: s, + StoragePath: s.StoragePath, + OnOutput: outputConsumer, + }) +} + func (s *Shell) Close() error { if s.TTY != nil { err := s.TTY.Close() diff --git a/pkg/shell/shell_test.go b/pkg/shell/shell_test.go index 094955d6..8b3f56a8 100644 --- a/pkg/shell/shell_test.go +++ b/pkg/shell/shell_test.go @@ -49,12 +49,11 @@ func Test__Shell__SimpleHelloWorld(t *testing.T) { shell, _ := NewShell(os.TempDir()) shell.Start() - p1 := shell.NewProcess("echo Hello") - p1.OnStdout(func(line string) { + p1 := shell.NewProcessWithOutput("echo Hello", func(line string) { output.WriteString(line) }) - p1.Run() + p1.Run() assert.Equal(t, output.String(), "Hello\n") } @@ -76,12 +75,11 @@ func Test__Shell__HandlingBashProcessKill(t *testing.T) { cmd = "echo Hello && exit 1" } - p1 := shell.NewProcess(cmd) - p1.OnStdout(func(line string) { + p1 := shell.NewProcessWithOutput(cmd, func(line string) { output.WriteString(line) }) - p1.Run() + p1.Run() assert.Equal(t, output.String(), "Hello\n") } @@ -106,16 +104,10 @@ func Test__Shell__HandlingBashProcessKillThatHasBackgroundJobs(t *testing.T) { shell, _ := NewShell(os.TempDir()) shell.Start() - p1 := shell.NewProcess("sleep infinity &") - p1.OnStdout(func(line string) { - output.WriteString(line) - }) + p1 := shell.NewProcessWithOutput("sleep infinity &", func(line string) { output.WriteString(line) }) p1.Run() - p2 := shell.NewProcess("echo 'Hello' && sleep 1 && exit 1") - p2.OnStdout(func(line string) { - output.WriteString(line) - }) + p2 := shell.NewProcessWithOutput("echo 'Hello' && sleep 1 && exit 1", func(line string) { output.WriteString(line) }) p2.Run() assert.Equal(t, output.String(), "Hello\n") From 592be2917849d9d0e3edf6481025d2aa501f8a5b Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Fri, 7 Oct 2022 09:37:00 -0300 Subject: [PATCH 043/130] fix: increase output buffer flush chunk size and timeout (#174) --- pkg/eventlogger/httpbackend_test.go | 8 +++--- pkg/eventlogger/inmemorybackend.go | 9 ++++-- pkg/eventlogger/test_helpers.go | 25 +++++++++++++++-- pkg/executors/shell_executor_test.go | 42 ++++++++++------------------ pkg/jobs/job_test.go | 34 +++++++++++----------- pkg/listener/listener_test.go | 4 +-- pkg/shell/output_buffer.go | 21 ++++++++++---- pkg/shell/output_buffer_test.go | 20 +++++++++++++ test/e2e/docker/unicode.rb | 6 +--- 9 files changed, 102 insertions(+), 67 deletions(-) diff --git a/pkg/eventlogger/httpbackend_test.go b/pkg/eventlogger/httpbackend_test.go index 9c7a9caf..2ca3e20b 100644 --- a/pkg/eventlogger/httpbackend_test.go +++ b/pkg/eventlogger/httpbackend_test.go @@ -106,7 +106,7 @@ func Test__LogsArePushedToHTTPEndpoint(t *testing.T) { eventObjects, err := TransformToObjects(mockServer.GetLogs()) assert.Nil(t, err) - simplifiedEvents, err := SimplifyLogEvents(eventObjects, true) + simplifiedEvents, err := SimplifyLogEvents(eventObjects, SimplifyOptions{IncludeOutput: true}) assert.Nil(t, err) assert.Equal(t, []string{ @@ -148,7 +148,7 @@ func Test__RequestsAreCappedAtLinesPerRequest(t *testing.T) { eventObjects, err := TransformToObjects(mockServer.GetLogs()) assert.Nil(t, err) - simplifiedEvents, err := SimplifyLogEvents(eventObjects, true) + simplifiedEvents, err := SimplifyLogEvents(eventObjects, SimplifyOptions{IncludeOutput: true}) assert.Nil(t, err) assert.Equal(t, []string{ @@ -197,7 +197,7 @@ func Test__FlushingGivesUpAfterTimeout(t *testing.T) { eventObjects, err := TransformToObjects(mockServer.GetLogs()) assert.Nil(t, err) - simplifiedEvents, err := SimplifyLogEvents(eventObjects, true) + simplifiedEvents, err := SimplifyLogEvents(eventObjects, SimplifyOptions{IncludeOutput: true}) assert.Nil(t, err) // logs are incomplete @@ -262,7 +262,7 @@ func Test__TokenIsRefreshed(t *testing.T) { eventObjects, err := TransformToObjects(mockServer.GetLogs()) assert.Nil(t, err) - simplifiedEvents, err := SimplifyLogEvents(eventObjects, true) + simplifiedEvents, err := SimplifyLogEvents(eventObjects, SimplifyOptions{IncludeOutput: true}) assert.Nil(t, err) assert.Equal(t, []string{ diff --git a/pkg/eventlogger/inmemorybackend.go b/pkg/eventlogger/inmemorybackend.go index e1a5a19a..6af41db1 100644 --- a/pkg/eventlogger/inmemorybackend.go +++ b/pkg/eventlogger/inmemorybackend.go @@ -35,12 +35,15 @@ func (l *InMemoryBackend) CloseWithOptions(options CloseOptions) error { return nil } -func (l *InMemoryBackend) SimplifiedEvents(includeOutput bool) ([]string, error) { - return SimplifyLogEvents(l.Events, includeOutput) +func (l *InMemoryBackend) SimplifiedEvents(includeOutput, useSingleOutputEvent bool) ([]string, error) { + return SimplifyLogEvents(l.Events, SimplifyOptions{ + IncludeOutput: includeOutput, + UseSingleItemForOutput: useSingleOutputEvent, + }) } func (l *InMemoryBackend) SimplifiedEventsWithoutDockerPull() ([]string, error) { - logs, err := l.SimplifiedEvents(true) + logs, err := l.SimplifiedEvents(true, false) if err != nil { return []string{}, err } diff --git a/pkg/eventlogger/test_helpers.go b/pkg/eventlogger/test_helpers.go index 295382ff..89b66ee5 100644 --- a/pkg/eventlogger/test_helpers.go +++ b/pkg/eventlogger/test_helpers.go @@ -31,9 +31,16 @@ func TransformToObjects(events []string) ([]interface{}, error) { return objects, nil } -func SimplifyLogEvents(events []interface{}, includeOutput bool) ([]string, error) { +type SimplifyOptions struct { + IncludeOutput bool + UseSingleItemForOutput bool +} + +func SimplifyLogEvents(events []interface{}, options SimplifyOptions) ([]string, error) { simplified := []string{} + output := "" + for _, event := range events { switch e := event.(type) { case *JobStartedEvent: @@ -43,10 +50,22 @@ func SimplifyLogEvents(events []interface{}, includeOutput bool) ([]string, erro case *CommandStartedEvent: simplified = append(simplified, "directive: "+e.Directive) case *CommandOutputEvent: - if includeOutput { - simplified = append(simplified, e.Output) + if options.IncludeOutput { + if options.UseSingleItemForOutput { + output = output + e.Output + } else { + simplified = append(simplified, e.Output) + } } case *CommandFinishedEvent: + if options.IncludeOutput && options.UseSingleItemForOutput { + if output != "" { + simplified = append(simplified, output) + } + + output = "" + } + simplified = append(simplified, fmt.Sprintf("Exit Code: %d", e.ExitCode)) default: return []string{}, fmt.Errorf("unknown shell event") diff --git a/pkg/executors/shell_executor_test.go b/pkg/executors/shell_executor_test.go index b873eab0..5da62555 100644 --- a/pkg/executors/shell_executor_test.go +++ b/pkg/executors/shell_executor_test.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "runtime" + "strings" "testing" "time" @@ -77,7 +78,7 @@ func Test__ShellExecutor__EnvVars(t *testing.T) { assert.Zero(t, e.Stop()) assert.Zero(t, e.Cleanup()) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, false) assert.Nil(t, err) assert.Equal(t, simplifiedEvents, []string{ @@ -169,7 +170,7 @@ func Test__ShellExecutor__InjectFiles(t *testing.T) { assert.Zero(t, e.Stop()) assert.Zero(t, e.Cleanup()) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, false) assert.Nil(t, err) assert.Equal(t, simplifiedEvents, []string{ @@ -234,7 +235,7 @@ func Test__ShellExecutor__MultilineCommand(t *testing.T) { assert.Zero(t, e.Stop()) assert.Zero(t, e.Cleanup()) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, false) assert.Nil(t, err) assert.Equal(t, simplifiedEvents, []string{ @@ -269,7 +270,7 @@ func Test__ShellExecutor__ChangesCurrentDirectory(t *testing.T) { assert.Zero(t, e.Stop()) assert.Zero(t, e.Cleanup()) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(false) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(false, false) assert.Nil(t, err) assert.Equal(t, simplifiedEvents, []string{ @@ -299,7 +300,7 @@ func Test__ShellExecutor__ChangesEnvVars(t *testing.T) { assert.Zero(t, e.Stop()) assert.Zero(t, e.Cleanup()) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, false) assert.Nil(t, err) assert.Equal(t, simplifiedEvents, []string{ @@ -333,7 +334,7 @@ func Test__ShellExecutor__StoppingRunningJob(t *testing.T) { time.Sleep(1 * time.Second) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, false) assert.Nil(t, err) assert.Equal(t, simplifiedEvents[0:4], []string{ @@ -348,27 +349,18 @@ func Test__ShellExecutor__StoppingRunningJob(t *testing.T) { func Test__ShellExecutor__LargeCommandOutput(t *testing.T) { e, testLoggerBackend := setupShellExecutor(t, true) - go func() { - assert.Zero(t, e.RunCommand(testsupport.LargeOutputCommand(), false, "")) - }() - - time.Sleep(5 * time.Second) - + assert.Zero(t, e.RunCommand(testsupport.LargeOutputCommand(), false, "")) assert.Zero(t, e.Stop()) assert.Zero(t, e.Cleanup()) time.Sleep(1 * time.Second) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, true) assert.Nil(t, err) assert.Equal(t, simplifiedEvents, []string{ fmt.Sprintf("directive: %s", testsupport.LargeOutputCommand()), - "hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello", - "hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello", - "hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello", - "hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello", - "hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello", + strings.Repeat("hello", 100), "Exit Code: 0", }) } @@ -381,22 +373,16 @@ func Test__ShellExecutor__Unicode(t *testing.T) { assert.Zero(t, e.Cleanup()) time.Sleep(1 * time.Second) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, true) assert.Nil(t, err) assert.Equal(t, simplifiedEvents, []string{ fmt.Sprintf("directive: %s", testsupport.Output(UnicodeOutput1)), - "特定の伝説に拠る物語の由来については諸説存在し。特定の伝説に拠る物", - "語の由来については諸説存在し。特定の伝説に拠る物語の由来については", - "諸説存在し。", + "特定の伝説に拠る物語の由来については諸説存在し。特定の伝説に拠る物語の由来については諸説存在し。特定の伝説に拠る物語の由来については諸説存在し。", "Exit Code: 0", fmt.Sprintf("directive: %s", testsupport.Output(UnicodeOutput2)), - "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", - "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", - "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", - "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", - "━━━━━━━━━━━━━━━━━━━━━━", + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "Exit Code: 0", }) } @@ -419,7 +405,7 @@ func Test__ShellExecutor__BrokenUnicode(t *testing.T) { time.Sleep(1 * time.Second) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, false) assert.Nil(t, err) assert.Equal(t, simplifiedEvents, []string{ diff --git a/pkg/jobs/job_test.go b/pkg/jobs/job_test.go index 55304884..533d0111 100644 --- a/pkg/jobs/job_test.go +++ b/pkg/jobs/job_test.go @@ -51,7 +51,7 @@ func Test__EnvVarsAreAvailableToCommands(t *testing.T) { job.Run() assert.True(t, job.Finished) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, false) assert.Nil(t, err) assert.Equal(t, simplifiedEvents, []string{ @@ -126,7 +126,7 @@ func Test__EnvVarsAreAvailableToEpilogueAlwaysAndOnPass(t *testing.T) { job.Run() assert.True(t, job.Finished) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, false) assert.Nil(t, err) assert.Equal(t, simplifiedEvents, []string{ @@ -230,7 +230,7 @@ func Test__EnvVarsAreAvailableToEpilogueAlwaysAndOnFail(t *testing.T) { job.Run() assert.True(t, job.Finished) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, false) assert.Nil(t, err) testsupport.AssertSimplifiedJobLogs(t, simplifiedEvents, []string{ @@ -330,7 +330,7 @@ func Test__EpilogueOnPassOnlyExecutesOnSuccessfulJob(t *testing.T) { job.Run() assert.True(t, job.Finished) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, false) assert.Nil(t, err) assert.Equal(t, simplifiedEvents, []string{ @@ -398,7 +398,7 @@ func Test__EpilogueOnFailOnlyExecutesOnFailedJob(t *testing.T) { job.Run() assert.True(t, job.Finished) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, false) assert.Nil(t, err) testsupport.AssertSimplifiedJobLogs(t, simplifiedEvents, []string{ @@ -457,7 +457,7 @@ func Test__UsingCommandAliases(t *testing.T) { job.Run() assert.True(t, job.Finished) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, false) assert.Nil(t, err) assert.Equal(t, simplifiedEvents, []string{ @@ -515,7 +515,7 @@ func Test__StopJob(t *testing.T) { assert.True(t, job.Stopped) assert.Eventually(t, func() bool { return job.Finished }, 5*time.Second, 1*time.Second) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, false) assert.Nil(t, err) assert.Equal(t, simplifiedEvents, []string{ @@ -569,7 +569,7 @@ func Test__StopJobOnEpilogue(t *testing.T) { assert.True(t, job.Stopped) assert.Eventually(t, func() bool { return job.Finished }, 5*time.Second, 1*time.Second) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, false) assert.Nil(t, err) assert.Equal(t, simplifiedEvents, []string{ @@ -623,7 +623,7 @@ func Test__STTYRestoration(t *testing.T) { job.Run() assert.True(t, job.Finished) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, false) assert.Nil(t, err) assert.Equal(t, simplifiedEvents, []string{ @@ -678,7 +678,7 @@ func Test__BackgroundJobIsKilledAfterJobIsDoneInWindows(t *testing.T) { job.Run() assert.True(t, job.Finished) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, false) assert.Nil(t, err) assert.Equal(t, simplifiedEvents, []string{ @@ -749,7 +749,7 @@ func Test__BackgroundJobIsKilledAfterJobIsDoneInNonWindows(t *testing.T) { job.Run() assert.True(t, job.Finished) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, false) assert.Nil(t, err) assert.Equal(t, simplifiedEvents, []string{ @@ -815,7 +815,7 @@ func Test__KillingRootBash(t *testing.T) { job.Run() assert.True(t, job.Finished) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, false) assert.Nil(t, err) assert.Equal(t, simplifiedEvents, []string{ @@ -869,7 +869,7 @@ func Test__BashSetE(t *testing.T) { job.Run() assert.True(t, job.Finished) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, false) assert.Nil(t, err) assert.Equal(t, simplifiedEvents, []string{ @@ -926,7 +926,7 @@ func Test__BashSetPipefail(t *testing.T) { job.Run() assert.True(t, job.Finished) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, false) assert.Nil(t, err) assert.Equal(t, simplifiedEvents, []string{ @@ -988,7 +988,7 @@ func Test__UsePreJobHook(t *testing.T) { assert.True(t, job.Finished) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, false) assert.Nil(t, err) testsupport.AssertSimplifiedJobLogs(t, simplifiedEvents, []string{ @@ -1060,7 +1060,7 @@ func Test__PreJobHookHasAccessToEnvVars(t *testing.T) { assert.True(t, job.Finished) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, false) assert.Nil(t, err) testsupport.AssertSimplifiedJobLogs(t, simplifiedEvents, []string{ @@ -1127,7 +1127,7 @@ func Test__UsePreJobHookAndFailOnError(t *testing.T) { assert.True(t, job.Finished) - simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true) + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, false) assert.Nil(t, err) testsupport.AssertSimplifiedJobLogs(t, simplifiedEvents, []string{ diff --git a/pkg/listener/listener_test.go b/pkg/listener/listener_test.go index 97aa25f5..8314ee20 100644 --- a/pkg/listener/listener_test.go +++ b/pkg/listener/listener_test.go @@ -472,7 +472,7 @@ func Test__HostEnvVarsAreExposedToJob(t *testing.T) { eventObjects, err := eventlogger.TransformToObjects(loghubMockServer.GetLogs()) assert.Nil(t, err) - simplifiedEvents, err := eventlogger.SimplifyLogEvents(eventObjects, true) + simplifiedEvents, err := eventlogger.SimplifyLogEvents(eventObjects, eventlogger.SimplifyOptions{IncludeOutput: true}) assert.Nil(t, err) assert.Equal(t, []string{ @@ -591,7 +591,7 @@ func Test__LogTokenIsRefreshed(t *testing.T) { eventObjects, err := eventlogger.TransformToObjects(loghubMockServer.GetLogs()) assert.Nil(t, err) - simplifiedEvents, err := eventlogger.SimplifyLogEvents(eventObjects, true) + simplifiedEvents, err := eventlogger.SimplifyLogEvents(eventObjects, eventlogger.SimplifyOptions{IncludeOutput: true}) assert.Nil(t, err) assert.Equal(t, []string{ diff --git a/pkg/shell/output_buffer.go b/pkg/shell/output_buffer.go index a0835645..609a1d62 100644 --- a/pkg/shell/output_buffer.go +++ b/pkg/shell/output_buffer.go @@ -99,13 +99,14 @@ func (b *OutputBuffer) flush() { defer b.mu.Unlock() timeSinceLastAppend := b.timeSinceLastAppend() + chunkSize := b.chunkSize() /* * If there's recent, but not enough data in the buffer, we don't yet flush. * Here, we don't want to use the exponential backoff strategy while waiting, * because we should respect the maximum of 100ms for data in the buffer. */ - if len(b.bytes) < OutputBufferDefaultCutLength && timeSinceLastAppend < OutputBufferMaxTimeSinceLastAppend { + if len(b.bytes) < chunkSize && timeSinceLastAppend < OutputBufferMaxTimeSinceLastAppend { log.Debugf("The output buffer has only %d bytes and the flush was %v ago - waiting...", len(b.bytes), timeSinceLastAppend) time.Sleep(10 * time.Millisecond) return @@ -122,7 +123,7 @@ func (b *OutputBuffer) flush() { * Starting from the default cut lenght, and decreasing the lenght until we * are ready to flush. */ - cutLength := OutputBufferDefaultCutLength + cutLength := chunkSize // We can't cut more than we have in the buffer. if len(b.bytes) < cutLength { @@ -148,7 +149,7 @@ func (b *OutputBuffer) flush() { * is because we are cutting it here to fit it into the chunk. */ - if !b.done || (b.done && cutLength == OutputBufferDefaultCutLength) { + if !b.done || (b.done && cutLength == chunkSize) { for i := 0; i < 4; i++ { if utf8.Valid(b.bytes[0:cutLength]) { break @@ -206,14 +207,24 @@ func (b *OutputBuffer) timeSinceLastAppend() time.Duration { return time.Millisecond } +func (b *OutputBuffer) chunkSize() int { + // If the output buffer was already closed, we should + // use bigger chunks to make sure we can flush everything left in time. + if b.done { + return OutputBufferDefaultCutLength * 10 + } + + return OutputBufferDefaultCutLength +} + func (b *OutputBuffer) Close() { b.done = true - // wait until buffer is empty, for at most 1s. + // wait until buffer is empty, for at most 10s. log.Debugf("Waiting for buffer to be completely flushed...") err := retry.RetryWithConstantWait(retry.RetryOptions{ Task: "wait for all output to be flushed", - MaxAttempts: 100, + MaxAttempts: 1000, DelayBetweenAttempts: 10 * time.Millisecond, HideError: true, Fn: func() error { diff --git a/pkg/shell/output_buffer_test.go b/pkg/shell/output_buffer_test.go index 10f4167e..0a4a6763 100644 --- a/pkg/shell/output_buffer_test.go +++ b/pkg/shell/output_buffer_test.go @@ -61,6 +61,9 @@ func Test__OutputBuffer__SimpleAscii__LongerThanMinimalCutLength(t *testing.T) { buffer.Append(input) + // wait for the output to be flushed + time.Sleep(time.Second) + buffer.Close() if assert.Len(t, output, 2) { assert.Equal(t, output[0], string(input[:OutputBufferDefaultCutLength])) @@ -68,6 +71,23 @@ func Test__OutputBuffer__SimpleAscii__LongerThanMinimalCutLength(t *testing.T) { } } +func Test__OutputBuffer__SimpleAscii__ChunkIncreasesWhenClosed(t *testing.T) { + output := []string{} + buffer, _ := NewOutputBuffer(func(s string) { output = append(output, s) }) + input := []byte{} + for i := 0; i < OutputBufferDefaultCutLength+50; i++ { + input = append(input, 'a') + } + + buffer.Append(input) + buffer.Close() + + // everything is flushed in one chunk + if assert.Len(t, output, 1) { + assert.Equal(t, output[0], string(input)) + } +} + func Test__OutputBuffer__UTF8_Sequence__Simple(t *testing.T) { output := []string{} buffer, _ := NewOutputBuffer(func(s string) { output = append(output, s) }) diff --git a/test/e2e/docker/unicode.rb b/test/e2e/docker/unicode.rb index 413c3c29..23f716a8 100644 --- a/test/e2e/docker/unicode.rb +++ b/test/e2e/docker/unicode.rb @@ -56,11 +56,7 @@ {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} {"event":"cmd_started", "timestamp":"*", "directive":"echo 特定の伝説に拠る物語の由来については諸説存在し。特定の伝説に拠る物語の由来については諸説存在し。特定の伝説に拠る物語の由来については諸説存在し。"} - - {"event":"cmd_output", "timestamp":"*", "output":"特定の伝説に拠る物語の由来については諸説存在し。特定の伝説に拠る物"} - {"event":"cmd_output", "timestamp":"*", "output":"語の由来については諸説存在し。特定の伝説に拠る物語の由来については"} - {"event":"cmd_output", "timestamp":"*", "output":"諸説存在し。\\n"} - + {"event":"cmd_output", "timestamp":"*", "output":"特定の伝説に拠る物語の由来については諸説存在し。特定の伝説に拠る物語の由来については諸説存在し。特定の伝説に拠る物語の由来については諸説存在し。\\n"} {"event":"cmd_finished", "timestamp":"*", "directive":"echo 特定の伝説に拠る物語の由来については諸説存在し。特定の伝説に拠る物語の由来については諸説存在し。特定の伝説に拠る物語の由来については諸説存在し。","exit_code":0,"finished_at":"*","started_at":"*"} {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} From 046a9fd0a0fc6a3a174575d1909871cc2b2c3cfb Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Mon, 7 Nov 2022 19:31:43 -0300 Subject: [PATCH 044/130] fix: update dependencies (#175) --- go.mod | 18 +++++++++--------- go.sum | 39 +++++++++++++++++++++------------------ 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/go.mod b/go.mod index f243d387..31559df6 100644 --- a/go.mod +++ b/go.mod @@ -11,29 +11,29 @@ require ( github.com/renderedtext/go-watchman v0.0.0-20220524201126-042727917d44 github.com/sirupsen/logrus v1.9.0 github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.12.0 - github.com/stretchr/testify v1.8.0 - golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 + github.com/spf13/viper v1.14.0 + github.com/stretchr/testify v1.8.1 + golang.org/x/sys v0.2.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/felixge/httpsnoop v1.0.2 // indirect - github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.0.1 // indirect + github.com/pelletier/go-toml/v2 v2.0.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/spf13/afero v1.8.2 // indirect + github.com/spf13/afero v1.9.2 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/subosito/gotenv v1.3.0 // indirect - golang.org/x/text v0.3.7 // indirect + github.com/subosito/gotenv v1.4.1 // indirect + golang.org/x/text v0.4.0 // indirect gopkg.in/alexcesaro/statsd.v2 v2.0.0 // indirect - gopkg.in/ini.v1 v1.66.4 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 075e8f14..0b44786d 100644 --- a/go.sum +++ b/go.sum @@ -63,8 +63,8 @@ github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -106,7 +106,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -154,8 +154,8 @@ github.com/mitchellh/panicwrap v1.0.0 h1:67zIyVakCIvcs69A0FGfZjBdPleaonSgGlXRSRl github.com/mitchellh/panicwrap v1.0.0/go.mod h1:pKvZHwWrZowLUzftuFq7coarnxbBXU4aQh3N0BJOeeA= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= -github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= +github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -167,27 +167,29 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= -github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= +github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= +github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= -github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= +github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU= +github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= -github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= +github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -322,9 +324,10 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -332,8 +335,8 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -482,8 +485,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= -gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= From 3390d5c5dd12e06112d1c36202a5b31e90d2944c Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Tue, 6 Dec 2022 15:16:47 -0300 Subject: [PATCH 045/130] fix: race condition on stop-job (#176) --- pkg/listener/job_processor.go | 14 ++++++++++++++ test/hub_reference/Gemfile.lock | 16 ++++++++-------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/pkg/listener/job_processor.go b/pkg/listener/job_processor.go index 0018b79d..db81f650 100644 --- a/pkg/listener/job_processor.go +++ b/pkg/listener/job_processor.go @@ -7,6 +7,7 @@ import ( "os/exec" "os/signal" "runtime" + "sync" "syscall" "time" @@ -68,6 +69,7 @@ type JobProcessor struct { FailOnPreJobHookError bool ExitOnShutdown bool ShutdownReason ShutdownReason + mutex sync.Mutex } func (p *JobProcessor) Start() { @@ -205,6 +207,16 @@ func (p *JobProcessor) getJobWithRetries(jobID string) (*api.JobRequest, error) } func (p *JobProcessor) StopJob(jobID string) { + p.mutex.Lock() + defer p.mutex.Unlock() + + // The job finished before the sync request returned a stop-job command. + // Here, we don't do anything since the job is already finished and + // a finished-job state will be reported in the next sync. + if p.State == selfhostedapi.AgentStateFinishedJob { + return + } + p.CurrentJobID = jobID p.State = selfhostedapi.AgentStateStoppingJob @@ -212,8 +224,10 @@ func (p *JobProcessor) StopJob(jobID string) { } func (p *JobProcessor) JobFinished(result selfhostedapi.JobResult) { + p.mutex.Lock() p.State = selfhostedapi.AgentStateFinishedJob p.CurrentJobResult = result + p.mutex.Unlock() } func (p *JobProcessor) WaitForJobs() { diff --git a/test/hub_reference/Gemfile.lock b/test/hub_reference/Gemfile.lock index 419dfbdc..feb4efec 100644 --- a/test/hub_reference/Gemfile.lock +++ b/test/hub_reference/Gemfile.lock @@ -3,22 +3,22 @@ GEM specs: daemons (1.4.1) eventmachine (1.2.7) - mustermann (1.1.1) + mustermann (3.0.0) ruby2_keywords (~> 0.0.1) - rack (2.2.3.1) - rack-protection (2.2.0) + rack (2.2.4) + rack-protection (3.0.4) rack ruby2_keywords (0.0.5) - sinatra (2.2.0) - mustermann (~> 1.0) - rack (~> 2.2) - rack-protection (= 2.2.0) + sinatra (3.0.4) + mustermann (~> 3.0) + rack (~> 2.2, >= 2.2.4) + rack-protection (= 3.0.4) tilt (~> 2.0) thin (1.8.1) daemons (~> 1.0, >= 1.0.9) eventmachine (~> 1.0, >= 1.0.4) rack (>= 1, < 3) - tilt (2.0.10) + tilt (2.0.11) PLATFORMS ruby From 4e2f82aeb778ed3bb1e3787d8fb7df1a3c199111 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Thu, 5 Jan 2023 10:54:37 -0300 Subject: [PATCH 046/130] fix: use the same installation directory for logs (#177) --- install.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/install.sh b/install.sh index 2e5e8eb4..2c3bebd8 100755 --- a/install.sh +++ b/install.sh @@ -22,6 +22,7 @@ RestartSec=$SEMAPHORE_AGENT_SYSTEMD_RESTART_SEC User=$SEMAPHORE_AGENT_INSTALLATION_USER WorkingDirectory=$AGENT_INSTALLATION_DIRECTORY ExecStart=$AGENT_INSTALLATION_DIRECTORY/agent start --config-file $AGENT_CONFIG_PATH +Environment=SEMAPHORE_AGENT_LOG_FILE_PATH=$AGENT_INSTALLATION_DIRECTORY/agent.log [Install] WantedBy=multi-user.target @@ -76,6 +77,11 @@ create_launchd_daemon() { --config-file $AGENT_CONFIG_PATH + EnvironmentVariables + + SEMAPHORE_AGENT_LOG_FILE_PATH + $AGENT_INSTALLATION_DIRECTORY/agent.log + RunAtLoad KeepAlive From 163ac1e9d907ea69ad67875eedd824f2f032782e Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Thu, 12 Jan 2023 15:48:13 -0300 Subject: [PATCH 047/130] fix: macOS installation on SEMAPHORE_AGENT_START=false (#178) --- install.sh | 26 ++++++++++++++++---------- test/e2e_support/api_mode.rb | 2 +- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/install.sh b/install.sh index 2c3bebd8..f2da9dfc 100755 --- a/install.sh +++ b/install.sh @@ -83,7 +83,7 @@ create_launchd_daemon() { $AGENT_INSTALLATION_DIRECTORY/agent.log RunAtLoad - + KeepAlive Crashed @@ -104,26 +104,32 @@ END echo "Creating $LAUNCHD_DAEMON_PATH..." if [[ -f "$LAUNCHD_DAEMON_PATH" ]]; then - echo "launchd daemon already exists at $LAUNCHD_DAEMON_PATH. Overriding and reloading it..." - launchctl unload $LAUNCHD_DAEMON_PATH + echo "launchd daemon already exists at $LAUNCHD_DAEMON_PATH. Overriding it..." echo "$LAUNCHD_DAEMON" > $LAUNCHD_DAEMON_PATH - launchctl load $LAUNCHD_DAEMON_PATH if [[ "$SEMAPHORE_AGENT_START" == "false" ]]; then - echo "Not restarting $LAUNCHD_DAEMON_LABEL." + echo "Not starting/restarting $LAUNCHD_DAEMON_LABEL." else - echo "Restarting $LAUNCHD_DAEMON_LABEL service..." - launchctl start $LAUNCHD_DAEMON_LABEL + echo "Bootout $LAUNCHD_DAEMON_LABEL..." + launchctl bootout system $LAUNCHD_DAEMON_PATH || true + + echo "Bootstrap $LAUNCHD_DAEMON_LABEL..." + launchctl bootstrap system $LAUNCHD_DAEMON_PATH + + echo "Kickstart $LAUNCHD_DAEMON_LABEL..." + launchctl kickstart -k system/com.semaphoreci.agent fi else echo "$LAUNCHD_DAEMON" > $LAUNCHD_DAEMON_PATH - launchctl load $LAUNCHD_DAEMON_PATH if [[ "$SEMAPHORE_AGENT_START" == "false" ]]; then echo "Not starting $LAUNCHD_DAEMON_LABEL." else - echo "Starting $LAUNCHD_DAEMON_LABEL service..." - launchctl start $LAUNCHD_DAEMON_LABEL + echo "Bootstrap $LAUNCHD_DAEMON_LABEL service..." + launchctl bootstrap system $LAUNCHD_DAEMON_PATH + + echo "Kickstart $LAUNCHD_DAEMON_LABEL service..." + launchctl kickstart -k system/com.semaphoreci.agent fi fi } diff --git a/test/e2e_support/api_mode.rb b/test/e2e_support/api_mode.rb index 2454bdf1..287c5dff 100644 --- a/test/e2e_support/api_mode.rb +++ b/test/e2e_support/api_mode.rb @@ -66,7 +66,7 @@ def wait_for_job_to_finish puts "=========================" puts "Waiting for job to finish" - Timeout.timeout(60 * 2) do + Timeout.timeout(60 * 3) do loop do `curl -H "Authorization: Bearer #{$TOKEN}" --fail -k "https://0.0.0.0:30000/job_logs" | grep "job_finished"` From 48e81dd713db4ec29099daafbc0ba141cb4da6e7 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Wed, 1 Feb 2023 15:34:46 -0300 Subject: [PATCH 048/130] feat: basic Kubernetes executor (#179) --- .semaphore/semaphore.yml | 41 ++ Dockerfile.test | 9 +- go.mod | 39 +- go.sum | 107 ++++- main.go | 62 ++- pkg/config/config.go | 24 + pkg/executors/executor.go | 1 + pkg/executors/kubernetes_executor.go | 360 +++++++++++++++ pkg/jobs/job.go | 41 +- pkg/kubernetes/client.go | 415 ++++++++++++++++++ pkg/kubernetes/client_test.go | 351 +++++++++++++++ pkg/listener/job_processor.go | 104 +++-- pkg/listener/listener.go | 40 +- pkg/shell/env.go | 11 + pkg/shell/env_test.go | 16 + pkg/shell/process.go | 90 ++-- pkg/shell/shell.go | 4 + pkg/shell/shell_test.go | 23 + test/e2e.rb | 4 +- .../kubernetes/docker_compose__env-vars.rb | 108 +++++ .../kubernetes/docker_compose__epilogue.rb | 74 ++++ .../docker_compose__file-injection.rb | 87 ++++ .../docker_compose__multiple-containers.rb | 84 ++++ test/e2e/kubernetes/shell__env-vars.rb | 90 ++++ test/e2e/kubernetes/shell__epilogue.rb | 67 +++ test/e2e/kubernetes/shell__file-injection.rb | 80 ++++ ...cker_compose_fail_on_missing_host_files.rb | 2 +- .../docker_compose_host_env_vars.rb | 2 +- .../self-hosted/docker_compose_host_files.rb | 2 +- .../docker_compose_missing_host_files.rb | 2 +- test/e2e_support/api_mode.rb | 2 +- test/e2e_support/docker-compose-listen.yml | 20 +- 32 files changed, 2211 insertions(+), 151 deletions(-) create mode 100644 pkg/executors/kubernetes_executor.go create mode 100644 pkg/kubernetes/client.go create mode 100644 pkg/kubernetes/client_test.go create mode 100644 test/e2e/kubernetes/docker_compose__env-vars.rb create mode 100644 test/e2e/kubernetes/docker_compose__epilogue.rb create mode 100644 test/e2e/kubernetes/docker_compose__file-injection.rb create mode 100644 test/e2e/kubernetes/docker_compose__multiple-containers.rb create mode 100644 test/e2e/kubernetes/shell__env-vars.rb create mode 100644 test/e2e/kubernetes/shell__epilogue.rb create mode 100644 test/e2e/kubernetes/shell__file-injection.rb diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 831fc07d..0195ab12 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -208,6 +208,47 @@ blocks: - docker_compose_missing_host_files - docker_compose_fail_on_missing_host_files + - name: "Kubernetes Executor E2E" + dependencies: [] + task: + env_vars: + - name: GO111MODULE + value: "on" + - name: TEST_MODE + value: "listen" + + prologue: + commands: + - sem-version go 1.18 + - curl -sLO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 && install minikube-linux-amd64 /tmp/ + - /tmp/minikube-linux-amd64 config set WantUpdateNotification false + - /tmp/minikube-linux-amd64 start --driver=docker + - checkout + - mkdir /tmp/agent + # The docker container uses root, and not semaphore + - grep -rl "/home/semaphore" ~/.kube | xargs sed -i 's/home\/semaphore/root/g' + - grep -rl "/home/semaphore" ~/.minikube | xargs sed -i 's/home\/semaphore/root/g' + + epilogue: + commands: + - docker logs e2e_support_agent_1 + - docker logs e2e_support_hub_1 + + jobs: + - name: Kubernetes executor + commands: + - "make e2e TEST=kubernetes/$TEST" + matrix: + - env_var: TEST + values: + - shell__env-vars + - shell__epilogue + - shell__file-injection + - docker_compose__env-vars + - docker_compose__epilogue + - docker_compose__file-injection + - docker_compose__multiple-containers + promotions: - name: Release pipeline_file: "release.yml" diff --git a/Dockerfile.test b/Dockerfile.test index 04656c90..647771f8 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -6,10 +6,17 @@ RUN apt-get update && \ apt-get install -y ssh && \ pip install docker-compose awscli +# By default, sshd runs on port 22, we need it to run on port 2222 +RUN sed -i 's/#Port 22/Port 2222/g' /etc/ssh/sshd_config + +# kubectl is required to be present in the container running the agent +RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && \ + install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl + ADD server.key /app/server.key ADD server.crt /app/server.crt ADD build/agent /app/agent WORKDIR /app -CMD service ssh restart && ./agent serve +CMD service ssh restart && ./agent serve --port 30000 diff --git a/go.mod b/go.mod index 31559df6..72619b0f 100644 --- a/go.mod +++ b/go.mod @@ -13,28 +13,63 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.14.0 github.com/stretchr/testify v1.8.1 - golang.org/x/sys v0.2.0 + golang.org/x/sys v0.3.0 gopkg.in/yaml.v3 v3.0.1 + k8s.io/api v0.26.0 + k8s.io/apimachinery v0.26.0 + k8s.io/client-go v0.26.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.9.0 // indirect + github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/felixge/httpsnoop v1.0.2 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/swag v0.19.14 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/gnostic v0.5.7-v3refs // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/gofuzz v1.1.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/imdario/mergo v0.3.6 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/magiconair/properties v1.8.6 // indirect + github.com/mailru/easyjson v0.7.6 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.5 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/afero v1.9.2 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/subosito/gotenv v1.4.1 // indirect - golang.org/x/text v0.4.0 // indirect + golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10 // indirect + golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect + golang.org/x/term v0.3.0 // indirect + golang.org/x/text v0.5.0 // indirect + golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.28.1 // indirect gopkg.in/alexcesaro/statsd.v2 v2.0.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/klog/v2 v2.80.1 // indirect + k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect + k8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect + sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect ) go 1.18 diff --git a/go.sum b/go.sum index 0b44786d..19676959 100644 --- a/go.sum +++ b/go.sum @@ -48,17 +48,23 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= +github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= @@ -68,6 +74,19 @@ github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbS github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -95,8 +114,13 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -106,7 +130,12 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -137,25 +166,50 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/panicwrap v1.0.0 h1:67zIyVakCIvcs69A0FGfZjBdPleaonSgGlXRSRlb6fE= github.com/mitchellh/panicwrap v1.0.0/go.mod h1:pKvZHwWrZowLUzftuFq7coarnxbBXU4aQh3N0BJOeeA= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/onsi/ginkgo/v2 v2.4.0 h1:+Ig9nvqgS5OBSACXNk15PLdp0U9XPYROt9CFzVdFGIs= +github.com/onsi/gomega v1.23.0 h1:/oxKu9c2HVap+F3PfKort2Hw5DEU+HGlW8n+tguWsys= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -177,12 +231,15 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU= github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -271,6 +328,8 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10 h1:Frnccbp+ok2GkUS2tC84yAq/U9Vg+0sIO7aRL3T4Xnc= +golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -280,6 +339,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -326,20 +387,24 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U= +golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -377,6 +442,7 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= @@ -385,6 +451,7 @@ golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -416,6 +483,7 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -447,6 +515,7 @@ google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -479,18 +548,28 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alexcesaro/statsd.v2 v2.0.0 h1:FXkZSCZIH17vLCO5sO2UucTHsH9pc+17F6pl3JVCwMc= gopkg.in/alexcesaro/statsd.v2 v2.0.0/go.mod h1:i0ubccKGzBVNBpdGV5MocxyA/XlLUJzA7SLonnE4drU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -500,6 +579,24 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.26.0 h1:IpPlZnxBpV1xl7TGk/X6lFtpgjgntCg8PJ+qrPHAC7I= +k8s.io/api v0.26.0/go.mod h1:k6HDTaIFC8yn1i6pSClSqIwLABIcLV9l5Q4EcngKnQg= +k8s.io/apimachinery v0.26.0 h1:1feANjElT7MvPqp0JT6F3Ss6TWDwmcjLypwoPpEf7zg= +k8s.io/apimachinery v0.26.0/go.mod h1:tnPmbONNJ7ByJNz9+n9kMjNP8ON+1qoAIIC70lztu74= +k8s.io/client-go v0.26.0 h1:lT1D3OfO+wIi9UFolCrifbjUUgu7CpLca0AD8ghRLI8= +k8s.io/client-go v0.26.0/go.mod h1:I2Sh57A79EQsDmn7F7ASpmru1cceh3ocVT9KlX2jEZg= +k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= +k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+OGxg8HsuBr/5f6tVAjDu6E= +k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4= +k8s.io/utils v0.0.0-20221107191617-1a15be271d1d h1:0Smp/HP1OH4Rvhe+4B8nWGERtlqAGSftbSbbmm45oFs= +k8s.io/utils v0.0.0-20221107191617-1a15be271d1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/main.go b/main.go index 1e25ac51..56e40685 100644 --- a/main.go +++ b/main.go @@ -119,6 +119,14 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { _ = pflag.Bool(config.FailOnMissingFiles, false, "Fail job if files specified using --files are missing") _ = pflag.String(config.UploadJobLogs, config.UploadJobLogsConditionNever, "When should the agent upload the job logs as a job artifact. Default is never.") _ = pflag.Bool(config.FailOnPreJobHookError, false, "Fail job if pre-job hook fails") + _ = pflag.Bool(config.KubernetesExecutor, false, "Use Kubernetes executor") + _ = pflag.String(config.KubernetesDefaultImage, "", "Default image used for jobs that do not specify images, when using kubernetes executor") + _ = pflag.String(config.KubernetesImagePullPolicy, config.ImagePullPolicyNever, "Image pull policy to use for Kubernetes executor. Default is never.") + _ = pflag.Int( + config.KubernetesPodStartTimeout, + config.DefaultKubernetesPodStartTimeout, + fmt.Sprintf("Timeout for the pod to be ready, in seconds. Default is %d.", config.DefaultKubernetesPodStartTimeout), + ) pflag.Parse() @@ -145,6 +153,10 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { log.Fatal("Idle timeout can't be negative. Exiting...") } + if viper.GetInt(config.KubernetesPodStartTimeout) < 0 { + log.Fatal("Kubernetes pod start timeout can't be negative. Exiting...") + } + scheme := "https" if viper.GetBool(config.NoHTTPS) { scheme = "http" @@ -165,24 +177,28 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { log.SetFormatter(&formatter) config := listener.Config{ - AgentName: agentName, - Endpoint: viper.GetString(config.Endpoint), - Token: viper.GetString(config.Token), - RegisterRetryLimit: 30, - GetJobRetryLimit: 10, - CallbackRetryLimit: 60, - Scheme: scheme, - ShutdownHookPath: viper.GetString(config.ShutdownHookPath), - PreJobHookPath: viper.GetString(config.PreJobHookPath), - DisconnectAfterJob: viper.GetBool(config.DisconnectAfterJob), - DisconnectAfterIdleSeconds: viper.GetInt(config.DisconnectAfterIdleTimeout), - EnvVars: hostEnvVars, - FileInjections: fileInjections, - FailOnMissingFiles: viper.GetBool(config.FailOnMissingFiles), - UploadJobLogs: viper.GetString(config.UploadJobLogs), - FailOnPreJobHookError: viper.GetBool(config.FailOnPreJobHookError), - AgentVersion: VERSION, - ExitOnShutdown: true, + AgentName: agentName, + Endpoint: viper.GetString(config.Endpoint), + Token: viper.GetString(config.Token), + RegisterRetryLimit: 30, + GetJobRetryLimit: 10, + CallbackRetryLimit: 60, + Scheme: scheme, + ShutdownHookPath: viper.GetString(config.ShutdownHookPath), + PreJobHookPath: viper.GetString(config.PreJobHookPath), + DisconnectAfterJob: viper.GetBool(config.DisconnectAfterJob), + DisconnectAfterIdleSeconds: viper.GetInt(config.DisconnectAfterIdleTimeout), + EnvVars: hostEnvVars, + FileInjections: fileInjections, + FailOnMissingFiles: viper.GetBool(config.FailOnMissingFiles), + UploadJobLogs: viper.GetString(config.UploadJobLogs), + FailOnPreJobHookError: viper.GetBool(config.FailOnPreJobHookError), + AgentVersion: VERSION, + ExitOnShutdown: true, + KubernetesExecutor: viper.GetBool(config.KubernetesExecutor), + KubernetesDefaultImage: viper.GetString(config.KubernetesDefaultImage), + KubernetesImagePullPolicy: viper.GetString(config.KubernetesImagePullPolicy), + KubernetesPodStartTimeoutSeconds: viper.GetInt(config.KubernetesPodStartTimeout), } go func() { @@ -232,6 +248,16 @@ func validateConfiguration() { config.ValidUploadJobLogsCondition, ) } + + imagePullPolicy := viper.GetString(config.KubernetesImagePullPolicy) + if !contains(config.ValidImagePullPolicies, imagePullPolicy) { + log.Fatalf( + "Unsupported value '%s' for '%s'. Allowed values are: %v. Exiting...", + imagePullPolicy, + config.KubernetesImagePullPolicy, + config.ValidImagePullPolicies, + ) + } } func getAgentName() string { diff --git a/pkg/config/config.go b/pkg/config/config.go index 787c5b2f..0150b679 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -17,8 +17,28 @@ const ( FailOnMissingFiles = "fail-on-missing-files" UploadJobLogs = "upload-job-logs" FailOnPreJobHookError = "fail-on-pre-job-hook-error" + KubernetesExecutor = "kubernetes-executor" + KubernetesDefaultImage = "kubernetes-default-image" + KubernetesImagePullPolicy = "kubernetes-image-pull-policy" + KubernetesPodStartTimeout = "kubernetes-pod-start-timeout" ) +const DefaultKubernetesPodStartTimeout = 60 + +type ImagePullPolicy string + +const ( + ImagePullPolicyNever = "Never" + ImagePullPolicyAlways = "Always" + ImagePullPolicyIfNotPresent = "IfNotPresent" +) + +var ValidImagePullPolicies = []string{ + ImagePullPolicyNever, + ImagePullPolicyAlways, + ImagePullPolicyIfNotPresent, +} + type UploadJobLogsCondition string const ( @@ -48,6 +68,10 @@ var ValidConfigKeys = []string{ FailOnMissingFiles, UploadJobLogs, FailOnPreJobHookError, + KubernetesExecutor, + KubernetesDefaultImage, + KubernetesImagePullPolicy, + KubernetesPodStartTimeout, } type HostEnvVar struct { diff --git a/pkg/executors/executor.go b/pkg/executors/executor.go index 2eabca49..87ba58e8 100644 --- a/pkg/executors/executor.go +++ b/pkg/executors/executor.go @@ -25,3 +25,4 @@ type CommandOptions struct { const ExecutorTypeShell = "shell" const ExecutorTypeDockerCompose = "dockercompose" +const ExecutorKubernetes = "kubernetes" diff --git a/pkg/executors/kubernetes_executor.go b/pkg/executors/kubernetes_executor.go new file mode 100644 index 00000000..c00b506f --- /dev/null +++ b/pkg/executors/kubernetes_executor.go @@ -0,0 +1,360 @@ +package executors + +import ( + "encoding/base64" + "fmt" + "math/rand" + "os" + "path/filepath" + "time" + + api "github.com/semaphoreci/agent/pkg/api" + "github.com/semaphoreci/agent/pkg/config" + eventlogger "github.com/semaphoreci/agent/pkg/eventlogger" + "github.com/semaphoreci/agent/pkg/kubernetes" + shell "github.com/semaphoreci/agent/pkg/shell" + + log "github.com/sirupsen/logrus" +) + +type KubernetesExecutor struct { + k8sClient *kubernetes.KubernetesClient + jobRequest *api.JobRequest + podName string + secretName string + logger *eventlogger.Logger + Shell *shell.Shell + + // We need to keep track if the initial environment has already + // been exposed or not, because ExportEnvVars() gets called twice. + initialEnvironmentExposed bool +} + +func NewKubernetesExecutor(jobRequest *api.JobRequest, logger *eventlogger.Logger, k8sConfig kubernetes.Config) (*KubernetesExecutor, error) { + clientset, err := kubernetes.NewInClusterClientset() + if err != nil { + log.Warnf("No in-cluster configuration found - using ~/.kube/config...") + + clientset, err = kubernetes.NewClientsetFromConfig() + if err != nil { + return nil, fmt.Errorf("error creating kubernetes clientset: %v", err) + } + } + + k8sClient, err := kubernetes.NewKubernetesClient(clientset, k8sConfig) + if err != nil { + return nil, err + } + + return &KubernetesExecutor{ + k8sClient: k8sClient, + jobRequest: jobRequest, + logger: logger, + }, nil +} + +func (e *KubernetesExecutor) Prepare() int { + e.podName = e.randomPodName() + e.secretName = fmt.Sprintf("%s-secret", e.podName) + + err := e.k8sClient.CreateSecret(e.secretName, e.jobRequest) + if err != nil { + log.Errorf("Error creating secret '%s': %v", e.secretName, err) + return 1 + } + + err = e.k8sClient.CreatePod(e.podName, e.secretName, e.jobRequest) + if err != nil { + log.Errorf("Error creating pod: %v", err) + return 1 + } + + return 0 +} + +func (e *KubernetesExecutor) randomPodName() string { + var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz0123456789") + + b := make([]rune, 12) + for i := range b { + b[i] = letterRunes[rand.Intn(len(letterRunes))] + } + + return string(b) +} + +func (e *KubernetesExecutor) Start() int { + commandStartedAt := int(time.Now().Unix()) + directive := "Starting shell session..." + exitCode := 0 + + e.logger.LogCommandStarted(directive) + + defer func() { + commandFinishedAt := int(time.Now().Unix()) + e.logger.LogCommandFinished(directive, exitCode, commandStartedAt, commandFinishedAt) + }() + + err := e.k8sClient.WaitForPod(e.podName, func(msg string) { + e.logger.LogCommandOutput(msg) + log.Info(msg) + }) + + if err != nil { + log.Errorf("Failed to create pod: %v", err) + e.logger.LogCommandOutput(fmt.Sprintf("Failed to create pod: %v\n", err)) + exitCode = 1 + return exitCode + } + + e.logger.LogCommandOutput("Starting a new bash session in the pod\n") + + // #nosec + executable := "kubectl" + args := []string{ + "exec", + "-it", + e.podName, + "-c", + "main", + "--", + "bash", + "--login", + } + + shell, err := shell.NewShellFromExecAndArgs(executable, args, os.TempDir()) + if err != nil { + log.Errorf("Failed to create shell: %v", err) + e.logger.LogCommandOutput("Failed to create shell in kubernetes container\n") + e.logger.LogCommandOutput(err.Error()) + + exitCode = 1 + return exitCode + } + + err = shell.Start() + if err != nil { + log.Errorf("Failed to start shell err: %+v", err) + e.logger.LogCommandOutput("Failed to start shell in kubernetes container\n") + e.logger.LogCommandOutput(err.Error()) + + exitCode = 1 + return exitCode + } + + e.Shell = shell + return exitCode +} + +// This function gets called twice during a job's execution: +// - On the first call, the environment variables come from a secret file injected into the pod. +// - On the second call, the environment variables (currently, just the job result) need to be exported +// through commands executed through the PTY. +func (e *KubernetesExecutor) ExportEnvVars(envVars []api.EnvVar, hostEnvVars []config.HostEnvVar) int { + commandStartedAt := int(time.Now().Unix()) + directive := "Exporting environment variables" + exitCode := 0 + + e.logger.LogCommandStarted(directive) + + defer func() { + commandFinishedAt := int(time.Now().Unix()) + e.logger.LogCommandFinished(directive, exitCode, commandStartedAt, commandFinishedAt) + e.initialEnvironmentExposed = true + }() + + // Include the environment variables exposed in the job log. + for _, envVar := range envVars { + e.logger.LogCommandOutput(fmt.Sprintf("Exporting %s\n", envVar.Name)) + } + + // Second call of this function. + // Export environment variables through the PTY, one by one, using commands. + if e.initialEnvironmentExposed { + env, err := shell.CreateEnvironment(envVars, hostEnvVars) + if err != nil { + exitCode = 1 + log.Errorf("Error creating environment: %v", err) + return exitCode + } + + for _, command := range env.ToCommands() { + exitCode = e.RunCommand(command, true, "") + if exitCode != 0 { + log.Errorf("Error exporting environment variables") + return exitCode + } + } + + return exitCode + } + + // First call of this function. + // In this case, a secret with all the environment variables has been exposed in the pod spec, + // so all we need to do here is to source that file through the PTY session. + exitCode = e.RunCommand("source /tmp/injected/.env", true, "") + if exitCode != 0 { + log.Errorf("Error sourcing environment file") + return exitCode + } + + return exitCode +} + +// All the files have already been exposed to the main container +// through a temporary secret created before the k8s pod was created, and used in the pod spec. +// Here, we just need to move the files to their correct location. +func (e *KubernetesExecutor) InjectFiles(files []api.File) int { + directive := "Injecting Files" + commandStartedAt := int(time.Now().Unix()) + exitCode := 0 + + e.logger.LogCommandStarted(directive) + + defer func() { + commandFinishedAt := int(time.Now().Unix()) + e.logger.LogCommandFinished(directive, exitCode, commandStartedAt, commandFinishedAt) + }() + + homeDir, err := os.UserHomeDir() + if err != nil { + log.Errorf("Error finding home directory: %v\n", err) + return 1 + } + + for _, file := range files { + + // Find the key used to inject the file in /tmp/injected + fileNameSecretKey := base64.RawURLEncoding.EncodeToString([]byte(file.Path)) + + // Normalize path to properly handle absolute/relative/~ paths + destPath := file.NormalizePath(homeDir) + e.logger.LogCommandOutput(fmt.Sprintf("Injecting %s with file mode %s\n", file.Path, file.Mode)) + + // Create the parent directory + parentDir := filepath.Dir(file.Path) + exitCode := e.RunCommand(fmt.Sprintf("mkdir -p %s", parentDir), true, "") + if exitCode != 0 { + errMessage := fmt.Sprintf("Error injecting file %s: failed to created parent directory %s\n", destPath, parentDir) + e.logger.LogCommandOutput(errMessage) + log.Errorf(errMessage) + exitCode = 1 + return exitCode + } + + // Copy the file injected as a secret in the /tmp/injected directory to its proper place + exitCode = e.RunCommand(fmt.Sprintf("cp /tmp/injected/%s %s", fileNameSecretKey, file.Path), true, "") + if exitCode != 0 { + e.logger.LogCommandOutput(fmt.Sprintf("Error injecting file %s\n", file.Path)) + log.Errorf("Error injecting file %s", file.Path) + exitCode = 1 + return exitCode + } + + // Adjust the injected file's mode + exitCode = e.RunCommand(fmt.Sprintf("chmod %s %s", file.Mode, file.Path), true, "") + if exitCode != 0 { + errMessage := fmt.Sprintf("Error setting file mode (%s) for %s\n", file.Mode, file.Path) + e.logger.LogCommandOutput(errMessage) + log.Errorf(errMessage) + exitCode = 1 + return exitCode + } + } + + return exitCode +} + +func (e *KubernetesExecutor) RunCommand(command string, silent bool, alias string) int { + return e.RunCommandWithOptions(CommandOptions{ + Command: command, + Silent: silent, + Alias: alias, + Warning: "", + }) +} + +func (e *KubernetesExecutor) RunCommandWithOptions(options CommandOptions) int { + directive := options.Command + if options.Alias != "" { + directive = options.Alias + } + + /* + * Unlike the shell and docker-compose executors, + * where a folder can be shared between the agent and the PTY executing the commands, + * in here, we don't have that ability. So, we do not use a temporary folder for storing + * the command being executed, and instead use base64 encoding to make sure multiline commands + * and commands with different types of quote usage are handled properly. + */ + p := e.Shell.NewProcessWithConfig(shell.Config{ + UseBase64Encoding: true, + Command: options.Command, + Shell: e.Shell, + OnOutput: func(output string) { + if !options.Silent { + e.logger.LogCommandOutput(output) + } + }, + }) + + if !options.Silent { + e.logger.LogCommandStarted(directive) + + if options.Alias != "" { + e.logger.LogCommandOutput(fmt.Sprintf("Running: %s\n", options.Command)) + } + + if options.Warning != "" { + e.logger.LogCommandOutput(fmt.Sprintf("Warning: %s\n", options.Warning)) + } + } + + p.Run() + + if !options.Silent { + e.logger.LogCommandFinished(directive, p.ExitCode, p.StartedAt, p.FinishedAt) + } + + return p.ExitCode +} + +func (e *KubernetesExecutor) Stop() int { + log.Debug("Starting the process killing procedure") + + if e.Shell != nil { + err := e.Shell.Close() + if err != nil { + log.Errorf("Process killing procedure returned an error %+v\n", err) + + return 0 + } + } + + return e.Cleanup() +} + +func (e *KubernetesExecutor) Cleanup() int { + e.removeK8sResources() + e.removeLocalResources() + return 0 +} + +func (e *KubernetesExecutor) removeK8sResources() { + err := e.k8sClient.DeletePod(e.podName) + if err != nil { + log.Errorf("Error deleting pod '%s': %v\n", e.podName, err) + } + + err = e.k8sClient.DeleteSecret(e.secretName) + if err != nil { + log.Errorf("Error deleting secret '%s': %v\n", e.secretName, err) + } +} + +func (e *KubernetesExecutor) removeLocalResources() { + envFileName := filepath.Join(os.TempDir(), ".env") + if err := os.Remove(envFileName); err != nil { + log.Errorf("Error removing local file '%s': %v", envFileName, err) + } +} diff --git a/pkg/jobs/job.go b/pkg/jobs/job.go index 2a16eef1..c3492332 100644 --- a/pkg/jobs/job.go +++ b/pkg/jobs/job.go @@ -15,6 +15,7 @@ import ( eventlogger "github.com/semaphoreci/agent/pkg/eventlogger" executors "github.com/semaphoreci/agent/pkg/executors" httputils "github.com/semaphoreci/agent/pkg/httputils" + "github.com/semaphoreci/agent/pkg/kubernetes" "github.com/semaphoreci/agent/pkg/listener/selfhostedapi" "github.com/semaphoreci/agent/pkg/retry" log "github.com/sirupsen/logrus" @@ -38,15 +39,19 @@ type Job struct { } type JobOptions struct { - Request *api.JobRequest - Client *http.Client - Logger *eventlogger.Logger - ExposeKvmDevice bool - FileInjections []config.FileInjection - FailOnMissingFiles bool - SelfHosted bool - UploadJobLogs string - RefreshTokenFn func() (string, error) + Request *api.JobRequest + Client *http.Client + Logger *eventlogger.Logger + ExposeKvmDevice bool + FileInjections []config.FileInjection + FailOnMissingFiles bool + SelfHosted bool + UseKubernetesExecutor bool + KubernetesDefaultImage string + KubernetesImagePullPolicy string + KubernetesPodStartTimeoutSeconds int + UploadJobLogs string + RefreshTokenFn func() (string, error) } func NewJob(request *api.JobRequest, client *http.Client) (*Job, error) { @@ -104,6 +109,24 @@ func NewJobWithOptions(options *JobOptions) (*Job, error) { } func CreateExecutor(request *api.JobRequest, logger *eventlogger.Logger, jobOptions JobOptions) (executors.Executor, error) { + if jobOptions.UseKubernetesExecutor { + // The downwards API allows the namespace to be exposed + // to the agent container through an environment variable. + // See: https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information. + namespace := os.Getenv("KUBERNETES_NAMESPACE") + if namespace == "" { + namespace = "default" + } + + return executors.NewKubernetesExecutor(request, logger, kubernetes.Config{ + Namespace: namespace, + DefaultImage: jobOptions.KubernetesDefaultImage, + ImagePullPolicy: jobOptions.KubernetesImagePullPolicy, + PodPollingAttempts: jobOptions.KubernetesPodStartTimeoutSeconds, + PodPollingInterval: time.Second, + }) + } + switch request.Executor { case executors.ExecutorTypeShell: return executors.NewShellExecutor(request, logger, jobOptions.SelfHosted), nil diff --git a/pkg/kubernetes/client.go b/pkg/kubernetes/client.go new file mode 100644 index 00000000..d3275b90 --- /dev/null +++ b/pkg/kubernetes/client.go @@ -0,0 +1,415 @@ +package kubernetes + +import ( + "context" + "encoding/base64" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "time" + + "github.com/semaphoreci/agent/pkg/api" + "github.com/semaphoreci/agent/pkg/config" + "github.com/semaphoreci/agent/pkg/retry" + "github.com/semaphoreci/agent/pkg/shell" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +type Config struct { + Namespace string + DefaultImage string + ImagePullPolicy string + PodPollingAttempts int + PodPollingInterval time.Duration +} + +func (c *Config) PollingInterval() time.Duration { + if c.PodPollingInterval == 0 { + return time.Second + } + + return c.PodPollingInterval +} + +func (c *Config) PollingAttempts() int { + if c.PodPollingAttempts == 0 { + return 60 + } + + return c.PodPollingAttempts +} + +func (c *Config) Validate() error { + if c.Namespace == "" { + return fmt.Errorf("namespace must be specified") + } + + return nil +} + +type KubernetesClient struct { + clientset kubernetes.Interface + config Config +} + +func NewKubernetesClient(clientset kubernetes.Interface, config Config) (*KubernetesClient, error) { + if err := config.Validate(); err != nil { + return nil, fmt.Errorf("config is invalid: %v", err) + } + + return &KubernetesClient{ + clientset: clientset, + config: config, + }, nil +} + +func NewInClusterClientset() (kubernetes.Interface, error) { + k8sConfig, err := rest.InClusterConfig() + if err != nil { + return nil, err + } + + clientset, err := kubernetes.NewForConfig(k8sConfig) + if err != nil { + return nil, err + } + + return clientset, nil +} + +func NewClientsetFromConfig() (kubernetes.Interface, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("error getting user home directory: %v", err) + } + + kubeConfigPath := filepath.Join(homeDir, ".kube", "config") + kubeConfig, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath) + if err != nil { + return nil, fmt.Errorf("error getting Kubernetes config: %v", err) + } + + clientset, err := kubernetes.NewForConfig(kubeConfig) + if err != nil { + return nil, fmt.Errorf("error creating kubernetes clientset from config file: %v", err) + } + + return clientset, nil +} + +func (c *KubernetesClient) CreateSecret(name string, jobRequest *api.JobRequest) error { + environment, err := shell.CreateEnvironment(jobRequest.EnvVars, []config.HostEnvVar{}) + if err != nil { + return fmt.Errorf("error creating environment: %v", err) + } + + envFileName := filepath.Join(os.TempDir(), ".env") + err = environment.ToFile(envFileName, nil) + if err != nil { + return fmt.Errorf("error creating temporary environment file: %v", err) + } + + envFile, err := os.Open(envFileName) + if err != nil { + return fmt.Errorf("error opening environment file for reading: %v", err) + } + + defer envFile.Close() + + env, err := ioutil.ReadAll(envFile) + if err != nil { + return fmt.Errorf("error reading environment file: %v", err) + } + + // We don't allow the secret to be changed after its creation. + immutable := true + + // We use one key for the environment variables. + data := map[string]string{".env": string(env)} + + // And one key for each file injected in the job definition. + // K8s doesn't allow many special characters in a secret's key; it uses [-._a-zA-Z0-9]+ for validation. + // So, we encode the flle's path (using base64 URL encoding, no padding), + // and use it as the secret's key. + // K8s will inject the file at /tmp/injected/ + // On InjectFiles(), we move the file to its proper place. + for _, file := range jobRequest.Files { + encodedPath := base64.RawURLEncoding.EncodeToString([]byte(file.Path)) + content, err := file.Decode() + if err != nil { + return fmt.Errorf("error decoding file '%s': %v", file.Path, err) + } + + data[encodedPath] = string(content) + } + + secret := corev1.Secret{ + ObjectMeta: v1.ObjectMeta{Name: name, Namespace: c.config.Namespace}, + Type: corev1.SecretTypeOpaque, + Immutable: &immutable, + StringData: data, + } + + _, err = c.clientset.CoreV1(). + Secrets(c.config.Namespace). + Create(context.Background(), &secret, v1.CreateOptions{}) + + if err != nil { + return fmt.Errorf("error creating secret '%s': %v", name, err) + } + + return nil +} + +func (c *KubernetesClient) CreatePod(name string, envSecretName string, jobRequest *api.JobRequest) error { + pod, err := c.podSpecFromJobRequest(name, envSecretName, jobRequest) + if err != nil { + return fmt.Errorf("error building pod spec: %v", err) + } + + _, err = c.clientset.CoreV1(). + Pods(c.config.Namespace). + Create(context.TODO(), pod, v1.CreateOptions{}) + + if err != nil { + return fmt.Errorf("error creating pod: %v", err) + } + + return nil +} + +func (c *KubernetesClient) podSpecFromJobRequest(podName string, envSecretName string, jobRequest *api.JobRequest) (*corev1.Pod, error) { + containers, err := c.containers(jobRequest.Compose.Containers) + if err != nil { + return nil, fmt.Errorf("error building containers for pod spec: %v", err) + } + + spec := corev1.PodSpec{ + Containers: containers, + ImagePullSecrets: c.imagePullSecrets(), + RestartPolicy: corev1.RestartPolicyNever, + Volumes: []corev1.Volume{ + { + Name: "environment", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: envSecretName, + }, + }, + }, + }, + } + + return &corev1.Pod{ + Spec: spec, + ObjectMeta: v1.ObjectMeta{ + Namespace: c.config.Namespace, + Name: podName, + Labels: map[string]string{ + "app": "semaphore-agent", + }, + }, + }, nil +} + +func (c *KubernetesClient) containers(containers []api.Container) ([]corev1.Container, error) { + + // If the job specifies containers in the YAML, we use them. + if len(containers) > 0 { + return c.convertContainersFromSemaphore(containers), nil + } + + // For jobs which do not specify containers, we require the default image to be specified. + if c.config.DefaultImage == "" { + return []corev1.Container{}, fmt.Errorf("no default image specified") + } + + return []corev1.Container{ + { + Name: "main", + Image: c.config.DefaultImage, + ImagePullPolicy: corev1.PullPolicy(c.config.ImagePullPolicy), + VolumeMounts: []corev1.VolumeMount{ + { + Name: "environment", + ReadOnly: true, + MountPath: "/tmp/injected", + }, + }, + + // The k8s pod shouldn't finish, so we sleep infinitely to keep it up. + Command: []string{"bash", "-c", "sleep infinity"}, + }, + }, nil +} + +func (c *KubernetesClient) convertContainersFromSemaphore(containers []api.Container) []corev1.Container { + main, rest := containers[0], containers[1:] + + // The main container needs to be up forever, + // so we 'sleep infinity' in its command. + k8sContainers := []corev1.Container{ + { + Name: main.Name, + Image: main.Image, + Env: c.convertEnvVars(main.EnvVars), + Command: []string{"bash", "-c", "sleep infinity"}, + ImagePullPolicy: corev1.PullPolicy(c.config.ImagePullPolicy), + VolumeMounts: []corev1.VolumeMount{ + { + Name: "environment", + ReadOnly: true, + MountPath: "/tmp/injected", + }, + }, + }, + } + + // The rest of the containers will just follow whatever + // their images are already configured to do. + for _, container := range rest { + k8sContainers = append(k8sContainers, corev1.Container{ + Name: container.Name, + Image: container.Image, + Env: c.convertEnvVars(container.EnvVars), + }) + } + + return k8sContainers +} + +func (c *KubernetesClient) convertEnvVars(envVarsFromSemaphore []api.EnvVar) []corev1.EnvVar { + k8sEnvVars := []corev1.EnvVar{} + + for _, envVar := range envVarsFromSemaphore { + v, _ := base64.StdEncoding.DecodeString(envVar.Value) + k8sEnvVars = append(k8sEnvVars, corev1.EnvVar{ + Name: envVar.Name, + Value: string(v), + }) + } + + return k8sEnvVars +} + +func (c *KubernetesClient) imagePullSecrets() []corev1.LocalObjectReference { + return []corev1.LocalObjectReference{} +} + +func (c *KubernetesClient) WaitForPod(name string, logFn func(string)) error { + return retry.RetryWithConstantWait(retry.RetryOptions{ + Task: "Waiting for pod to be ready", + MaxAttempts: c.config.PollingAttempts(), + DelayBetweenAttempts: c.config.PollingInterval(), + HideError: true, + Fn: func() error { + _, err := c.findPod(name) + if err != nil { + logFn(fmt.Sprintf("Pod is not ready yet: %v\n", err)) + return err + } + + logFn("Pod is ready.\n") + return nil + }, + }) +} + +func (c *KubernetesClient) findPod(name string) (*corev1.Pod, error) { + pod, err := c.clientset.CoreV1(). + Pods(c.config.Namespace). + Get(context.Background(), name, v1.GetOptions{}) + + if err != nil { + return nil, err + } + + // If the pod already finished, something went wrong. + if pod.Status.Phase == corev1.PodFailed || pod.Status.Phase == corev1.PodSucceeded { + return nil, fmt.Errorf( + "pod '%s' already finished with status %s - reason: '%v', message: '%v', statuses: %v", + pod.Name, + pod.Status.Phase, + pod.Status.Reason, + pod.Status.Message, + c.getContainerStatuses(pod.Status.ContainerStatuses), + ) + } + + // if pod is pending, we need to wait + if pod.Status.Phase == corev1.PodPending { + return nil, fmt.Errorf("pod in pending state - statuses: %v", c.getContainerStatuses(pod.Status.ContainerStatuses)) + } + + // if one of the pod's containers isn't ready, we need to wait + for _, container := range pod.Status.ContainerStatuses { + if !container.Ready { + return nil, fmt.Errorf( + "container '%s' is not ready yet - statuses: %v", + container.Name, + c.getContainerStatuses(pod.Status.ContainerStatuses), + ) + } + } + + return pod, nil +} + +func (c *KubernetesClient) getContainerStatuses(statuses []corev1.ContainerStatus) []string { + messages := []string{} + for _, s := range statuses { + if s.State.Terminated != nil { + messages = append( + messages, + fmt.Sprintf( + "container '%s' terminated - reason='%s', message='%s'", + s.Image, + s.State.Terminated.Reason, + s.State.Terminated.Message, + ), + ) + } + + if s.State.Waiting != nil { + messages = append( + messages, + fmt.Sprintf( + "container '%s' waiting - reason='%s', message='%s'", + s.Image, + s.State.Waiting.Reason, + s.State.Waiting.Message, + ), + ) + } + + if s.State.Running != nil { + messages = append( + messages, + fmt.Sprintf( + "container '%s' is running since %v", + s.Image, + s.State.Running.StartedAt, + ), + ) + } + } + + return messages +} + +func (c *KubernetesClient) DeletePod(name string) error { + return c.clientset.CoreV1(). + Pods(c.config.Namespace). + Delete(context.Background(), name, v1.DeleteOptions{}) +} + +func (c *KubernetesClient) DeleteSecret(name string) error { + return c.clientset.CoreV1(). + Secrets(c.config.Namespace). + Delete(context.Background(), name, v1.DeleteOptions{}) +} diff --git a/pkg/kubernetes/client_test.go b/pkg/kubernetes/client_test.go new file mode 100644 index 00000000..9838f1b5 --- /dev/null +++ b/pkg/kubernetes/client_test.go @@ -0,0 +1,351 @@ +package kubernetes + +import ( + "context" + "encoding/base64" + "testing" + + "github.com/semaphoreci/agent/pkg/api" + assert "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" +) + +func Test__CreateSecret(t *testing.T) { + t.Run("stores .env file in secret", func(t *testing.T) { + clientset := newFakeClientset([]runtime.Object{}) + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", DefaultImage: "default-image"}) + secretName := "mysecret" + + // create secret using job request + assert.NoError(t, client.CreateSecret(secretName, &api.JobRequest{ + EnvVars: []api.EnvVar{ + {Name: "A", Value: base64.StdEncoding.EncodeToString([]byte("AAA"))}, + {Name: "B", Value: base64.StdEncoding.EncodeToString([]byte("BBB"))}, + {Name: "C", Value: base64.StdEncoding.EncodeToString([]byte("CCC"))}, + }, + })) + + secret, err := clientset.CoreV1(). + Secrets("default"). + Get(context.Background(), secretName, v1.GetOptions{}) + + assert.NoError(t, err) + assert.True(t, *secret.Immutable) + assert.Equal(t, secret.Name, secretName) + assert.Equal(t, secret.StringData, map[string]string{ + ".env": "export A=AAA\nexport B=BBB\nexport C=CCC\n", + }) + }) + + t.Run("stores files in secret, with base64-encoded keys", func(t *testing.T) { + clientset := newFakeClientset([]runtime.Object{}) + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", DefaultImage: "default-image"}) + secretName := "mysecret" + + // create secret using job request + assert.NoError(t, client.CreateSecret(secretName, &api.JobRequest{ + EnvVars: []api.EnvVar{ + {Name: "A", Value: base64.StdEncoding.EncodeToString([]byte("AAA"))}, + {Name: "B", Value: base64.StdEncoding.EncodeToString([]byte("BBB"))}, + {Name: "C", Value: base64.StdEncoding.EncodeToString([]byte("CCC"))}, + }, + Files: []api.File{ + { + Path: "/tmp/random-file.txt", + Content: base64.StdEncoding.EncodeToString([]byte("Random content")), + Mode: "0600", + }, + { + Path: "/tmp/random-file-2.txt", + Content: base64.StdEncoding.EncodeToString([]byte("Random content 2")), + Mode: "0600", + }, + }, + })) + + secret, err := clientset.CoreV1(). + Secrets("default"). + Get(context.Background(), secretName, v1.GetOptions{}) + + key1 := base64.RawURLEncoding.EncodeToString([]byte("/tmp/random-file.txt")) + key2 := base64.RawURLEncoding.EncodeToString([]byte("/tmp/random-file-2.txt")) + + assert.NoError(t, err) + assert.True(t, *secret.Immutable) + assert.Equal(t, secret.Name, secretName) + assert.Equal(t, secret.StringData, map[string]string{ + ".env": "export A=AAA\nexport B=BBB\nexport C=CCC\n", + key1: "Random content", + key2: "Random content 2", + }) + }) +} + +func Test__CreatePod(t *testing.T) { + t.Run("no containers and no default image specified -> error", func(t *testing.T) { + clientset := newFakeClientset([]runtime.Object{}) + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", ImagePullPolicy: "Never"}) + podName := "mypod" + envSecretName := "mysecret" + + assert.ErrorContains(t, client.CreatePod(podName, envSecretName, &api.JobRequest{ + Compose: api.Compose{ + Containers: []api.Container{}, + }, + }), "no default image specified") + }) + + t.Run("no containers specified in job uses default image", func(t *testing.T) { + clientset := newFakeClientset([]runtime.Object{}) + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", DefaultImage: "default-image", ImagePullPolicy: "Never"}) + podName := "mypod" + envSecretName := "mysecret" + + // create pod using job request + assert.NoError(t, client.CreatePod(podName, envSecretName, &api.JobRequest{ + Compose: api.Compose{ + Containers: []api.Container{}, + }, + })) + + pod, err := clientset.CoreV1(). + Pods("default"). + Get(context.Background(), podName, v1.GetOptions{}) + + assert.NoError(t, err) + + // assert pod metadata + assert.Equal(t, pod.ObjectMeta.Name, podName) + assert.Equal(t, pod.ObjectMeta.Namespace, "default") + assert.Equal(t, pod.ObjectMeta.Labels, map[string]string{"app": "semaphore-agent"}) + + // assert pod spec + assert.Equal(t, pod.Spec.RestartPolicy, corev1.RestartPolicyNever) + assert.Empty(t, pod.Spec.ImagePullSecrets) + + // assert pod spec containers + if assert.Len(t, pod.Spec.Containers, 1) { + assert.Equal(t, pod.Spec.Containers[0].Name, "main") + assert.Equal(t, pod.Spec.Containers[0].Image, "default-image") + assert.Equal(t, pod.Spec.Containers[0].ImagePullPolicy, corev1.PullNever) + assert.Equal(t, pod.Spec.Containers[0].Command, []string{"bash", "-c", "sleep infinity"}) + assert.Empty(t, pod.Spec.Containers[0].Env) + assert.Equal(t, pod.Spec.Containers[0].VolumeMounts, []corev1.VolumeMount{{Name: "environment", ReadOnly: true, MountPath: "/tmp/injected"}}) + } + + // assert pod spec volumes + if assert.Len(t, pod.Spec.Volumes, 1) { + assert.Equal(t, pod.Spec.Volumes[0].Name, "environment") + assert.Equal(t, pod.Spec.Volumes[0].VolumeSource, corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: envSecretName, + }, + }) + } + }) + + t.Run("1 container", func(t *testing.T) { + clientset := newFakeClientset([]runtime.Object{}) + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", DefaultImage: "default-image", ImagePullPolicy: "Always"}) + podName := "mypod" + envSecretName := "mysecret" + + // create pod using job request + assert.NoError(t, client.CreatePod(podName, envSecretName, &api.JobRequest{ + Compose: api.Compose{ + Containers: []api.Container{ + { + Name: "main", + Image: "custom-image", + }, + }, + }, + })) + + pod, err := clientset.CoreV1(). + Pods("default"). + Get(context.Background(), podName, v1.GetOptions{}) + + assert.NoError(t, err) + + // assert pod spec containers + if assert.Len(t, pod.Spec.Containers, 1) { + assert.Equal(t, pod.Spec.Containers[0].Name, "main") + assert.Equal(t, pod.Spec.Containers[0].Image, "custom-image") + assert.Equal(t, pod.Spec.Containers[0].ImagePullPolicy, corev1.PullAlways) + assert.Equal(t, pod.Spec.Containers[0].Command, []string{"bash", "-c", "sleep infinity"}) + assert.Empty(t, pod.Spec.Containers[0].Env) + assert.Equal(t, pod.Spec.Containers[0].VolumeMounts, []corev1.VolumeMount{{Name: "environment", ReadOnly: true, MountPath: "/tmp/injected"}}) + } + }) + + t.Run("container with env vars", func(t *testing.T) { + clientset := newFakeClientset([]runtime.Object{}) + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", DefaultImage: "default-image", ImagePullPolicy: "Always"}) + podName := "mypod" + envSecretName := "mysecret" + + // create pod using job request + assert.NoError(t, client.CreatePod(podName, envSecretName, &api.JobRequest{ + Compose: api.Compose{ + Containers: []api.Container{ + { + Name: "main", + Image: "custom-image", + EnvVars: []api.EnvVar{ + { + Name: "A", + Value: base64.StdEncoding.EncodeToString([]byte("AAA")), + }, + }, + }, + }, + }, + })) + + pod, err := clientset.CoreV1(). + Pods("default"). + Get(context.Background(), podName, v1.GetOptions{}) + + assert.NoError(t, err) + if assert.Len(t, pod.Spec.Containers, 1) { + assert.Equal(t, pod.Spec.Containers[0].Name, "main") + assert.Equal(t, pod.Spec.Containers[0].Image, "custom-image") + assert.Equal(t, pod.Spec.Containers[0].ImagePullPolicy, corev1.PullAlways) + assert.Equal(t, pod.Spec.Containers[0].Command, []string{"bash", "-c", "sleep infinity"}) + assert.Equal(t, pod.Spec.Containers[0].Env, []corev1.EnvVar{{Name: "A", Value: "AAA"}}) + assert.Equal(t, pod.Spec.Containers[0].VolumeMounts, []corev1.VolumeMount{{Name: "environment", ReadOnly: true, MountPath: "/tmp/injected"}}) + } + }) + + t.Run("multiple containers", func(t *testing.T) { + clientset := newFakeClientset([]runtime.Object{}) + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", DefaultImage: "default-image", ImagePullPolicy: "Always"}) + podName := "mypod" + envSecretName := "mysecret" + + // create pod using job request + assert.NoError(t, client.CreatePod(podName, envSecretName, &api.JobRequest{ + Compose: api.Compose{ + Containers: []api.Container{ + { + Name: "main", + Image: "custom-image", + }, + { + Name: "db", + Image: "postgres:9.6", + }, + }, + }, + })) + + pod, err := clientset.CoreV1(). + Pods("default"). + Get(context.Background(), podName, v1.GetOptions{}) + + assert.NoError(t, err) + + // assert 2 containers are used and command and volume mounts are only set for the main one + if assert.Len(t, pod.Spec.Containers, 2) { + assert.Equal(t, pod.Spec.Containers[0].Name, "main") + assert.Equal(t, pod.Spec.Containers[0].Image, "custom-image") + assert.Equal(t, pod.Spec.Containers[0].Command, []string{"bash", "-c", "sleep infinity"}) + assert.Empty(t, pod.Spec.Containers[0].Env) + assert.Equal(t, pod.Spec.Containers[0].VolumeMounts, []corev1.VolumeMount{{Name: "environment", ReadOnly: true, MountPath: "/tmp/injected"}}) + assert.Equal(t, pod.Spec.Containers[1].Name, "db") + assert.Equal(t, pod.Spec.Containers[1].Image, "postgres:9.6") + assert.Empty(t, pod.Spec.Containers[1].Env) + assert.Empty(t, pod.Spec.Containers[1].Command) + assert.Empty(t, pod.Spec.Containers[1].VolumeMounts) + } + }) +} + +func Test__WaitForPod(t *testing.T) { + t.Run("pod exist and is ready - no error", func(t *testing.T) { + podName := "mypod" + clientset := newFakeClientset([]runtime.Object{ + &corev1.Pod{ + ObjectMeta: v1.ObjectMeta{Name: podName, Namespace: "default"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "main", Image: "whatever"}}, + }, + }, + }) + + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", DefaultImage: "default-image"}) + assert.NoError(t, client.WaitForPod(podName, func(s string) {})) + }) + + t.Run("pod does not exist - error", func(t *testing.T) { + podName := "mypod" + clientset := newFakeClientset([]runtime.Object{ + &corev1.Pod{ + ObjectMeta: v1.ObjectMeta{Name: podName, Namespace: "default"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "main", Image: "whatever"}}, + }, + }, + }) + + client, _ := NewKubernetesClient(clientset, Config{ + Namespace: "default", + DefaultImage: "default-image", + PodPollingAttempts: 2, + }) + + assert.Error(t, client.WaitForPod("somepodthatdoesnotexist", func(s string) {})) + }) +} + +func Test_DeletePod(t *testing.T) { + podName := "mypod" + clientset := newFakeClientset([]runtime.Object{ + &corev1.Pod{ + ObjectMeta: v1.ObjectMeta{Name: podName, Namespace: "default"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "main", Image: "whatever"}}, + }, + }, + }) + + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", DefaultImage: "default-image"}) + assert.NoError(t, client.DeletePod(podName)) + + // pod does not exist anymore + pod, err := clientset.CoreV1(). + Pods("default"). + Get(context.Background(), podName, v1.GetOptions{}) + assert.Error(t, err) + assert.Nil(t, pod) +} + +func Test_DeleteSecret(t *testing.T) { + secretName := "mysecret" + clientset := newFakeClientset([]runtime.Object{ + &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{Name: secretName, Namespace: "default"}, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{}, + }, + }) + + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", DefaultImage: "default-image"}) + assert.NoError(t, client.DeleteSecret(secretName)) + + // secret does not exist anymore + pod, err := clientset.CoreV1(). + Secrets("default"). + Get(context.Background(), secretName, v1.GetOptions{}) + assert.Error(t, err) + assert.Nil(t, pod) +} + +func newFakeClientset(objects []runtime.Object) kubernetes.Interface { + return fake.NewSimpleClientset(objects...) +} diff --git a/pkg/listener/job_processor.go b/pkg/listener/job_processor.go index db81f650..d2753384 100644 --- a/pkg/listener/job_processor.go +++ b/pkg/listener/job_processor.go @@ -23,21 +23,25 @@ import ( func StartJobProcessor(httpClient *http.Client, apiClient *selfhostedapi.API, config Config) (*JobProcessor, error) { p := &JobProcessor{ - HTTPClient: httpClient, - APIClient: apiClient, - LastSuccessfulSync: time.Now(), - State: selfhostedapi.AgentStateWaitingForJobs, - DisconnectRetryAttempts: 100, - GetJobRetryAttempts: config.GetJobRetryLimit, - CallbackRetryAttempts: config.CallbackRetryLimit, - ShutdownHookPath: config.ShutdownHookPath, - PreJobHookPath: config.PreJobHookPath, - EnvVars: config.EnvVars, - FileInjections: config.FileInjections, - FailOnMissingFiles: config.FailOnMissingFiles, - UploadJobLogs: config.UploadJobLogs, - FailOnPreJobHookError: config.FailOnPreJobHookError, - ExitOnShutdown: config.ExitOnShutdown, + HTTPClient: httpClient, + APIClient: apiClient, + LastSuccessfulSync: time.Now(), + State: selfhostedapi.AgentStateWaitingForJobs, + DisconnectRetryAttempts: 100, + GetJobRetryAttempts: config.GetJobRetryLimit, + CallbackRetryAttempts: config.CallbackRetryLimit, + ShutdownHookPath: config.ShutdownHookPath, + PreJobHookPath: config.PreJobHookPath, + EnvVars: config.EnvVars, + FileInjections: config.FileInjections, + FailOnMissingFiles: config.FailOnMissingFiles, + UploadJobLogs: config.UploadJobLogs, + FailOnPreJobHookError: config.FailOnPreJobHookError, + ExitOnShutdown: config.ExitOnShutdown, + KubernetesExecutor: config.KubernetesExecutor, + KubernetesDefaultImage: config.KubernetesDefaultImage, + KubernetesImagePullPolicy: config.KubernetesImagePullPolicy, + KubernetesPodStartTimeoutSeconds: config.KubernetesPodStartTimeoutSeconds, } go p.Start() @@ -48,28 +52,36 @@ func StartJobProcessor(httpClient *http.Client, apiClient *selfhostedapi.API, co } type JobProcessor struct { - HTTPClient *http.Client - APIClient *selfhostedapi.API - State selfhostedapi.AgentState - CurrentJobID string - CurrentJobResult selfhostedapi.JobResult - CurrentJob *jobs.Job - LastSyncErrorAt *time.Time - LastSuccessfulSync time.Time - DisconnectRetryAttempts int - GetJobRetryAttempts int - CallbackRetryAttempts int - ShutdownHookPath string - PreJobHookPath string - StopSync bool - EnvVars []config.HostEnvVar - FileInjections []config.FileInjection - FailOnMissingFiles bool - UploadJobLogs string - FailOnPreJobHookError bool - ExitOnShutdown bool - ShutdownReason ShutdownReason - mutex sync.Mutex + + // Job processor state + HTTPClient *http.Client + APIClient *selfhostedapi.API + State selfhostedapi.AgentState + CurrentJobID string + CurrentJobResult selfhostedapi.JobResult + CurrentJob *jobs.Job + LastSyncErrorAt *time.Time + LastSuccessfulSync time.Time + ShutdownReason ShutdownReason + mutex sync.Mutex + + // Job processor config + DisconnectRetryAttempts int + GetJobRetryAttempts int + CallbackRetryAttempts int + ShutdownHookPath string + PreJobHookPath string + StopSync bool + EnvVars []config.HostEnvVar + FileInjections []config.FileInjection + FailOnMissingFiles bool + UploadJobLogs string + FailOnPreJobHookError bool + ExitOnShutdown bool + KubernetesExecutor bool + KubernetesDefaultImage string + KubernetesImagePullPolicy string + KubernetesPodStartTimeoutSeconds int } func (p *JobProcessor) Start() { @@ -155,13 +167,17 @@ func (p *JobProcessor) RunJob(jobID string) { } job, err := jobs.NewJobWithOptions(&jobs.JobOptions{ - Request: jobRequest, - Client: p.HTTPClient, - ExposeKvmDevice: false, - FileInjections: p.FileInjections, - FailOnMissingFiles: p.FailOnMissingFiles, - SelfHosted: true, - UploadJobLogs: p.UploadJobLogs, + Request: jobRequest, + Client: p.HTTPClient, + ExposeKvmDevice: false, + FileInjections: p.FileInjections, + FailOnMissingFiles: p.FailOnMissingFiles, + SelfHosted: true, + UseKubernetesExecutor: p.KubernetesExecutor, + KubernetesDefaultImage: p.KubernetesDefaultImage, + KubernetesImagePullPolicy: p.KubernetesImagePullPolicy, + KubernetesPodStartTimeoutSeconds: p.KubernetesPodStartTimeoutSeconds, + UploadJobLogs: p.UploadJobLogs, RefreshTokenFn: func() (string, error) { return p.APIClient.RefreshToken() }, diff --git a/pkg/listener/listener.go b/pkg/listener/listener.go index 3bbea26a..3414a1b0 100644 --- a/pkg/listener/listener.go +++ b/pkg/listener/listener.go @@ -20,24 +20,28 @@ type Listener struct { } type Config struct { - Endpoint string - RegisterRetryLimit int - GetJobRetryLimit int - CallbackRetryLimit int - Token string - Scheme string - ShutdownHookPath string - PreJobHookPath string - DisconnectAfterJob bool - DisconnectAfterIdleSeconds int - EnvVars []config.HostEnvVar - FileInjections []config.FileInjection - FailOnMissingFiles bool - UploadJobLogs string - FailOnPreJobHookError bool - ExitOnShutdown bool - AgentVersion string - AgentName string + Endpoint string + RegisterRetryLimit int + GetJobRetryLimit int + CallbackRetryLimit int + Token string + Scheme string + ShutdownHookPath string + PreJobHookPath string + DisconnectAfterJob bool + DisconnectAfterIdleSeconds int + EnvVars []config.HostEnvVar + FileInjections []config.FileInjection + FailOnMissingFiles bool + UploadJobLogs string + FailOnPreJobHookError bool + ExitOnShutdown bool + AgentVersion string + AgentName string + KubernetesExecutor bool + KubernetesDefaultImage string + KubernetesImagePullPolicy string + KubernetesPodStartTimeoutSeconds int } func Start(httpClient *http.Client, config Config) (*Listener, error) { diff --git a/pkg/shell/env.go b/pkg/shell/env.go index a96462b9..3dbb3dd6 100644 --- a/pkg/shell/env.go +++ b/pkg/shell/env.go @@ -110,6 +110,17 @@ func (e *Environment) ToSlice() []string { return arr } +func (e *Environment) ToCommands() []string { + commands := []string{} + + for _, name := range e.Keys() { + value, _ := e.Get(name) + commands = append(commands, fmt.Sprintf("export %s=%s\n", name, shellQuote(value))) + } + + return commands +} + func (e *Environment) ToFile(fileName string, callback func(name string)) error { fileContent := "" for _, name := range e.Keys() { diff --git a/pkg/shell/env_test.go b/pkg/shell/env_test.go index 4bc402f2..b023fbd5 100644 --- a/pkg/shell/env_test.go +++ b/pkg/shell/env_test.go @@ -144,6 +144,22 @@ func Test__EnvironmentToSlice(t *testing.T) { assert.Contains(t, env.ToSlice(), "C=CCC") } +func Test__EnvironmentToCommands(t *testing.T) { + varsFromRequest := []api.EnvVar{ + {Name: "A", Value: base64.StdEncoding.EncodeToString([]byte("AAA"))}, + {Name: "B", Value: base64.StdEncoding.EncodeToString([]byte("BBB"))}, + {Name: "C", Value: base64.StdEncoding.EncodeToString([]byte("CCC"))}, + } + + env, err := CreateEnvironment(varsFromRequest, []config.HostEnvVar{}) + assert.Nil(t, err) + assert.Equal(t, env.ToCommands(), []string{ + "export A=AAA\n", + "export B=BBB\n", + "export C=CCC\n", + }) +} + func Test__EnvironmentAppend(t *testing.T) { vars := []api.EnvVar{ {Name: "C", Value: base64.StdEncoding.EncodeToString([]byte("CCC"))}, diff --git a/pkg/shell/process.go b/pkg/shell/process.go index 7cb7c6a7..d78c7a2a 100644 --- a/pkg/shell/process.go +++ b/pkg/shell/process.go @@ -1,6 +1,7 @@ package shell import ( + "encoding/base64" "flag" "fmt" "io" @@ -35,26 +36,28 @@ exit $Env:SEMAPHORE_AGENT_CURRENT_CMD_EXIT_STATUS ` type Config struct { - Shell *Shell - StoragePath string - Command string - OnOutput func(string) + Shell *Shell + StoragePath string + Command string + OnOutput func(string) + UseBase64Encoding bool } type Process struct { - Command string - Shell *Shell - StoragePath string - StartedAt int - FinishedAt int - ExitCode int - Pid int - startMark string - endMark string - commandEndRegex *regexp.Regexp - inputBuffer []byte - outputBuffer *OutputBuffer - SysProcAttr *syscall.SysProcAttr + Command string + Shell *Shell + StoragePath string + StartedAt int + FinishedAt int + ExitCode int + Pid int + startMark string + endMark string + commandEndRegex *regexp.Regexp + inputBuffer []byte + outputBuffer *OutputBuffer + SysProcAttr *syscall.SysProcAttr + UseBase64Encoding bool } func randomMagicMark() string { @@ -68,14 +71,15 @@ func NewProcess(config Config) *Process { outputBuffer, _ := NewOutputBuffer(config.OnOutput) return &Process{ - Shell: config.Shell, - StoragePath: config.StoragePath, - Command: config.Command, - ExitCode: 1, - startMark: startMark, - endMark: endMark, - commandEndRegex: commandEndRegex, - outputBuffer: outputBuffer, + Shell: config.Shell, + StoragePath: config.StoragePath, + Command: config.Command, + ExitCode: 1, + startMark: startMark, + endMark: endMark, + commandEndRegex: commandEndRegex, + outputBuffer: outputBuffer, + UseBase64Encoding: config.UseBase64Encoding, } } @@ -103,18 +107,18 @@ func (p *Process) flushInputBufferTill(index int) { } func (p *Process) Run() { - instruction := p.constructShellInstruction() - p.StartedAt = int(time.Now().Unix()) - defer func() { - p.FinishedAt = int(time.Now().Unix()) - }() - err := p.loadCommand() if err != nil { log.Errorf("Err: %v", err) return } + instruction := p.constructShellInstruction() + p.StartedAt = int(time.Now().Unix()) + defer func() { + p.FinishedAt = int(time.Now().Unix()) + }() + /* * If the agent is running in an non-windows environment, * we use a PTY session to run commands. @@ -256,6 +260,23 @@ func (p *Process) runWithPTY(instruction string) { } func (p *Process) constructShellInstruction() string { + /* + * When the agent and the PTY executing the commands can't share + * a folder, we need to execute the command without the aid of a temporary file. + * To handle that, we make the agent encode the command here, + * and make the PTY decode it before executing it. That allows us to handle + * multiline commands and commands with quotes without going into escaping character hell. + */ + if p.UseBase64Encoding { + base64EncodedCommand := base64.StdEncoding.EncodeToString([]byte(p.Command)) + return fmt.Sprintf( + `echo -e "\001 %s"; source <(echo %s | base64 -d); AGENT_CMD_RESULT=$?; echo -e "\001 %s $AGENT_CMD_RESULT"; echo "exit $AGENT_CMD_RESULT" | sh`, + p.startMark, + base64EncodedCommand, + p.endMark, + ) + } + if runtime.GOOS == "windows" { return fmt.Sprintf(`%s.ps1`, p.CmdFilePath()) } @@ -280,6 +301,13 @@ func (p *Process) constructShellInstruction() string { * scheme. To circumvent this, we are storing the command in a file. */ func (p *Process) loadCommand() error { + + // If we are using base64 encoding when executing the command, + // we don't need a temporary file for storing it, so we don't create it. + if p.UseBase64Encoding { + return nil + } + if runtime.GOOS != "windows" { return p.writeCommandToFile(p.CmdFilePath(), p.Command) } diff --git a/pkg/shell/shell.go b/pkg/shell/shell.go index 34e4d75b..5733e321 100644 --- a/pkg/shell/shell.go +++ b/pkg/shell/shell.go @@ -234,6 +234,10 @@ func (s *Shell) NewProcessWithOutput(command string, outputConsumer func(string) }) } +func (s *Shell) NewProcessWithConfig(config Config) *Process { + return NewProcess(config) +} + func (s *Shell) Close() error { if s.TTY != nil { err := s.TTY.Close() diff --git a/pkg/shell/shell_test.go b/pkg/shell/shell_test.go index 8b3f56a8..d58f6411 100644 --- a/pkg/shell/shell_test.go +++ b/pkg/shell/shell_test.go @@ -57,6 +57,29 @@ func Test__Shell__SimpleHelloWorld(t *testing.T) { assert.Equal(t, output.String(), "Hello\n") } +func Test__Shell__SimpleHelloWorldUsingBase64Encoding(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip() + } + + var output bytes.Buffer + + shell, _ := NewShell(os.TempDir()) + shell.Start() + + p1 := shell.NewProcessWithConfig(Config{ + Command: "echo Hello", + UseBase64Encoding: true, + Shell: shell, + OnOutput: func(line string) { + output.WriteString(line) + }, + }) + + p1.Run() + assert.Equal(t, output.String(), "Hello\n") +} + func Test__Shell__HandlingBashProcessKill(t *testing.T) { var output bytes.Buffer diff --git a/test/e2e.rb b/test/e2e.rb index de1c05af..7bd065cf 100644 --- a/test/e2e.rb +++ b/test/e2e.rb @@ -26,14 +26,14 @@ $LOGGER = <<-JSON { "method": "push", - "url": "http://hub:4567/api/v1/logs/#{$JOB_ID}", + "url": "http://localhost:4567/api/v1/logs/#{$JOB_ID}", "token": "jwtToken" } JSON if !$AGENT_CONFIG $AGENT_CONFIG = { - "endpoint" => "hub:4567", + "endpoint" => "localhost:4567", "token" => "321h1l2jkh1jk42341", "no-https" => true, "shutdown-hook-path" => "", diff --git a/test/e2e/kubernetes/docker_compose__env-vars.rb b/test/e2e/kubernetes/docker_compose__env-vars.rb new file mode 100644 index 00000000..d2c11aa7 --- /dev/null +++ b/test/e2e/kubernetes/docker_compose__env-vars.rb @@ -0,0 +1,108 @@ +#!/bin/ruby +# rubocop:disable all + +$AGENT_CONFIG = { + "endpoint" => "localhost:4567", + "token" => "321h1l2jkh1jk42341", + "no-https" => true, + "shutdown-hook-path" => "", + "disconnect-after-job" => false, + "env-vars" => [], + "files" => [], + "fail-on-missing-files" => false, + "kubernetes-executor" => true, + "kubernetes-image-pull-policy" => "IfNotPresent" +} + +require_relative '../../e2e' + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + + "executor": "dockercompose", + + "compose": { + "containers": [ + { + "name": "main", + "image": "ruby:3-slim", + "env_vars": [ + { "name": "FOO", "value": "#{`echo "bar" | base64 | tr -d '\n'`}" } + ] + } + ] + }, + + "env_vars": [ + { "name": "A", "value": "#{`echo "hello" | base64 | tr -d '\n'`}" }, + { "name": "B", "value": "#{`echo "how are you?" | base64 | tr -d '\n'`}" }, + { "name": "C", "value": "#{`echo "quotes ' quotes" | base64 | tr -d '\n'`}" }, + { "name": "D", "value": "#{`echo '$PATH:/etc/a' | base64 | tr -d '\n'`}" } + ], + + "files": [], + + "commands": [ + { "directive": "echo $A" }, + { "directive": "echo $B" }, + { "directive": "echo $C" }, + { "directive": "echo $D" }, + { "directive": "echo $FOO" } + ], + + "epilogue_always_commands": [], + + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Starting shell session..."} + *** LONG_OUTPUT *** + {"event":"cmd_finished", "timestamp":"*", "directive":"Starting shell session...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting A\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting B\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting C\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting D\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"echo $A"} + {"event":"cmd_output", "timestamp":"*", "output":"hello\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo $A","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"echo $B"} + {"event":"cmd_output", "timestamp":"*", "output":"how are you?\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo $B","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"echo $C"} + {"event":"cmd_output", "timestamp":"*", "output":"quotes ' quotes\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo $C","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"echo $D"} + {"event":"cmd_output", "timestamp":"*", "output":"$PATH:/etc/a\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo $D","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"echo $FOO"} + {"event":"cmd_output", "timestamp":"*", "output":"bar\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo $FOO","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + + {"event":"job_finished", "timestamp":"*", "result":"passed"} +LOG diff --git a/test/e2e/kubernetes/docker_compose__epilogue.rb b/test/e2e/kubernetes/docker_compose__epilogue.rb new file mode 100644 index 00000000..f6772e6c --- /dev/null +++ b/test/e2e/kubernetes/docker_compose__epilogue.rb @@ -0,0 +1,74 @@ +#!/bin/ruby +# rubocop:disable all + +$AGENT_CONFIG = { + "endpoint" => "localhost:4567", + "token" => "321h1l2jkh1jk42341", + "no-https" => true, + "shutdown-hook-path" => "", + "disconnect-after-job" => false, + "env-vars" => [], + "files" => [], + "fail-on-missing-files" => false, + "kubernetes-executor" => true, + "kubernetes-image-pull-policy" => "IfNotPresent" +} + +require_relative '../../e2e' + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + "executor": "dockercompose", + "compose": { + "containers": [ + { + "name": "main", + "image": "ruby:3-slim" + } + ] + }, + "env_vars": [], + "files": [], + "commands": [ + { "directive": "echo Hello World" } + ], + "epilogue_always_commands": [ + { "directive": "echo Hello Epilogue $SEMAPHORE_JOB_RESULT" } + ], + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Starting shell session..."} + *** LONG_OUTPUT *** + {"event":"cmd_finished", "timestamp":"*", "directive":"Starting shell session...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello World"} + {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello World","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello Epilogue $SEMAPHORE_JOB_RESULT"} + {"event":"cmd_output", "timestamp":"*", "output":"Hello Epilogue passed\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello Epilogue $SEMAPHORE_JOB_RESULT","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"job_finished", "timestamp":"*", "result":"passed"} +LOG diff --git a/test/e2e/kubernetes/docker_compose__file-injection.rb b/test/e2e/kubernetes/docker_compose__file-injection.rb new file mode 100644 index 00000000..c0522c26 --- /dev/null +++ b/test/e2e/kubernetes/docker_compose__file-injection.rb @@ -0,0 +1,87 @@ +#!/bin/ruby +# rubocop:disable all + +$AGENT_CONFIG = { + "endpoint" => "localhost:4567", + "token" => "321h1l2jkh1jk42341", + "no-https" => true, + "shutdown-hook-path" => "", + "disconnect-after-job" => false, + "env-vars" => [], + "files" => [], + "fail-on-missing-files" => false, + "kubernetes-executor" => true, + "kubernetes-image-pull-policy" => "IfNotPresent" +} + +require_relative '../../e2e' + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + "executor": "dockercompose", + "compose": { + "containers": [ + { + "name": "main", + "image": "ruby:3-slim" + } + ] + }, + "env_vars": [], + "files": [ + { "path": "test.txt", "content": "#{`echo "hello" | base64 | tr -d '\n'`}", "mode": "0644" }, + { "path": "/a/b/c", "content": "#{`echo "hello" | base64 | tr -d '\n'`}", "mode": "0644" }, + { "path": "/tmp/a", "content": "#{`echo "hello" | base64 | tr -d '\n'`}", "mode": "0600" } + ], + "commands": [ + { "directive": "cat test.txt" }, + { "directive": "cat /a/b/c" }, + { "directive": "stat -c '%a' /tmp/a" } + ], + + "epilogue_always_commands": [], + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Starting shell session..."} + *** LONG_OUTPUT *** + {"event":"cmd_finished", "timestamp":"*", "directive":"Starting shell session...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_output", "timestamp":"*", "output":"Injecting test.txt with file mode 0644\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Injecting /a/b/c with file mode 0644\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Injecting /tmp/a with file mode 0600\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"cat test.txt"} + {"event":"cmd_output", "timestamp":"*", "output":"hello\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"cat test.txt","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"cat /a/b/c"} + {"event":"cmd_output", "timestamp":"*", "output":"hello\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"cat /a/b/c","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"stat -c '%a' /tmp/a"} + {"event":"cmd_output", "timestamp":"*", "output":"600\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"stat -c '%a' /tmp/a","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + + {"event":"job_finished", "timestamp":"*", "result":"passed"} +LOG diff --git a/test/e2e/kubernetes/docker_compose__multiple-containers.rb b/test/e2e/kubernetes/docker_compose__multiple-containers.rb new file mode 100644 index 00000000..e3407a7b --- /dev/null +++ b/test/e2e/kubernetes/docker_compose__multiple-containers.rb @@ -0,0 +1,84 @@ +#!/bin/ruby +# rubocop:disable all + +$AGENT_CONFIG = { + "endpoint" => "localhost:4567", + "token" => "321h1l2jkh1jk42341", + "no-https" => true, + "shutdown-hook-path" => "", + "disconnect-after-job" => false, + "env-vars" => [], + "files" => [], + "fail-on-missing-files" => false, + "kubernetes-executor" => true, + "kubernetes-image-pull-policy" => "IfNotPresent" +} + +require_relative '../../e2e' + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + + "executor": "dockercompose", + + "compose": { + "containers": [ + { + "name": "main", + "image": "ruby:3-slim" + }, + { + "name": "redis", + "image": "redis:6" + } + ] + }, + + "env_vars": [], + "files": [], + + "commands": [ + { "directive": "apt update && apt install redis-tools -y" }, + { "directive": "redis-cli ping" } + ], + + "epilogue_always_commands": [], + + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Starting shell session..."} + *** LONG_OUTPUT *** + {"event":"cmd_finished", "timestamp":"*", "directive":"Starting shell session...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"apt update && apt install redis-tools -y"} + *** LONG_OUTPUT *** + {"event":"cmd_finished", "timestamp":"*", "directive":"apt update && apt install redis-tools -y","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"redis-cli ping"} + {"event":"cmd_output", "timestamp":"*", "output":"PONG\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"redis-cli ping","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + + {"event":"job_finished", "timestamp":"*", "result":"passed"} +LOG diff --git a/test/e2e/kubernetes/shell__env-vars.rb b/test/e2e/kubernetes/shell__env-vars.rb new file mode 100644 index 00000000..f2a62c7d --- /dev/null +++ b/test/e2e/kubernetes/shell__env-vars.rb @@ -0,0 +1,90 @@ +#!/bin/ruby +# rubocop:disable all + +$AGENT_CONFIG = { + "endpoint" => "localhost:4567", + "token" => "321h1l2jkh1jk42341", + "no-https" => true, + "shutdown-hook-path" => "", + "disconnect-after-job" => false, + "env-vars" => [], + "files" => [], + "fail-on-missing-files" => false, + "kubernetes-executor" => true, + "kubernetes-default-image" => "ruby:3-slim", + "kubernetes-image-pull-policy" => "IfNotPresent" +} + +require_relative '../../e2e' + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + "executor": "shell", + "env_vars": [ + { "name": "A", "value": "#{`echo "hello" | base64 | tr -d '\n'`}" }, + { "name": "B", "value": "#{`echo "how are you?" | base64 | tr -d '\n'`}" }, + { "name": "C", "value": "#{`echo "quotes ' quotes" | base64 | tr -d '\n'`}" }, + { "name": "D", "value": "#{`echo '$PATH:/etc/a' | base64 | tr -d '\n'`}" } + ], + + "files": [], + + "commands": [ + { "directive": "echo $A" }, + { "directive": "echo $B" }, + { "directive": "echo $C" }, + { "directive": "echo $D" } + ], + + "epilogue_always_commands": [], + + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Starting shell session..."} + *** LONG_OUTPUT *** + {"event":"cmd_finished", "timestamp":"*", "directive":"Starting shell session...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting A\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting B\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting C\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting D\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"echo $A"} + {"event":"cmd_output", "timestamp":"*", "output":"hello\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo $A","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"echo $B"} + {"event":"cmd_output", "timestamp":"*", "output":"how are you?\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo $B","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"echo $C"} + {"event":"cmd_output", "timestamp":"*", "output":"quotes ' quotes\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo $C","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"echo $D"} + {"event":"cmd_output", "timestamp":"*", "output":"$PATH:/etc/a\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo $D","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + + {"event":"job_finished", "timestamp":"*", "result":"passed"} +LOG diff --git a/test/e2e/kubernetes/shell__epilogue.rb b/test/e2e/kubernetes/shell__epilogue.rb new file mode 100644 index 00000000..f20af36d --- /dev/null +++ b/test/e2e/kubernetes/shell__epilogue.rb @@ -0,0 +1,67 @@ +#!/bin/ruby +# rubocop:disable all + +$AGENT_CONFIG = { + "endpoint" => "localhost:4567", + "token" => "321h1l2jkh1jk42341", + "no-https" => true, + "shutdown-hook-path" => "", + "disconnect-after-job" => false, + "env-vars" => [], + "files" => [], + "fail-on-missing-files" => false, + "kubernetes-executor" => true, + "kubernetes-default-image" => "ruby:3-slim", + "kubernetes-image-pull-policy" => "IfNotPresent" +} + +require_relative '../../e2e' + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + "executor": "shell", + "env_vars": [], + "files": [], + "commands": [ + { "directive": "echo Hello World" } + ], + "epilogue_always_commands": [ + { "directive": "echo Hello Epilogue $SEMAPHORE_JOB_RESULT" } + ], + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Starting shell session..."} + *** LONG_OUTPUT *** + {"event":"cmd_finished", "timestamp":"*", "directive":"Starting shell session...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello World"} + {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello World","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello Epilogue $SEMAPHORE_JOB_RESULT"} + {"event":"cmd_output", "timestamp":"*", "output":"Hello Epilogue passed\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello Epilogue $SEMAPHORE_JOB_RESULT","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"job_finished", "timestamp":"*", "result":"passed"} +LOG diff --git a/test/e2e/kubernetes/shell__file-injection.rb b/test/e2e/kubernetes/shell__file-injection.rb new file mode 100644 index 00000000..330e0c67 --- /dev/null +++ b/test/e2e/kubernetes/shell__file-injection.rb @@ -0,0 +1,80 @@ +#!/bin/ruby +# rubocop:disable all + +$AGENT_CONFIG = { + "endpoint" => "localhost:4567", + "token" => "321h1l2jkh1jk42341", + "no-https" => true, + "shutdown-hook-path" => "", + "disconnect-after-job" => false, + "env-vars" => [], + "files" => [], + "fail-on-missing-files" => false, + "kubernetes-executor" => true, + "kubernetes-default-image" => "ruby:3-slim", + "kubernetes-image-pull-policy" => "IfNotPresent" +} + +require_relative '../../e2e' + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + "executor": "shell", + "env_vars": [], + "files": [ + { "path": "test.txt", "content": "#{`echo "hello" | base64 | tr -d '\n'`}", "mode": "0644" }, + { "path": "/a/b/c", "content": "#{`echo "hello" | base64 | tr -d '\n'`}", "mode": "0644" }, + { "path": "/tmp/a", "content": "#{`echo "hello" | base64 | tr -d '\n'`}", "mode": "0600" } + ], + "commands": [ + { "directive": "cat test.txt" }, + { "directive": "cat /a/b/c" }, + { "directive": "stat -c '%a' /tmp/a" } + ], + + "epilogue_always_commands": [], + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Starting shell session..."} + *** LONG_OUTPUT *** + {"event":"cmd_finished", "timestamp":"*", "directive":"Starting shell session...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_output", "timestamp":"*", "output":"Injecting test.txt with file mode 0644\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Injecting /a/b/c with file mode 0644\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Injecting /tmp/a with file mode 0600\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"cat test.txt"} + {"event":"cmd_output", "timestamp":"*", "output":"hello\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"cat test.txt","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"cat /a/b/c"} + {"event":"cmd_output", "timestamp":"*", "output":"hello\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"cat /a/b/c","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"stat -c '%a' /tmp/a"} + {"event":"cmd_output", "timestamp":"*", "output":"600\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"stat -c '%a' /tmp/a","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + + {"event":"job_finished", "timestamp":"*", "result":"passed"} +LOG diff --git a/test/e2e/self-hosted/docker_compose_fail_on_missing_host_files.rb b/test/e2e/self-hosted/docker_compose_fail_on_missing_host_files.rb index 04edd0d3..cb25c7e5 100644 --- a/test/e2e/self-hosted/docker_compose_fail_on_missing_host_files.rb +++ b/test/e2e/self-hosted/docker_compose_fail_on_missing_host_files.rb @@ -5,7 +5,7 @@ File.write("/tmp/agent/file2.txt", "Hello from file2.txt") $AGENT_CONFIG = { - "endpoint" => "hub:4567", + "endpoint" => "localhost:4567", "token" => "321h1l2jkh1jk42341", "no-https" => true, "shutdown-hook-path" => "", diff --git a/test/e2e/self-hosted/docker_compose_host_env_vars.rb b/test/e2e/self-hosted/docker_compose_host_env_vars.rb index f9fce855..feda0fdf 100644 --- a/test/e2e/self-hosted/docker_compose_host_env_vars.rb +++ b/test/e2e/self-hosted/docker_compose_host_env_vars.rb @@ -2,7 +2,7 @@ # rubocop:disable all $AGENT_CONFIG = { - "endpoint" => "hub:4567", + "endpoint" => "localhost:4567", "token" => "321h1l2jkh1jk42341", "no-https" => true, "shutdown-hook-path" => "", diff --git a/test/e2e/self-hosted/docker_compose_host_files.rb b/test/e2e/self-hosted/docker_compose_host_files.rb index 13711b0b..00b4d280 100644 --- a/test/e2e/self-hosted/docker_compose_host_files.rb +++ b/test/e2e/self-hosted/docker_compose_host_files.rb @@ -5,7 +5,7 @@ File.write("/tmp/agent/file2.txt", "Hello from file2.txt") $AGENT_CONFIG = { - "endpoint" => "hub:4567", + "endpoint" => "localhost:4567", "token" => "321h1l2jkh1jk42341", "no-https" => true, "shutdown-hook-path" => "", diff --git a/test/e2e/self-hosted/docker_compose_missing_host_files.rb b/test/e2e/self-hosted/docker_compose_missing_host_files.rb index d82d94b7..5969f00e 100644 --- a/test/e2e/self-hosted/docker_compose_missing_host_files.rb +++ b/test/e2e/self-hosted/docker_compose_missing_host_files.rb @@ -5,7 +5,7 @@ File.write("/tmp/agent/file2.txt", "Hello from file2.txt") $AGENT_CONFIG = { - "endpoint" => "hub:4567", + "endpoint" => "localhost:4567", "token" => "321h1l2jkh1jk42341", "no-https" => true, "shutdown-hook-path" => "", diff --git a/test/e2e_support/api_mode.rb b/test/e2e_support/api_mode.rb index 287c5dff..20242b3e 100644 --- a/test/e2e_support/api_mode.rb +++ b/test/e2e_support/api_mode.rb @@ -11,7 +11,7 @@ def boot_up_agent system "docker stop $(docker ps -q)" system "docker rm $(docker ps -qa)" system "docker build -t agent -f Dockerfile.test ." - system "docker run --privileged --device /dev/ptmx -v /tmp/agent-temp-directory/:/tmp/agent-temp-directory -v /var/run/docker.sock:/var/run/docker.sock -p #{$AGENT_PORT_IN_TESTS}:8000 -p #{$AGENT_SSH_PORT_IN_TESTS}:22 --name agent -tdi agent bash -c \"service ssh restart && nohup ./agent serve --auth-token-secret 'TzRVcspTmxhM9fUkdi1T/0kVXNETCi8UdZ8dLM8va4E' & sleep infinity\"" + system "docker run --privileged --device /dev/ptmx --network=host -v /tmp/agent-temp-directory/:/tmp/agent-temp-directory -v /var/run/docker.sock:/var/run/docker.sock --name agent -tdi agent bash -c \"service ssh restart && nohup ./agent serve --port 30000 --auth-token-secret 'TzRVcspTmxhM9fUkdi1T/0kVXNETCi8UdZ8dLM8va4E' & sleep infinity\"" pingable = nil until pingable diff --git a/test/e2e_support/docker-compose-listen.yml b/test/e2e_support/docker-compose-listen.yml index 5f39f3a1..d8e1efeb 100644 --- a/test/e2e_support/docker-compose-listen.yml +++ b/test/e2e_support/docker-compose-listen.yml @@ -1,35 +1,23 @@ version: '3.0' - services: agent: + network_mode: host build: context: ../.. dockerfile: Dockerfile.test - command: 'bash -c "service ssh restart && SEMAPHORE_AGENT_LOG_LEVEL=DEBUG ./agent start --config-file /tmp/agent/config.yaml"' - - ports: - - "30000:8000" - - "2222:22" - - links: - - hub:hub - devices: - /dev/ptmx - volumes: - /tmp/agent:/tmp/agent - /tmp/agent-temp-directory:/tmp/agent-temp-directory - /var/run/docker.sock:/var/run/docker.sock - + - ~/.kube:/root/.kube + - ~/.minikube:/root/.minikube hub: + network_mode: host build: context: ../hub_reference dockerfile: Dockerfile - - ports: - - "4567:4567" - volumes: - ../hub_reference:/app From 1b1d0eeea447f15756ec5db9b6c14b10ef57f74a Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Wed, 1 Feb 2023 17:18:44 -0300 Subject: [PATCH 049/130] feat: handle SIGTERM gracefully (#180) --- main.go | 2 + pkg/config/config.go | 2 + pkg/listener/job_processor.go | 15 ++- pkg/listener/listener.go | 23 ++-- pkg/listener/listener_test.go | 148 +++++++++++++++++++++++++ pkg/listener/selfhostedapi/register.go | 17 +-- pkg/listener/selfhostedapi/sync.go | 8 +- pkg/listener/shutdown_reason.go | 4 +- test/support/hub.go | 16 +++ 9 files changed, 210 insertions(+), 25 deletions(-) diff --git a/main.go b/main.go index 56e40685..37c235e6 100644 --- a/main.go +++ b/main.go @@ -114,6 +114,7 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { _ = pflag.String(config.PreJobHookPath, "", "Pre-job hook path") _ = pflag.Bool(config.DisconnectAfterJob, false, "Disconnect after job") _ = pflag.Int(config.DisconnectAfterIdleTimeout, 0, "Disconnect after idle timeout, in seconds") + _ = pflag.Int(config.InterruptionGracePeriod, 0, "The grace period, in seconds, to wait after receiving an interrupt signal") _ = pflag.StringSlice(config.EnvVars, []string{}, "Export environment variables in jobs") _ = pflag.StringSlice(config.Files, []string{}, "Inject files into container, when using docker compose executor") _ = pflag.Bool(config.FailOnMissingFiles, false, "Fail job if files specified using --files are missing") @@ -188,6 +189,7 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { PreJobHookPath: viper.GetString(config.PreJobHookPath), DisconnectAfterJob: viper.GetBool(config.DisconnectAfterJob), DisconnectAfterIdleSeconds: viper.GetInt(config.DisconnectAfterIdleTimeout), + InterruptionGracePeriod: viper.GetInt(config.InterruptionGracePeriod), EnvVars: hostEnvVars, FileInjections: fileInjections, FailOnMissingFiles: viper.GetBool(config.FailOnMissingFiles), diff --git a/pkg/config/config.go b/pkg/config/config.go index 0150b679..b949cb61 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -17,6 +17,7 @@ const ( FailOnMissingFiles = "fail-on-missing-files" UploadJobLogs = "upload-job-logs" FailOnPreJobHookError = "fail-on-pre-job-hook-error" + InterruptionGracePeriod = "interruption-grace-period" KubernetesExecutor = "kubernetes-executor" KubernetesDefaultImage = "kubernetes-default-image" KubernetesImagePullPolicy = "kubernetes-image-pull-policy" @@ -68,6 +69,7 @@ var ValidConfigKeys = []string{ FailOnMissingFiles, UploadJobLogs, FailOnPreJobHookError, + InterruptionGracePeriod, KubernetesExecutor, KubernetesDefaultImage, KubernetesImagePullPolicy, diff --git a/pkg/listener/job_processor.go b/pkg/listener/job_processor.go index d2753384..84142b53 100644 --- a/pkg/listener/job_processor.go +++ b/pkg/listener/job_processor.go @@ -62,6 +62,7 @@ type JobProcessor struct { CurrentJob *jobs.Job LastSyncErrorAt *time.Time LastSuccessfulSync time.Time + InterruptedAt int64 ShutdownReason ShutdownReason mutex sync.Mutex @@ -104,9 +105,10 @@ func (p *JobProcessor) SyncLoop() { func (p *JobProcessor) Sync() { request := &selfhostedapi.SyncRequest{ - State: p.State, - JobID: p.CurrentJobID, - JobResult: p.CurrentJobResult, + State: p.State, + JobID: p.CurrentJobID, + JobResult: p.CurrentJobResult, + InterruptedAt: p.InterruptedAt, } response, err := p.APIClient.Sync(request) @@ -258,8 +260,11 @@ func (p *JobProcessor) SetupInterruptHandler() { signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c - log.Info("Ctrl+C pressed in Terminal") - p.Shutdown(ShutdownReasonInterrupted, 0) + log.Info("Termination signal received") + + // When we receive an interruption signal + // we tell the API about it, and let it tell the agent when to shut down. + p.InterruptedAt = time.Now().Unix() }() } diff --git a/pkg/listener/listener.go b/pkg/listener/listener.go index 3414a1b0..497d39eb 100644 --- a/pkg/listener/listener.go +++ b/pkg/listener/listener.go @@ -30,6 +30,7 @@ type Config struct { PreJobHookPath string DisconnectAfterJob bool DisconnectAfterIdleSeconds int + InterruptionGracePeriod int EnvVars []config.HostEnvVar FileInjections []config.FileInjection FailOnMissingFiles bool @@ -75,6 +76,11 @@ func (l *Listener) Stop() { l.JobProcessor.Shutdown(ShutdownReasonRequested, 0) } +// only used during tests +func (l *Listener) Interrupt() { + l.JobProcessor.InterruptedAt = time.Now().Unix() +} + func (l *Listener) DisplayHelloMessage() { fmt.Println(" ") fmt.Println(" 00000000000 ") @@ -93,14 +99,15 @@ func (l *Listener) DisplayHelloMessage() { func (l *Listener) Register(name string) error { req := &selfhostedapi.RegisterRequest{ - Version: l.Config.AgentVersion, - Name: name, - PID: os.Getpid(), - OS: osinfo.Name(), - Arch: osinfo.Arch(), - Hostname: osinfo.Hostname(), - SingleJob: l.Config.DisconnectAfterJob, - IdleTimeout: l.Config.DisconnectAfterIdleSeconds, + Version: l.Config.AgentVersion, + Name: name, + PID: os.Getpid(), + OS: osinfo.Name(), + Arch: osinfo.Arch(), + Hostname: osinfo.Hostname(), + SingleJob: l.Config.DisconnectAfterJob, + IdleTimeout: l.Config.DisconnectAfterIdleSeconds, + InterruptionGracePeriod: l.Config.InterruptionGracePeriod, } err := retry.RetryWithConstantWait(retry.RetryOptions{ diff --git a/pkg/listener/listener_test.go b/pkg/listener/listener_test.go index 8314ee20..8ea08f96 100644 --- a/pkg/listener/listener_test.go +++ b/pkg/listener/listener_test.go @@ -322,6 +322,154 @@ func Test__ShutdownAfterIdleTimeout(t *testing.T) { loghubMockServer.Close() } +func Test__ShutdownAfterInterruption(t *testing.T) { + testsupport.SetupTestLogs() + + loghubMockServer := testsupport.NewLoghubMockServer() + loghubMockServer.Init() + + hubMockServer := testsupport.NewHubMockServer() + hubMockServer.Init() + hubMockServer.UseLogsURL(loghubMockServer.URL()) + + config := Config{ + AgentName: fmt.Sprintf("agent-name-%d", rand.Intn(10000000)), + ExitOnShutdown: false, + Endpoint: hubMockServer.Host(), + Token: "token", + RegisterRetryLimit: 5, + Scheme: "http", + EnvVars: []config.HostEnvVar{}, + FileInjections: []config.FileInjection{}, + AgentVersion: "0.0.7", + } + + listener, err := Start(http.DefaultClient, config) + assert.Nil(t, err) + + listener.Interrupt() + assert.Nil(t, hubMockServer.WaitUntilDisconnected(15, 2*time.Second)) + assert.Equal(t, listener.JobProcessor.ShutdownReason, ShutdownReasonInterrupted) + + hubMockServer.Close() + loghubMockServer.Close() +} + +func Test__ShutdownAfterInterruptionNoGracePeriod(t *testing.T) { + testsupport.SetupTestLogs() + + loghubMockServer := testsupport.NewLoghubMockServer() + loghubMockServer.Init() + + hubMockServer := testsupport.NewHubMockServer() + hubMockServer.Init() + hubMockServer.UseLogsURL(loghubMockServer.URL()) + + config := Config{ + AgentName: fmt.Sprintf("agent-name-%d", rand.Intn(10000000)), + ExitOnShutdown: false, + DisconnectAfterJob: true, + Endpoint: hubMockServer.Host(), + Token: "token", + RegisterRetryLimit: 5, + Scheme: "http", + EnvVars: []config.HostEnvVar{}, + FileInjections: []config.FileInjection{}, + AgentVersion: "0.0.7", + } + + listener, err := Start(http.DefaultClient, config) + assert.Nil(t, err) + + // assigns job that sleeps for 60s + hubMockServer.AssignJob(&api.JobRequest{ + ID: "Test__ShutdownAfterJobFinished", + Commands: []api.Command{ + {Directive: "sleep 60"}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + URL: loghubMockServer.URL(), + Token: "doesnotmatter", + }, + }) + + // wait until job is running + assert.Nil(t, hubMockServer.WaitUntilRunningJob(10, time.Second)) + + // send interrupt signal and assert agents disconnected + // with interrupted reason and job is stopped immediately. + listener.Interrupt() + assert.Nil(t, hubMockServer.WaitUntilDisconnected(30, 2*time.Second)) + assert.Equal(t, listener.JobProcessor.ShutdownReason, ShutdownReasonInterrupted) + assert.Equal(t, selfhostedapi.JobResult(selfhostedapi.JobResultStopped), hubMockServer.GetLastJobResult()) + + hubMockServer.Close() + loghubMockServer.Close() +} + +func Test__ShutdownAfterInterruptionWithGracePeriod(t *testing.T) { + testsupport.SetupTestLogs() + + loghubMockServer := testsupport.NewLoghubMockServer() + loghubMockServer.Init() + + hubMockServer := testsupport.NewHubMockServer() + hubMockServer.Init() + hubMockServer.UseLogsURL(loghubMockServer.URL()) + + config := Config{ + AgentName: fmt.Sprintf("agent-name-%d", rand.Intn(10000000)), + ExitOnShutdown: false, + DisconnectAfterJob: true, + Endpoint: hubMockServer.Host(), + Token: "token", + RegisterRetryLimit: 5, + Scheme: "http", + EnvVars: []config.HostEnvVar{}, + FileInjections: []config.FileInjection{}, + AgentVersion: "0.0.7", + InterruptionGracePeriod: 30, + } + + listener, err := Start(http.DefaultClient, config) + assert.Nil(t, err) + + // assigns job that sleeps for 10s + hubMockServer.AssignJob(&api.JobRequest{ + ID: "Test__ShutdownAfterJobFinished", + Commands: []api.Command{ + {Directive: "sleep 15"}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + URL: loghubMockServer.URL(), + Token: "doesnotmatter", + }, + }) + + // wait until job is running + assert.Nil(t, hubMockServer.WaitUntilRunningJob(10, time.Second)) + + // send interrupt signal and assert agents disconnected + // with interrupted reason and job finishes properly. + listener.Interrupt() + assert.Nil(t, hubMockServer.WaitUntilDisconnected(30, time.Second)) + assert.Equal(t, listener.JobProcessor.ShutdownReason, ShutdownReasonInterrupted) + assert.Equal(t, selfhostedapi.JobResult(selfhostedapi.JobResultPassed), hubMockServer.GetLastJobResult()) + + hubMockServer.Close() + loghubMockServer.Close() +} + func Test__ShutdownFromUpstreamWhileWaiting(t *testing.T) { testsupport.SetupTestLogs() diff --git a/pkg/listener/selfhostedapi/register.go b/pkg/listener/selfhostedapi/register.go index 683e0d91..7d4cc137 100644 --- a/pkg/listener/selfhostedapi/register.go +++ b/pkg/listener/selfhostedapi/register.go @@ -12,14 +12,15 @@ import ( ) type RegisterRequest struct { - Name string `json:"name"` - Version string `json:"version"` - PID int `json:"pid"` - OS string `json:"os"` - Arch string `json:"arch"` - Hostname string `json:"hostname"` - SingleJob bool `json:"single_job"` - IdleTimeout int `json:"idle_timeout"` + Name string `json:"name"` + Version string `json:"version"` + PID int `json:"pid"` + OS string `json:"os"` + Arch string `json:"arch"` + Hostname string `json:"hostname"` + SingleJob bool `json:"single_job"` + IdleTimeout int `json:"idle_timeout"` + InterruptionGracePeriod int `json:"interruption_grace_period"` } type RegisterResponse struct { diff --git a/pkg/listener/selfhostedapi/sync.go b/pkg/listener/selfhostedapi/sync.go index aa707d0e..097fb36a 100644 --- a/pkg/listener/selfhostedapi/sync.go +++ b/pkg/listener/selfhostedapi/sync.go @@ -34,11 +34,13 @@ const JobResultPassed = "passed" const ShutdownReasonIdle = "idle" const ShutdownReasonJobFinished = "job-finished" const ShutdownReasonRequested = "requested" +const ShutdownReasonInterrupted = "interrupted" type SyncRequest struct { - State AgentState `json:"state"` - JobID string `json:"job_id"` - JobResult JobResult `json:"job_result"` + State AgentState `json:"state"` + JobID string `json:"job_id"` + JobResult JobResult `json:"job_result"` + InterruptedAt int64 `json:"interrupted_at"` } type SyncResponse struct { diff --git a/pkg/listener/shutdown_reason.go b/pkg/listener/shutdown_reason.go index 440d9b8e..29441279 100644 --- a/pkg/listener/shutdown_reason.go +++ b/pkg/listener/shutdown_reason.go @@ -11,12 +11,12 @@ const ( ShutdownReasonIdle ShutdownReason = iota ShutdownReasonJobFinished ShutdownReasonRequested + ShutdownReasonInterrupted ShutdownReasonUnknown // When the agent shuts down due to these reasons, // the agent decides to do so. ShutdownReasonUnableToSync - ShutdownReasonInterrupted ) func ShutdownReasonFromAPI(reasonFromAPI selfhostedapi.ShutdownReason) ShutdownReason { @@ -27,6 +27,8 @@ func ShutdownReasonFromAPI(reasonFromAPI selfhostedapi.ShutdownReason) ShutdownR return ShutdownReasonJobFinished case selfhostedapi.ShutdownReasonRequested: return ShutdownReasonRequested + case selfhostedapi.ShutdownReasonInterrupted: + return ShutdownReasonInterrupted } return ShutdownReasonUnknown diff --git a/test/support/hub.go b/test/support/hub.go index f3cdb7eb..93eba701 100644 --- a/test/support/hub.go +++ b/test/support/hub.go @@ -145,6 +145,11 @@ func (m *HubMockServer) handleSyncRequest(w http.ResponseWriter, r *http.Request switch request.State { case selfhostedapi.AgentStateWaitingForJobs: + if request.InterruptedAt > 0 { + syncResponse.Action = selfhostedapi.AgentActionShutdown + syncResponse.ShutdownReason = selfhostedapi.ShutdownReasonInterrupted + } + if m.ShouldShutdown { syncResponse.Action = selfhostedapi.AgentActionShutdown syncResponse.ShutdownReason = selfhostedapi.ShutdownReasonRequested @@ -166,6 +171,14 @@ func (m *HubMockServer) handleSyncRequest(w http.ResponseWriter, r *http.Request case selfhostedapi.AgentStateRunningJob: m.RunningJob = true + if request.InterruptedAt > 0 { + gracePeriodEnd := time.Unix(request.InterruptedAt, 0).Add(time.Duration(m.RegisterRequest.InterruptionGracePeriod) * time.Second) + if time.Now().After(gracePeriodEnd) { + syncResponse.Action = selfhostedapi.AgentActionStopJob + syncResponse.JobID = m.JobRequest.ID + } + } + if m.ShouldShutdown { syncResponse.Action = selfhostedapi.AgentActionStopJob syncResponse.JobID = m.JobRequest.ID @@ -179,6 +192,9 @@ func (m *HubMockServer) handleSyncRequest(w http.ResponseWriter, r *http.Request if m.ShouldShutdown { syncResponse.Action = selfhostedapi.AgentActionShutdown syncResponse.ShutdownReason = selfhostedapi.ShutdownReasonRequested + } else if request.InterruptedAt > 0 { + syncResponse.Action = selfhostedapi.AgentActionShutdown + syncResponse.ShutdownReason = selfhostedapi.ShutdownReasonInterrupted } else if m.RegisterRequest.SingleJob { syncResponse.Action = selfhostedapi.AgentActionShutdown syncResponse.ShutdownReason = selfhostedapi.ShutdownReasonJobFinished From b5d8bd5adedc912f5ed38169a79647035ab8640d Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Fri, 3 Feb 2023 17:03:24 -0300 Subject: [PATCH 050/130] feat: k8s executor private image support (#182) --- .semaphore/semaphore.yml | 10 + main.go | 2 + pkg/api/job_request.go | 48 ++++- pkg/api/job_request_test.go | 64 ++++++ pkg/aws/aws.go | 70 +++++-- pkg/config/config.go | 4 +- pkg/docker/docker.go | 109 ++++++++++ pkg/docker/docker_test.go | 196 ++++++++++++++++++ pkg/executors/docker_compose_executor.go | 22 +- pkg/executors/kubernetes_executor.go | 46 ++-- pkg/jobs/job.go | 2 + pkg/kubernetes/client.go | 69 +++++- pkg/kubernetes/client_test.go | 144 ++++++++++++- pkg/listener/job_processor.go | 3 + pkg/listener/listener.go | 1 + .../private_image_ecr_no_account_id.rb | 83 ++++++++ .../private_image_ecr_with_account_id.rb | 86 ++++++++ test/e2e/kubernetes/private_image_gcr.rb | 85 ++++++++ test/e2e/kubernetes/private_image_generic.rb | 84 ++++++++ 19 files changed, 1069 insertions(+), 59 deletions(-) create mode 100644 pkg/docker/docker.go create mode 100644 pkg/docker/docker_test.go create mode 100644 test/e2e/kubernetes/private_image_ecr_no_account_id.rb create mode 100644 test/e2e/kubernetes/private_image_ecr_with_account_id.rb create mode 100644 test/e2e/kubernetes/private_image_gcr.rb create mode 100644 test/e2e/kubernetes/private_image_generic.rb diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 0195ab12..6f8a8776 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -211,11 +211,17 @@ blocks: - name: "Kubernetes Executor E2E" dependencies: [] task: + secrets: + - name: aws-ecr-agent-e2e-secret + - name: gcr-test-secret + - name: docker-registry-test-secret env_vars: - name: GO111MODULE value: "on" - name: TEST_MODE value: "listen" + - name: AWS_REGION + value: "us-east-1" prologue: commands: @@ -248,6 +254,10 @@ blocks: - docker_compose__epilogue - docker_compose__file-injection - docker_compose__multiple-containers + - private_image_gcr + - private_image_ecr_no_account_id + - private_image_ecr_with_account_id + - private_image_generic promotions: - name: Release diff --git a/main.go b/main.go index 37c235e6..87e7525f 100644 --- a/main.go +++ b/main.go @@ -123,6 +123,7 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { _ = pflag.Bool(config.KubernetesExecutor, false, "Use Kubernetes executor") _ = pflag.String(config.KubernetesDefaultImage, "", "Default image used for jobs that do not specify images, when using kubernetes executor") _ = pflag.String(config.KubernetesImagePullPolicy, config.ImagePullPolicyNever, "Image pull policy to use for Kubernetes executor. Default is never.") + _ = pflag.StringSlice(config.KubernetesImagePullSecrets, []string{}, "Kubernetes secrets to use to pull images.") _ = pflag.Int( config.KubernetesPodStartTimeout, config.DefaultKubernetesPodStartTimeout, @@ -200,6 +201,7 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { KubernetesExecutor: viper.GetBool(config.KubernetesExecutor), KubernetesDefaultImage: viper.GetString(config.KubernetesDefaultImage), KubernetesImagePullPolicy: viper.GetString(config.KubernetesImagePullPolicy), + KubernetesImagePullSecrets: viper.GetStringSlice(config.KubernetesImagePullSecrets), KubernetesPodStartTimeoutSeconds: viper.GetInt(config.KubernetesPodStartTimeout), } diff --git a/pkg/api/job_request.go b/pkg/api/job_request.go index de3d6907..e2885041 100644 --- a/pkg/api/job_request.go +++ b/pkg/api/job_request.go @@ -152,6 +152,52 @@ const ImagePullCredentialsStrategyGenericDocker = "GenericDocker" const ImagePullCredentialsStrategyECR = "AWS_ECR" const ImagePullCredentialsStrategyGCR = "GCR" +func (c *ImagePullCredentials) ToCmdEnvVars() ([]string, error) { + envs := []string{} + + for _, env := range c.EnvVars { + name := env.Name + value, err := env.Decode() + if err != nil { + return envs, fmt.Errorf("error decoding '%s': %v", env.Name, err) + } + + envs = append(envs, fmt.Sprintf("%s=%s", name, string(value))) + } + + return envs, nil +} + +func (c *ImagePullCredentials) FindFile(path string) (string, error) { + for _, f := range c.Files { + if f.Path == path { + v, err := f.Decode() + if err != nil { + return "", fmt.Errorf("error decoding '%s': %v", path, err) + } + + return string(v), nil + } + } + + return "", fmt.Errorf("no file '%s' found", path) +} + +func (c *ImagePullCredentials) FindEnvVar(varName string) (string, error) { + for _, envVar := range c.EnvVars { + if envVar.Name == varName { + v, err := envVar.Decode() + if err != nil { + return "", fmt.Errorf("error decoding '%s': %v", varName, err) + } + + return string(v), nil + } + } + + return "", fmt.Errorf("no env var '%s' found", varName) +} + func (c *ImagePullCredentials) Strategy() (string, error) { for _, e := range c.EnvVars { if e.Name == "DOCKER_CREDENTIAL_TYPE" { @@ -171,7 +217,7 @@ func (c *ImagePullCredentials) Strategy() (string, error) { case ImagePullCredentialsStrategyGCR: return ImagePullCredentialsStrategyGCR, nil default: - return "", fmt.Errorf("Unknown DOCKER_CREDENTIAL_TYPE: '%s'", v) + return "", fmt.Errorf("unknown DOCKER_CREDENTIAL_TYPE: '%s'", v) } } } diff --git a/pkg/api/job_request_test.go b/pkg/api/job_request_test.go index 06bf4cd4..f4760ed7 100644 --- a/pkg/api/job_request_test.go +++ b/pkg/api/job_request_test.go @@ -1,6 +1,7 @@ package api import ( + "encoding/base64" "path/filepath" "runtime" "testing" @@ -57,3 +58,66 @@ func Test__JobRequest(t *testing.T) { } }) } + +func Test__ImagePullCredentials(t *testing.T) { + t.Run("ToCmdEnvVars()", func(t *testing.T) { + // returns slice of key-value env vars + c := ImagePullCredentials{EnvVars: []EnvVar{ + {Name: "FOO", Value: base64.StdEncoding.EncodeToString([]byte("FOO_VALUE"))}, + {Name: "BAR", Value: base64.StdEncoding.EncodeToString([]byte("BAR_VALUE"))}, + }} + + envs, err := c.ToCmdEnvVars() + assert.NoError(t, err) + assert.Equal(t, envs, []string{"FOO=FOO_VALUE", "BAR=BAR_VALUE"}) + + // returns error + c = ImagePullCredentials{EnvVars: []EnvVar{ + {Name: "FOO", Value: base64.StdEncoding.EncodeToString([]byte("FOO_VALUE"))}, + {Name: "BAR", Value: "NOT_PROPERLY_ENCODED"}, + }} + + _, err = c.ToCmdEnvVars() + assert.ErrorContains(t, err, "error decoding 'BAR'") + }) + + t.Run("FindEnvVar()", func(t *testing.T) { + c := ImagePullCredentials{EnvVars: []EnvVar{ + {Name: "FOO", Value: base64.StdEncoding.EncodeToString([]byte("FOO_VALUE"))}, + {Name: "BAR", Value: "not-encoded-value"}, + }} + + // env var that exists returns no error + v, err := c.FindEnvVar("FOO") + assert.NoError(t, err) + assert.Equal(t, "FOO_VALUE", v) + + // env var that exists, but is not properly encoded returns error + _, err = c.FindEnvVar("BAR") + assert.ErrorContains(t, err, "error decoding 'BAR'") + + // env var that does not exist returns error + _, err = c.FindEnvVar("DOES_NOT_EXIST") + assert.ErrorContains(t, err, "no env var 'DOES_NOT_EXIST' found") + }) + + t.Run("FindFile()", func(t *testing.T) { + c := ImagePullCredentials{Files: []File{ + {Path: "a/b/c", Content: base64.StdEncoding.EncodeToString([]byte("VALUE_1"))}, + {Path: "d/e/f", Content: "not-encoded-value"}, + }} + + // file that exists returns no error + v, err := c.FindFile("a/b/c") + assert.NoError(t, err) + assert.Equal(t, "VALUE_1", v) + + // file that exists, but is not properly encoded returns error + _, err = c.FindFile("d/e/f") + assert.ErrorContains(t, err, "error decoding 'd/e/f'") + + // file that does not exist returns error + _, err = c.FindFile("does/not/exist") + assert.ErrorContains(t, err, "no file 'does/not/exist' found") + }) +} diff --git a/pkg/aws/aws.go b/pkg/aws/aws.go index 284a76da..19b61b14 100644 --- a/pkg/aws/aws.go +++ b/pkg/aws/aws.go @@ -6,10 +6,41 @@ import ( "strings" versions "github.com/hashicorp/go-version" + "github.com/semaphoreci/agent/pkg/api" log "github.com/sirupsen/logrus" ) -func GetECRLoginCmd(envs []string) (string, error) { +func GetECRServerURL(credentials api.ImagePullCredentials) (string, error) { + region, err := credentials.FindEnvVar("AWS_REGION") + if err != nil { + return "", err + } + + accountID, err := GetAccountID(credentials) + if err != nil { + return "", err + } + + return fmt.Sprintf("%s.dkr.ecr.%s.amazonaws.com", accountID, region), nil +} + +func GetECRLoginPassword(credentials api.ImagePullCredentials) (string, error) { + envs, err := credentials.ToCmdEnvVars() + if err != nil { + return "", err + } + + cmd := exec.Command("bash", "-c", "aws ecr get-login-password --region $AWS_REGION") + cmd.Env = envs + output, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("error executing aws ecr get-login-password: Output: %s - Error: %v", string(output), err) + } + + return strings.TrimSuffix(string(output), "\n"), nil +} + +func GetECRLoginCmd(credentials api.ImagePullCredentials) (string, error) { awsV2, _ := versions.NewVersion("2.0.0") awsCLIVersion, err := findAWSCLIVersion() if err != nil { @@ -17,12 +48,9 @@ func GetECRLoginCmd(envs []string) (string, error) { } if awsCLIVersion.GreaterThanOrEqual(awsV2) { - accountID := getAccountIDFromVars(envs) - if accountID == "" { - accountID, err = getAccountIDFromSTS(envs) - if err != nil { - return "", err - } + accountID, err := GetAccountID(credentials) + if err != nil { + return "", err } /* @@ -45,8 +73,8 @@ func GetECRLoginCmd(envs []string) (string, error) { * This is to make sure we execute the output of that command as well. * See: https://docs.aws.amazon.com/cli/latest/reference/ecr/get-login.html */ - accountID := getAccountIDFromVars(envs) - if accountID == "" { + accountID, err := credentials.FindEnvVar("AWS_ACCOUNT_ID") + if err != nil { return `$(aws ecr get-login --no-include-email --region $AWS_REGION)`, nil } @@ -57,18 +85,26 @@ func GetECRLoginCmd(envs []string) (string, error) { return fmt.Sprintf(`$(aws ecr get-login --no-include-email --region $AWS_REGION --registry-ids %s)`, accountID), nil } -func getAccountIDFromVars(envs []string) string { - for _, envVar := range envs { - parts := strings.Split(envVar, "=") - if parts[0] == "AWS_ACCOUNT_ID" { - return parts[1] - } +func GetAccountID(credentials api.ImagePullCredentials) (string, error) { + accountID, err := credentials.FindEnvVar("AWS_ACCOUNT_ID") + if err == nil { + return accountID, nil } - return "" + accountID, err = getAccountIDFromSTS(credentials) + if err != nil { + return "", err + } + + return accountID, nil } -func getAccountIDFromSTS(envs []string) (string, error) { +func getAccountIDFromSTS(credentials api.ImagePullCredentials) (string, error) { + envs, err := credentials.ToCmdEnvVars() + if err != nil { + return "", err + } + cmd := exec.Command("bash", "-c", "aws sts get-caller-identity --query Account --output text") cmd.Env = envs diff --git a/pkg/config/config.go b/pkg/config/config.go index b949cb61..45f9ee63 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -21,10 +21,11 @@ const ( KubernetesExecutor = "kubernetes-executor" KubernetesDefaultImage = "kubernetes-default-image" KubernetesImagePullPolicy = "kubernetes-image-pull-policy" + KubernetesImagePullSecrets = "kubernetes-image-pull-secrets" KubernetesPodStartTimeout = "kubernetes-pod-start-timeout" ) -const DefaultKubernetesPodStartTimeout = 60 +const DefaultKubernetesPodStartTimeout = 300 type ImagePullPolicy string @@ -73,6 +74,7 @@ var ValidConfigKeys = []string{ KubernetesExecutor, KubernetesDefaultImage, KubernetesImagePullPolicy, + KubernetesImagePullSecrets, KubernetesPodStartTimeout, } diff --git a/pkg/docker/docker.go b/pkg/docker/docker.go new file mode 100644 index 00000000..1a0482ad --- /dev/null +++ b/pkg/docker/docker.go @@ -0,0 +1,109 @@ +package docker + +import ( + "encoding/base64" + "fmt" + + "github.com/semaphoreci/agent/pkg/api" + "github.com/semaphoreci/agent/pkg/aws" +) + +type DockerConfig struct { + Auths map[string]DockerConfigAuthEntry `json:"auths" datapolicy:"token"` +} + +type DockerConfigAuthEntry struct { + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty" datapolicy:"password"` + Auth string `json:"auth,omitempty" datapolicy:"token"` +} + +func NewDockerConfig(credentials []api.ImagePullCredentials) (*DockerConfig, error) { + if len(credentials) == 0 { + return nil, fmt.Errorf("no credentials") + } + + dockerConfig := DockerConfig{Auths: map[string]DockerConfigAuthEntry{}} + + for _, credential := range credentials { + strategy, err := credential.Strategy() + if err != nil { + return nil, err + } + + u, err := configUsername(strategy, credential) + if err != nil { + return nil, err + } + + p, err := configPassword(strategy, credential) + if err != nil { + return nil, err + } + + server, err := configServerURL(strategy, credential) + if err != nil { + return nil, err + } + + e := DockerConfigAuthEntry{ + Username: u, + Password: p, + Auth: base64.StdEncoding.EncodeToString([]byte(u + ":" + p)), + } + + dockerConfig.Auths[server] = e + } + + return &dockerConfig, nil +} + +func configUsername(strategy string, credentials api.ImagePullCredentials) (string, error) { + switch strategy { + case api.ImagePullCredentialsStrategyDockerHub: + return credentials.FindEnvVar("DOCKERHUB_USERNAME") + case api.ImagePullCredentialsStrategyGenericDocker: + return credentials.FindEnvVar("DOCKER_USERNAME") + case api.ImagePullCredentialsStrategyECR: + return "AWS", nil + case api.ImagePullCredentialsStrategyGCR: + return "_json_key", nil + default: + return "", fmt.Errorf("%s not supported", strategy) + } +} + +func configPassword(strategy string, credentials api.ImagePullCredentials) (string, error) { + switch strategy { + case api.ImagePullCredentialsStrategyDockerHub: + return credentials.FindEnvVar("DOCKERHUB_PASSWORD") + case api.ImagePullCredentialsStrategyGenericDocker: + return credentials.FindEnvVar("DOCKER_PASSWORD") + case api.ImagePullCredentialsStrategyECR: + return aws.GetECRLoginPassword(credentials) + case api.ImagePullCredentialsStrategyGCR: + fileContent, err := credentials.FindFile("/tmp/gcr/keyfile.json") + if err != nil { + return "", err + } + + return fileContent, nil + default: + return "", fmt.Errorf("%s not supported", strategy) + } +} + +func configServerURL(strategy string, credentials api.ImagePullCredentials) (string, error) { + switch strategy { + case api.ImagePullCredentialsStrategyDockerHub: + return "docker.io", nil + case api.ImagePullCredentialsStrategyGenericDocker: + return credentials.FindEnvVar("DOCKER_URL") + case api.ImagePullCredentialsStrategyGCR: + return credentials.FindEnvVar("GCR_HOSTNAME") + case api.ImagePullCredentialsStrategyECR: + return aws.GetECRServerURL(credentials) + default: + return "", fmt.Errorf("%s not supported", strategy) + } +} diff --git a/pkg/docker/docker_test.go b/pkg/docker/docker_test.go new file mode 100644 index 00000000..e60ae59f --- /dev/null +++ b/pkg/docker/docker_test.go @@ -0,0 +1,196 @@ +package docker + +import ( + "encoding/base64" + "testing" + + "github.com/semaphoreci/agent/pkg/api" + "github.com/stretchr/testify/assert" +) + +func Test__NewDockerConfig(t *testing.T) { + t.Run("no credentials", func(t *testing.T) { + _, err := NewDockerConfig([]api.ImagePullCredentials{}) + assert.ErrorContains(t, err, "no credentials") + }) + + t.Run("no strategy", func(t *testing.T) { + _, err := NewDockerConfig([]api.ImagePullCredentials{{EnvVars: []api.EnvVar{}}}) + assert.ErrorContains(t, err, "DOCKER_CREDENTIAL_TYPE not set") + }) + + t.Run("unsupported strategy", func(t *testing.T) { + _, err := NewDockerConfig([]api.ImagePullCredentials{ + { + EnvVars: []api.EnvVar{ + {Name: "DOCKER_CREDENTIAL_TYPE", Value: base64.StdEncoding.EncodeToString([]byte("WHATEVER"))}, + }, + }, + }) + + assert.ErrorContains(t, err, "unknown DOCKER_CREDENTIAL_TYPE: 'WHATEVER'") + }) + + t.Run("dockerhub - no DOCKERHUB_USERNAME leads to error", func(t *testing.T) { + _, err := NewDockerConfig([]api.ImagePullCredentials{ + { + EnvVars: []api.EnvVar{ + {Name: "DOCKER_CREDENTIAL_TYPE", Value: base64.StdEncoding.EncodeToString([]byte(api.ImagePullCredentialsStrategyDockerHub))}, + }, + }, + }) + + assert.ErrorContains(t, err, "no env var 'DOCKERHUB_USERNAME' found") + }) + + t.Run("dockerhub - no DOCKERHUB_PASSWORD leads to error", func(t *testing.T) { + _, err := NewDockerConfig([]api.ImagePullCredentials{ + { + EnvVars: []api.EnvVar{ + {Name: "DOCKER_CREDENTIAL_TYPE", Value: base64.StdEncoding.EncodeToString([]byte(api.ImagePullCredentialsStrategyDockerHub))}, + {Name: "DOCKERHUB_USERNAME", Value: base64.StdEncoding.EncodeToString([]byte("dockerhubuser"))}, + }, + }, + }) + + assert.ErrorContains(t, err, "no env var 'DOCKERHUB_PASSWORD' found") + }) + + t.Run("dockerhub - returns config", func(t *testing.T) { + dockerCfg, err := NewDockerConfig([]api.ImagePullCredentials{ + { + EnvVars: []api.EnvVar{ + {Name: "DOCKER_CREDENTIAL_TYPE", Value: base64.StdEncoding.EncodeToString([]byte(api.ImagePullCredentialsStrategyDockerHub))}, + {Name: "DOCKERHUB_USERNAME", Value: base64.StdEncoding.EncodeToString([]byte("dockerhubuser"))}, + {Name: "DOCKERHUB_PASSWORD", Value: base64.StdEncoding.EncodeToString([]byte("dockerhubpass"))}, + }, + }, + }) + + assert.NoError(t, err) + assert.Equal(t, *dockerCfg, DockerConfig{ + Auths: map[string]DockerConfigAuthEntry{ + "docker.io": { + Username: "dockerhubuser", + Password: "dockerhubpass", + Auth: base64.StdEncoding.EncodeToString([]byte("dockerhubuser:dockerhubpass")), + }, + }, + }) + }) + + t.Run("generic docker - no DOCKER_USERNAME leads to error", func(t *testing.T) { + _, err := NewDockerConfig([]api.ImagePullCredentials{ + { + EnvVars: []api.EnvVar{ + {Name: "DOCKER_CREDENTIAL_TYPE", Value: base64.StdEncoding.EncodeToString([]byte(api.ImagePullCredentialsStrategyGenericDocker))}, + }, + }, + }) + + assert.ErrorContains(t, err, "no env var 'DOCKER_USERNAME' found") + }) + + t.Run("generic docker - no DOCKER_PASSWORD leads to error", func(t *testing.T) { + _, err := NewDockerConfig([]api.ImagePullCredentials{ + { + EnvVars: []api.EnvVar{ + {Name: "DOCKER_CREDENTIAL_TYPE", Value: base64.StdEncoding.EncodeToString([]byte(api.ImagePullCredentialsStrategyGenericDocker))}, + {Name: "DOCKER_USERNAME", Value: base64.StdEncoding.EncodeToString([]byte("dockeruser"))}, + }, + }, + }) + + assert.ErrorContains(t, err, "no env var 'DOCKER_PASSWORD' found") + }) + + t.Run("generic docker - no DOCKER_URL leads to error", func(t *testing.T) { + _, err := NewDockerConfig([]api.ImagePullCredentials{ + { + EnvVars: []api.EnvVar{ + {Name: "DOCKER_CREDENTIAL_TYPE", Value: base64.StdEncoding.EncodeToString([]byte(api.ImagePullCredentialsStrategyGenericDocker))}, + {Name: "DOCKER_USERNAME", Value: base64.StdEncoding.EncodeToString([]byte("dockeruser"))}, + {Name: "DOCKER_PASSWORD", Value: base64.StdEncoding.EncodeToString([]byte("dockerpass"))}, + }, + }, + }) + + assert.ErrorContains(t, err, "no env var 'DOCKER_URL' found") + }) + + t.Run("generic docker - returns config", func(t *testing.T) { + dockerCfg, err := NewDockerConfig([]api.ImagePullCredentials{ + { + EnvVars: []api.EnvVar{ + {Name: "DOCKER_CREDENTIAL_TYPE", Value: base64.StdEncoding.EncodeToString([]byte(api.ImagePullCredentialsStrategyGenericDocker))}, + {Name: "DOCKER_USERNAME", Value: base64.StdEncoding.EncodeToString([]byte("dockeruser"))}, + {Name: "DOCKER_PASSWORD", Value: base64.StdEncoding.EncodeToString([]byte("dockerpass"))}, + {Name: "DOCKER_URL", Value: base64.StdEncoding.EncodeToString([]byte("custom-registry.com"))}, + }, + }, + }) + + assert.NoError(t, err) + assert.Equal(t, *dockerCfg, DockerConfig{ + Auths: map[string]DockerConfigAuthEntry{ + "custom-registry.com": { + Username: "dockeruser", + Password: "dockerpass", + Auth: base64.StdEncoding.EncodeToString([]byte("dockeruser:dockerpass")), + }, + }, + }) + }) + + t.Run("GCR - no /tmp/gcr/keyfile.json leads to error", func(t *testing.T) { + _, err := NewDockerConfig([]api.ImagePullCredentials{ + { + EnvVars: []api.EnvVar{ + {Name: "DOCKER_CREDENTIAL_TYPE", Value: base64.StdEncoding.EncodeToString([]byte(api.ImagePullCredentialsStrategyGCR))}, + }, + }, + }) + + assert.ErrorContains(t, err, "no file '/tmp/gcr/keyfile.json' found") + }) + + t.Run("GCR - no GCR_HOSTNAME leads to error", func(t *testing.T) { + _, err := NewDockerConfig([]api.ImagePullCredentials{ + { + EnvVars: []api.EnvVar{ + {Name: "DOCKER_CREDENTIAL_TYPE", Value: base64.StdEncoding.EncodeToString([]byte(api.ImagePullCredentialsStrategyGCR))}, + }, + Files: []api.File{ + {Path: "/tmp/gcr/keyfile.json", Content: base64.StdEncoding.EncodeToString([]byte("aosidaoshd0a9hsd"))}, + }, + }, + }) + + assert.ErrorContains(t, err, "no env var 'GCR_HOSTNAME' found") + }) + + t.Run("GCR - returns config", func(t *testing.T) { + dockerCfg, err := NewDockerConfig([]api.ImagePullCredentials{ + { + EnvVars: []api.EnvVar{ + {Name: "DOCKER_CREDENTIAL_TYPE", Value: base64.StdEncoding.EncodeToString([]byte(api.ImagePullCredentialsStrategyGCR))}, + {Name: "GCR_HOSTNAME", Value: base64.StdEncoding.EncodeToString([]byte("gcr.io"))}, + }, + Files: []api.File{ + {Path: "/tmp/gcr/keyfile.json", Content: base64.StdEncoding.EncodeToString([]byte("aosidaoshd0a9hsd"))}, + }, + }, + }) + + assert.NoError(t, err) + assert.Equal(t, *dockerCfg, DockerConfig{ + Auths: map[string]DockerConfigAuthEntry{ + "gcr.io": { + Username: "_json_key", + Password: "aosidaoshd0a9hsd", + Auth: base64.StdEncoding.EncodeToString([]byte("_json_key:aosidaoshd0a9hsd")), + }, + }, + }) + }) +} diff --git a/pkg/executors/docker_compose_executor.go b/pkg/executors/docker_compose_executor.go index 8111a58d..96e03d2b 100644 --- a/pkg/executors/docker_compose_executor.go +++ b/pkg/executors/docker_compose_executor.go @@ -280,7 +280,7 @@ func (e *DockerComposeExecutor) injectImagePullSecrets() int { case api.ImagePullCredentialsStrategyDockerHub: exitCode = e.injectImagePullSecretsForDockerHub(c.EnvVars) case api.ImagePullCredentialsStrategyECR: - exitCode = e.injectImagePullSecretsForECR(c.EnvVars) + exitCode = e.injectImagePullSecretsForECR(c) case api.ImagePullCredentialsStrategyGenericDocker: exitCode = e.injectImagePullSecretsForGenericDocker(c.EnvVars) case api.ImagePullCredentialsStrategyGCR: @@ -376,24 +376,16 @@ func (e *DockerComposeExecutor) injectImagePullSecretsForGenericDocker(envVars [ return 0 } -func (e *DockerComposeExecutor) injectImagePullSecretsForECR(envVars []api.EnvVar) int { +func (e *DockerComposeExecutor) injectImagePullSecretsForECR(credentials api.ImagePullCredentials) int { e.Logger.LogCommandOutput("Setting up credentials for ECR\n") - envs := []string{} - - for _, env := range envVars { - name := env.Name - value, err := env.Decode() - - if err != nil { - e.Logger.LogCommandOutput(fmt.Sprintf("Failed to decode %s\n", name)) - return 1 - } - - envs = append(envs, fmt.Sprintf("%s=%s", name, string(value))) + envs, err := credentials.ToCmdEnvVars() + if err != nil { + e.Logger.LogCommandOutput(fmt.Sprintf("Error preparing environment variables: %v", err)) + return 1 } - loginCmd, err := aws.GetECRLoginCmd(envs) + loginCmd, err := aws.GetECRLoginCmd(credentials) if err != nil { e.Logger.LogCommandOutput(fmt.Sprintf("Failed to determine docker login command: %v\n", err)) return 1 diff --git a/pkg/executors/kubernetes_executor.go b/pkg/executors/kubernetes_executor.go index c00b506f..5c54784e 100644 --- a/pkg/executors/kubernetes_executor.go +++ b/pkg/executors/kubernetes_executor.go @@ -18,12 +18,13 @@ import ( ) type KubernetesExecutor struct { - k8sClient *kubernetes.KubernetesClient - jobRequest *api.JobRequest - podName string - secretName string - logger *eventlogger.Logger - Shell *shell.Shell + k8sClient *kubernetes.KubernetesClient + jobRequest *api.JobRequest + podName string + envSecretName string + imagePullSecret string + logger *eventlogger.Logger + Shell *shell.Shell // We need to keep track if the initial environment has already // been exposed or not, because ExportEnvVars() gets called twice. @@ -55,15 +56,26 @@ func NewKubernetesExecutor(jobRequest *api.JobRequest, logger *eventlogger.Logge func (e *KubernetesExecutor) Prepare() int { e.podName = e.randomPodName() - e.secretName = fmt.Sprintf("%s-secret", e.podName) + e.envSecretName = fmt.Sprintf("%s-secret", e.podName) - err := e.k8sClient.CreateSecret(e.secretName, e.jobRequest) + err := e.k8sClient.CreateSecret(e.envSecretName, e.jobRequest) if err != nil { - log.Errorf("Error creating secret '%s': %v", e.secretName, err) + log.Errorf("Error creating secret '%s': %v", e.envSecretName, err) return 1 } - err = e.k8sClient.CreatePod(e.podName, e.secretName, e.jobRequest) + // If image pull credentials are specified in the YAML, + // we create a temporary secret to store them and use it to pull the image. + if len(e.jobRequest.Compose.ImagePullCredentials) > 0 { + e.imagePullSecret = fmt.Sprintf("%s-image-pull-secret", e.podName) + err = e.k8sClient.CreateImagePullSecret(e.imagePullSecret, e.jobRequest.Compose.ImagePullCredentials) + if err != nil { + log.Errorf("Error creating image pull credentials '%s': %v", e.envSecretName, err) + return 1 + } + } + + err = e.k8sClient.CreatePod(e.podName, e.envSecretName, e.imagePullSecret, e.jobRequest) if err != nil { log.Errorf("Error creating pod: %v", err) return 1 @@ -346,9 +358,19 @@ func (e *KubernetesExecutor) removeK8sResources() { log.Errorf("Error deleting pod '%s': %v\n", e.podName, err) } - err = e.k8sClient.DeleteSecret(e.secretName) + err = e.k8sClient.DeleteSecret(e.envSecretName) if err != nil { - log.Errorf("Error deleting secret '%s': %v\n", e.secretName, err) + log.Errorf("Error deleting secret '%s': %v\n", e.envSecretName, err) + } + + // Not all jobs create this temporary secret, + // just the ones that send credentials to pull images + // in the job definition, so we only delete it if it was previously created. + if e.imagePullSecret != "" { + err = e.k8sClient.DeleteSecret(e.imagePullSecret) + if err != nil { + log.Errorf("Error deleting secret '%s': %v\n", e.imagePullSecret, err) + } } } diff --git a/pkg/jobs/job.go b/pkg/jobs/job.go index c3492332..bcfb870a 100644 --- a/pkg/jobs/job.go +++ b/pkg/jobs/job.go @@ -49,6 +49,7 @@ type JobOptions struct { UseKubernetesExecutor bool KubernetesDefaultImage string KubernetesImagePullPolicy string + KubernetesImagePullSecrets []string KubernetesPodStartTimeoutSeconds int UploadJobLogs string RefreshTokenFn func() (string, error) @@ -122,6 +123,7 @@ func CreateExecutor(request *api.JobRequest, logger *eventlogger.Logger, jobOpti Namespace: namespace, DefaultImage: jobOptions.KubernetesDefaultImage, ImagePullPolicy: jobOptions.KubernetesImagePullPolicy, + ImagePullSecrets: jobOptions.KubernetesImagePullSecrets, PodPollingAttempts: jobOptions.KubernetesPodStartTimeoutSeconds, PodPollingInterval: time.Second, }) diff --git a/pkg/kubernetes/client.go b/pkg/kubernetes/client.go index d3275b90..513b0135 100644 --- a/pkg/kubernetes/client.go +++ b/pkg/kubernetes/client.go @@ -3,6 +3,7 @@ package kubernetes import ( "context" "encoding/base64" + "encoding/json" "fmt" "io/ioutil" "os" @@ -11,6 +12,7 @@ import ( "github.com/semaphoreci/agent/pkg/api" "github.com/semaphoreci/agent/pkg/config" + "github.com/semaphoreci/agent/pkg/docker" "github.com/semaphoreci/agent/pkg/retry" "github.com/semaphoreci/agent/pkg/shell" corev1 "k8s.io/api/core/v1" @@ -24,6 +26,7 @@ type Config struct { Namespace string DefaultImage string ImagePullPolicy string + ImagePullSecrets []string PodPollingAttempts int PodPollingInterval time.Duration } @@ -166,8 +169,46 @@ func (c *KubernetesClient) CreateSecret(name string, jobRequest *api.JobRequest) return nil } -func (c *KubernetesClient) CreatePod(name string, envSecretName string, jobRequest *api.JobRequest) error { - pod, err := c.podSpecFromJobRequest(name, envSecretName, jobRequest) +func (c *KubernetesClient) CreateImagePullSecret(secretName string, credentials []api.ImagePullCredentials) error { + secret, err := c.buildImagePullSecret(secretName, credentials) + if err != nil { + return fmt.Errorf("error building image pull secret spec for '%s': %v", secretName, err) + } + + _, err = c.clientset.CoreV1(). + Secrets(c.config.Namespace). + Create(context.Background(), secret, v1.CreateOptions{}) + if err != nil { + return fmt.Errorf("error creating image pull secret '%s': %v", secretName, err) + } + + return nil +} + +func (c *KubernetesClient) buildImagePullSecret(secretName string, credentials []api.ImagePullCredentials) (*corev1.Secret, error) { + data, err := docker.NewDockerConfig(credentials) + if err != nil { + return nil, fmt.Errorf("error creating docker config for '%s': %v", secretName, err) + } + + json, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("error serializing docker config for '%s': %v", secretName, err) + } + + immutable := true + secret := corev1.Secret{ + ObjectMeta: v1.ObjectMeta{Name: secretName, Namespace: c.config.Namespace}, + Type: corev1.SecretTypeDockerConfigJson, + Immutable: &immutable, + Data: map[string][]byte{corev1.DockerConfigJsonKey: json}, + } + + return &secret, nil +} + +func (c *KubernetesClient) CreatePod(name string, envSecretName string, imagePullSecret string, jobRequest *api.JobRequest) error { + pod, err := c.podSpecFromJobRequest(name, envSecretName, imagePullSecret, jobRequest) if err != nil { return fmt.Errorf("error building pod spec: %v", err) } @@ -183,7 +224,7 @@ func (c *KubernetesClient) CreatePod(name string, envSecretName string, jobReque return nil } -func (c *KubernetesClient) podSpecFromJobRequest(podName string, envSecretName string, jobRequest *api.JobRequest) (*corev1.Pod, error) { +func (c *KubernetesClient) podSpecFromJobRequest(podName string, envSecretName string, imagePullSecret string, jobRequest *api.JobRequest) (*corev1.Pod, error) { containers, err := c.containers(jobRequest.Compose.Containers) if err != nil { return nil, fmt.Errorf("error building containers for pod spec: %v", err) @@ -191,7 +232,7 @@ func (c *KubernetesClient) podSpecFromJobRequest(podName string, envSecretName s spec := corev1.PodSpec{ Containers: containers, - ImagePullSecrets: c.imagePullSecrets(), + ImagePullSecrets: c.imagePullSecrets(imagePullSecret), RestartPolicy: corev1.RestartPolicyNever, Volumes: []corev1.Volume{ { @@ -217,6 +258,22 @@ func (c *KubernetesClient) podSpecFromJobRequest(podName string, envSecretName s }, nil } +func (c *KubernetesClient) imagePullSecrets(imagePullSecret string) []corev1.LocalObjectReference { + secrets := []corev1.LocalObjectReference{} + + // Use the secrets previously created, and passed to the agent through its configuration. + for _, s := range c.config.ImagePullSecrets { + secrets = append(secrets, corev1.LocalObjectReference{Name: s}) + } + + // Use the temporary secret created for the credentials sent in the job definition. + if imagePullSecret != "" { + secrets = append(secrets, corev1.LocalObjectReference{Name: imagePullSecret}) + } + + return secrets +} + func (c *KubernetesClient) containers(containers []api.Container) ([]corev1.Container, error) { // If the job specifies containers in the YAML, we use them. @@ -297,10 +354,6 @@ func (c *KubernetesClient) convertEnvVars(envVarsFromSemaphore []api.EnvVar) []c return k8sEnvVars } -func (c *KubernetesClient) imagePullSecrets() []corev1.LocalObjectReference { - return []corev1.LocalObjectReference{} -} - func (c *KubernetesClient) WaitForPod(name string, logFn func(string)) error { return retry.RetryWithConstantWait(retry.RetryOptions{ Task: "Waiting for pod to be ready", diff --git a/pkg/kubernetes/client_test.go b/pkg/kubernetes/client_test.go index 9838f1b5..00c76ad0 100644 --- a/pkg/kubernetes/client_test.go +++ b/pkg/kubernetes/client_test.go @@ -85,6 +85,51 @@ func Test__CreateSecret(t *testing.T) { }) } +func Test__CreateImagePullSecret(t *testing.T) { + t.Run("bad image pull credentials -> error", func(t *testing.T) { + clientset := newFakeClientset([]runtime.Object{}) + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", ImagePullPolicy: "Never"}) + err := client.CreateImagePullSecret("badsecret", []api.ImagePullCredentials{ + { + EnvVars: []api.EnvVar{ + {Name: "DOCKER_CREDENTIAL_TYPE", Value: base64.StdEncoding.EncodeToString([]byte("NOT_SUPPORTED"))}, + }, + }, + }) + + assert.Error(t, err) + assert.ErrorContains(t, err, "unknown DOCKER_CREDENTIAL_TYPE") + }) + + t.Run("good image pull credentials -> creates secret", func(t *testing.T) { + clientset := newFakeClientset([]runtime.Object{}) + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", ImagePullPolicy: "Never"}) + secretName := "mysecretname" + + err := client.CreateImagePullSecret(secretName, []api.ImagePullCredentials{ + { + EnvVars: []api.EnvVar{ + {Name: "DOCKER_CREDENTIAL_TYPE", Value: base64.StdEncoding.EncodeToString([]byte(api.ImagePullCredentialsStrategyGenericDocker))}, + {Name: "DOCKER_USERNAME", Value: base64.StdEncoding.EncodeToString([]byte("myuser"))}, + {Name: "DOCKER_PASSWORD", Value: base64.StdEncoding.EncodeToString([]byte("mypass"))}, + {Name: "DOCKER_URL", Value: base64.StdEncoding.EncodeToString([]byte("my-custom-registry.com"))}, + }, + }, + }) + + assert.NoError(t, err) + + secret, err := clientset.CoreV1(). + Secrets("default"). + Get(context.Background(), secretName, v1.GetOptions{}) + + assert.NoError(t, err) + assert.Equal(t, corev1.SecretTypeDockerConfigJson, secret.Type) + assert.True(t, *secret.Immutable) + assert.NotEmpty(t, secret.Data) + }) +} + func Test__CreatePod(t *testing.T) { t.Run("no containers and no default image specified -> error", func(t *testing.T) { clientset := newFakeClientset([]runtime.Object{}) @@ -92,7 +137,7 @@ func Test__CreatePod(t *testing.T) { podName := "mypod" envSecretName := "mysecret" - assert.ErrorContains(t, client.CreatePod(podName, envSecretName, &api.JobRequest{ + assert.ErrorContains(t, client.CreatePod(podName, envSecretName, "", &api.JobRequest{ Compose: api.Compose{ Containers: []api.Container{}, }, @@ -106,7 +151,7 @@ func Test__CreatePod(t *testing.T) { envSecretName := "mysecret" // create pod using job request - assert.NoError(t, client.CreatePod(podName, envSecretName, &api.JobRequest{ + assert.NoError(t, client.CreatePod(podName, envSecretName, "", &api.JobRequest{ Compose: api.Compose{ Containers: []api.Container{}, }, @@ -155,7 +200,7 @@ func Test__CreatePod(t *testing.T) { envSecretName := "mysecret" // create pod using job request - assert.NoError(t, client.CreatePod(podName, envSecretName, &api.JobRequest{ + assert.NoError(t, client.CreatePod(podName, envSecretName, "", &api.JobRequest{ Compose: api.Compose{ Containers: []api.Container{ { @@ -190,7 +235,7 @@ func Test__CreatePod(t *testing.T) { envSecretName := "mysecret" // create pod using job request - assert.NoError(t, client.CreatePod(podName, envSecretName, &api.JobRequest{ + assert.NoError(t, client.CreatePod(podName, envSecretName, "", &api.JobRequest{ Compose: api.Compose{ Containers: []api.Container{ { @@ -229,7 +274,7 @@ func Test__CreatePod(t *testing.T) { envSecretName := "mysecret" // create pod using job request - assert.NoError(t, client.CreatePod(podName, envSecretName, &api.JobRequest{ + assert.NoError(t, client.CreatePod(podName, envSecretName, "", &api.JobRequest{ Compose: api.Compose{ Containers: []api.Container{ { @@ -264,6 +309,95 @@ func Test__CreatePod(t *testing.T) { assert.Empty(t, pod.Spec.Containers[1].VolumeMounts) } }) + + t.Run("no image pull secrets", func(t *testing.T) { + clientset := newFakeClientset([]runtime.Object{}) + client, _ := NewKubernetesClient(clientset, Config{ + Namespace: "default", + DefaultImage: "default-image", + ImagePullPolicy: "Always", + }) + + podName := "mypod" + + // create pod using job request + assert.NoError(t, client.CreatePod(podName, "myenvsecret", "", &api.JobRequest{})) + + pod, err := clientset.CoreV1(). + Pods("default"). + Get(context.Background(), podName, v1.GetOptions{}) + + assert.NoError(t, err) + assert.Len(t, pod.Spec.ImagePullSecrets, 0) + }) + + t.Run("with image pull secrets from config", func(t *testing.T) { + clientset := newFakeClientset([]runtime.Object{}) + client, _ := NewKubernetesClient(clientset, Config{ + Namespace: "default", + DefaultImage: "default-image", + ImagePullPolicy: "Always", + ImagePullSecrets: []string{"secret-1"}, + }) + + podName := "mypod" + + // create pod using job request + assert.NoError(t, client.CreatePod(podName, "myenvsecret", "", &api.JobRequest{})) + + pod, err := clientset.CoreV1(). + Pods("default"). + Get(context.Background(), podName, v1.GetOptions{}) + + assert.NoError(t, err) + assert.Equal(t, pod.Spec.ImagePullSecrets, []corev1.LocalObjectReference{{Name: "secret-1"}}) + }) + + t.Run("with image pull secret - ephemeral", func(t *testing.T) { + clientset := newFakeClientset([]runtime.Object{}) + client, _ := NewKubernetesClient(clientset, Config{ + Namespace: "default", + DefaultImage: "default-image", + ImagePullPolicy: "Always", + }) + + podName := "mypod" + + // create pod using job request + assert.NoError(t, client.CreatePod(podName, "myenvsecret", "my-image-pull-secret", &api.JobRequest{})) + + pod, err := clientset.CoreV1(). + Pods("default"). + Get(context.Background(), podName, v1.GetOptions{}) + + assert.NoError(t, err) + assert.Equal(t, pod.Spec.ImagePullSecrets, []corev1.LocalObjectReference{{Name: "my-image-pull-secret"}}) + }) + + t.Run("with image pull secret from config + ephemeral", func(t *testing.T) { + clientset := newFakeClientset([]runtime.Object{}) + client, _ := NewKubernetesClient(clientset, Config{ + Namespace: "default", + DefaultImage: "default-image", + ImagePullPolicy: "Always", + ImagePullSecrets: []string{"secret-1"}, + }) + + podName := "mypod" + + // create pod using job request + assert.NoError(t, client.CreatePod(podName, "myenvsecret", "my-image-pull-secret", &api.JobRequest{})) + + pod, err := clientset.CoreV1(). + Pods("default"). + Get(context.Background(), podName, v1.GetOptions{}) + + assert.NoError(t, err) + assert.Equal(t, pod.Spec.ImagePullSecrets, []corev1.LocalObjectReference{ + {Name: "secret-1"}, + {Name: "my-image-pull-secret"}, + }) + }) } func Test__WaitForPod(t *testing.T) { diff --git a/pkg/listener/job_processor.go b/pkg/listener/job_processor.go index 84142b53..cbcae4ea 100644 --- a/pkg/listener/job_processor.go +++ b/pkg/listener/job_processor.go @@ -41,6 +41,7 @@ func StartJobProcessor(httpClient *http.Client, apiClient *selfhostedapi.API, co KubernetesExecutor: config.KubernetesExecutor, KubernetesDefaultImage: config.KubernetesDefaultImage, KubernetesImagePullPolicy: config.KubernetesImagePullPolicy, + KubernetesImagePullSecrets: config.KubernetesImagePullSecrets, KubernetesPodStartTimeoutSeconds: config.KubernetesPodStartTimeoutSeconds, } @@ -82,6 +83,7 @@ type JobProcessor struct { KubernetesExecutor bool KubernetesDefaultImage string KubernetesImagePullPolicy string + KubernetesImagePullSecrets []string KubernetesPodStartTimeoutSeconds int } @@ -178,6 +180,7 @@ func (p *JobProcessor) RunJob(jobID string) { UseKubernetesExecutor: p.KubernetesExecutor, KubernetesDefaultImage: p.KubernetesDefaultImage, KubernetesImagePullPolicy: p.KubernetesImagePullPolicy, + KubernetesImagePullSecrets: p.KubernetesImagePullSecrets, KubernetesPodStartTimeoutSeconds: p.KubernetesPodStartTimeoutSeconds, UploadJobLogs: p.UploadJobLogs, RefreshTokenFn: func() (string, error) { diff --git a/pkg/listener/listener.go b/pkg/listener/listener.go index 497d39eb..9a008e9d 100644 --- a/pkg/listener/listener.go +++ b/pkg/listener/listener.go @@ -42,6 +42,7 @@ type Config struct { KubernetesExecutor bool KubernetesDefaultImage string KubernetesImagePullPolicy string + KubernetesImagePullSecrets []string KubernetesPodStartTimeoutSeconds int } diff --git a/test/e2e/kubernetes/private_image_ecr_no_account_id.rb b/test/e2e/kubernetes/private_image_ecr_no_account_id.rb new file mode 100644 index 00000000..13a96e66 --- /dev/null +++ b/test/e2e/kubernetes/private_image_ecr_no_account_id.rb @@ -0,0 +1,83 @@ +#!/bin/ruby +# rubocop:disable all + +$AGENT_CONFIG = { + "endpoint" => "localhost:4567", + "token" => "321h1l2jkh1jk42341", + "no-https" => true, + "shutdown-hook-path" => "", + "disconnect-after-job" => false, + "env-vars" => [], + "files" => [], + "fail-on-missing-files" => false, + "kubernetes-executor" => true, + "kubernetes-image-pull-policy" => "IfNotPresent" +} + +require_relative '../../e2e' + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + + "executor": "dockercompose", + + "compose": { + "containers": [ + { + "name": "main", + "image": "#{ENV['AWS_IMAGE']}" + } + ], + + "image_pull_credentials": [ + { + "env_vars": [ + { "name": "DOCKER_CREDENTIAL_TYPE", "value": "#{Base64.strict_encode64("AWS_ECR")}" }, + { "name": "AWS_REGION", "value": "#{Base64.strict_encode64(ENV['AWS_REGION'])}" }, + { "name": "AWS_ACCESS_KEY_ID", "value": "#{Base64.strict_encode64(ENV['AWS_ACCESS_KEY_ID'])}" }, + { "name": "AWS_SECRET_ACCESS_KEY", "value": "#{Base64.strict_encode64(ENV['AWS_SECRET_ACCESS_KEY'])}" } + ] + } + ] + }, + + "env_vars": [], + + "files": [], + + "commands": [ + { "directive": "echo Hello World" } + ], + + "epilogue_always_commands": [], + + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Starting shell session..."} + *** LONG_OUTPUT *** + {"event":"cmd_finished", "timestamp":"*", "directive":"Starting shell session...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello World"} + {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello World","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} +LOG diff --git a/test/e2e/kubernetes/private_image_ecr_with_account_id.rb b/test/e2e/kubernetes/private_image_ecr_with_account_id.rb new file mode 100644 index 00000000..2fc19ec7 --- /dev/null +++ b/test/e2e/kubernetes/private_image_ecr_with_account_id.rb @@ -0,0 +1,86 @@ +#!/bin/ruby +# rubocop:disable all + +$AGENT_CONFIG = { + "endpoint" => "localhost:4567", + "token" => "321h1l2jkh1jk42341", + "no-https" => true, + "shutdown-hook-path" => "", + "disconnect-after-job" => false, + "env-vars" => [], + "files" => [], + "fail-on-missing-files" => false, + "kubernetes-executor" => true, + "kubernetes-image-pull-policy" => "IfNotPresent" +} + +require_relative '../../e2e' + +aws_account_id = ENV['AWS_ACCOUNT_ID'] + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + + "executor": "dockercompose", + + "compose": { + "containers": [ + { + "name": "main", + "image": "#{ENV['AWS_IMAGE']}" + } + ], + + "image_pull_credentials": [ + { + "env_vars": [ + { "name": "DOCKER_CREDENTIAL_TYPE", "value": "#{Base64.strict_encode64("AWS_ECR")}" }, + { "name": "AWS_REGION", "value": "#{Base64.strict_encode64(ENV['AWS_REGION'])}" }, + { "name": "AWS_ACCESS_KEY_ID", "value": "#{Base64.strict_encode64(ENV['AWS_ACCESS_KEY_ID'])}" }, + { "name": "AWS_SECRET_ACCESS_KEY", "value": "#{Base64.strict_encode64(ENV['AWS_SECRET_ACCESS_KEY'])}" }, + { "name": "AWS_ACCOUNT_ID", "value": "#{Base64.strict_encode64(aws_account_id)}" } + ] + } + ] + }, + + "env_vars": [], + + "files": [], + + "commands": [ + { "directive": "echo Hello World" } + ], + + "epilogue_always_commands": [], + + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Starting shell session..."} + *** LONG_OUTPUT *** + {"event":"cmd_finished", "timestamp":"*", "directive":"Starting shell session...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello World"} + {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello World","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + {"event":"job_finished", "timestamp":"*", "result":"passed"} +LOG diff --git a/test/e2e/kubernetes/private_image_gcr.rb b/test/e2e/kubernetes/private_image_gcr.rb new file mode 100644 index 00000000..f2ae4349 --- /dev/null +++ b/test/e2e/kubernetes/private_image_gcr.rb @@ -0,0 +1,85 @@ +#!/bin/ruby +# rubocop:disable all + +$AGENT_CONFIG = { + "endpoint" => "localhost:4567", + "token" => "321h1l2jkh1jk42341", + "no-https" => true, + "shutdown-hook-path" => "", + "disconnect-after-job" => false, + "env-vars" => [], + "files" => [], + "fail-on-missing-files" => false, + "kubernetes-executor" => true, + "kubernetes-image-pull-policy" => "IfNotPresent" +} + +require_relative '../../e2e' + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + + "executor": "dockercompose", + + "compose": { + "containers": [ + { + "name": "main", + "image": "#{ENV['GCR_IMAGE']}" + } + ], + + "image_pull_credentials": [ + { + "env_vars": [ + { "name": "DOCKER_CREDENTIAL_TYPE", "value": "#{Base64.strict_encode64("GCR")}" }, + { "name": "GCR_HOSTNAME", "value": "#{Base64.strict_encode64(ENV['GCR_HOSTNAME'])}" } + ], + "files": [ + { "path": "/tmp/gcr/keyfile.json", "content": "#{ENV['GCR_KEYFILE']}", "mode": "0755" } + ] + } + ] + }, + + "env_vars": [], + + "files": [], + + "commands": [ + { "directive": "echo Hello World" } + ], + + "epilogue_always_commands": [], + + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Starting shell session..."} + *** LONG_OUTPUT *** + {"event":"cmd_finished", "timestamp":"*", "directive":"Starting shell session...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"directive":"Exporting environment variables","event":"cmd_started","timestamp":"*"} + {"directive":"Exporting environment variables","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + {"directive":"Injecting Files","event":"cmd_started","timestamp":"*"} + {"directive":"Injecting Files","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + {"directive":"echo Hello World","event":"cmd_started","timestamp":"*"} + {"event":"cmd_output","output":"Hello World\\n","timestamp":"*"} + {"directive":"echo Hello World","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + + {"event":"job_finished","result":"passed","timestamp":"*"} +LOG diff --git a/test/e2e/kubernetes/private_image_generic.rb b/test/e2e/kubernetes/private_image_generic.rb new file mode 100644 index 00000000..a2f6594e --- /dev/null +++ b/test/e2e/kubernetes/private_image_generic.rb @@ -0,0 +1,84 @@ +#!/bin/ruby +# rubocop:disable all + +$AGENT_CONFIG = { + "endpoint" => "localhost:4567", + "token" => "321h1l2jkh1jk42341", + "no-https" => true, + "shutdown-hook-path" => "", + "disconnect-after-job" => false, + "env-vars" => [], + "files" => [], + "fail-on-missing-files" => false, + "kubernetes-executor" => true, + "kubernetes-image-pull-policy" => "IfNotPresent" +} + +require_relative '../../e2e' + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + + "executor": "dockercompose", + + "compose": { + "containers": [ + { + "name": "main", + "image": "#{ENV['DOCKER_REGISTRY_IMAGE']}" + } + ], + + "image_pull_credentials": [ + { + "env_vars": [ + { "name": "DOCKER_CREDENTIAL_TYPE", "value": "#{Base64.strict_encode64("GenericDocker")}" }, + { "name": "DOCKER_URL", "value": "#{Base64.strict_encode64(ENV['DOCKER_URL'])}" }, + { "name": "DOCKER_USERNAME", "value": "#{Base64.strict_encode64(ENV['DOCKER_USERNAME'])}" }, + { "name": "DOCKER_PASSWORD", "value": "#{Base64.strict_encode64(ENV['DOCKER_PASSWORD'])}" } + ] + } + ] + }, + + "env_vars": [], + + "files": [], + + "commands": [ + { "directive": "echo Hello World" } + ], + + "epilogue_always_commands": [], + + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Starting shell session..."} + *** LONG_OUTPUT *** + {"event":"cmd_finished", "timestamp":"*", "directive":"Starting shell session...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello World"} + {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello World","exit_code":0,"finished_at":"*","started_at":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + + {"event":"job_finished", "timestamp":"*", "result":"passed"} +LOG From 7261628321506ffc704f5ceaab660af5395f24f5 Mon Sep 17 00:00:00 2001 From: Mateusz Rymuszka Date: Mon, 6 Feb 2023 20:24:47 +0100 Subject: [PATCH 051/130] feat: stop job by returning 130 (#181) Co-authored-by: Lucas Pinheiro --- pkg/jobs/job.go | 3 ++- pkg/jobs/job_test.go | 53 ++++++++++++++++++++++++++++++++++++++++ test/support/commands.go | 8 ++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/pkg/jobs/job.go b/pkg/jobs/job.go index bcfb870a..049fc08a 100644 --- a/pkg/jobs/job.go +++ b/pkg/jobs/job.go @@ -272,7 +272,8 @@ func (job *Job) RunRegularCommands(options RunOptions) string { exitCode = job.RunCommandsUntilFirstFailure(job.Request.Commands) } - if job.Stopped { + if job.Stopped || exitCode == 130 { + job.Stopped = true log.Info("Regular commands were stopped") return JobStopped } else if exitCode == 0 { diff --git a/pkg/jobs/job_test.go b/pkg/jobs/job_test.go index 533d0111..6ee42a3d 100644 --- a/pkg/jobs/job_test.go +++ b/pkg/jobs/job_test.go @@ -534,6 +534,59 @@ func Test__StopJob(t *testing.T) { }) } +func Test__StopJobWithExitCode(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip() + } + + testLogger, testLoggerBackend := eventlogger.DefaultTestLogger() + request := &api.JobRequest{ + EnvVars: []api.EnvVar{}, + Commands: []api.Command{ + {Directive: testsupport.ReturnExitCodeCommand(130)}, + {Directive: testsupport.Output("hello")}, + }, + Callbacks: api.Callbacks{ + Finished: "https://httpbin.org/status/200", + TeardownFinished: "https://httpbin.org/status/200", + }, + Logger: api.Logger{ + Method: eventlogger.LoggerMethodPush, + }, + } + + job, err := NewJobWithOptions(&JobOptions{ + Request: request, + Client: http.DefaultClient, + Logger: testLogger, + }) + + assert.Nil(t, err) + + job.Run() + + assert.True(t, job.Stopped) + assert.Eventually(t, func() bool { return job.Finished }, 5*time.Second, 1*time.Second) + + simplifiedEvents, err := testLoggerBackend.SimplifiedEvents(true, false) + assert.Nil(t, err) + + assert.Equal(t, simplifiedEvents, []string{ + "job_started", + + "directive: Exporting environment variables", + "Exit Code: 0", + + "directive: Injecting Files", + "Exit Code: 0", + + fmt.Sprintf("directive: %s", testsupport.ReturnExitCodeCommand(130)), + fmt.Sprintf("Exit Code: %d", testsupport.ManuallyStoppedCommandExitCode()), + + "job_finished: stopped", + }) +} + func Test__StopJobOnEpilogue(t *testing.T) { testLogger, testLoggerBackend := eventlogger.DefaultTestLogger() request := &api.JobRequest{ diff --git a/test/support/commands.go b/test/support/commands.go index 18661d8f..e8608b4a 100644 --- a/test/support/commands.go +++ b/test/support/commands.go @@ -135,6 +135,14 @@ func StoppedCommandExitCode() int { return 1 } +func ReturnExitCodeCommand(exitCode int) string { + return "echo 'exit 130' | sh" +} + +func ManuallyStoppedCommandExitCode() int { + return 130 +} + func CopyFile(src, dest string) string { if runtime.GOOS == "windows" { return fmt.Sprintf("Copy-Item %s -Destination %s", src, dest) From f3b53684ef3925233dab4263fcf3ec1da7f96cfc Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Tue, 7 Feb 2023 17:10:53 -0300 Subject: [PATCH 052/130] feat: update job logs as artifact on agent serve (#183) --- .semaphore/semaphore.yml | 13 +++- Dockerfile.test | 8 +++ main.go | 17 ++--- pkg/api/job_request.go | 17 +++-- pkg/eventlogger/default.go | 12 +++- pkg/eventlogger/filebackend.go | 30 ++++++-- pkg/eventlogger/filebackend_test.go | 46 +++++++++++- pkg/eventlogger/httpbackend.go | 7 +- pkg/eventlogger/logger_test.go | 2 +- pkg/jobs/job.go | 9 ++- pkg/server/server.go | 24 +++++++ pkg/slices/slices.go | 11 +++ test/e2e.rb | 34 +++++++++ .../e2e/hosted/job_logs_as_artifact_always.rb | 69 ++++++++++++++++++ .../hosted/job_logs_as_artifact_default.rb | 68 ++++++++++++++++++ test/e2e/hosted/job_logs_as_artifact_never.rb | 70 +++++++++++++++++++ .../job_logs_as_artifact_not_trimmed.rb | 70 +++++++++++++++++++ .../hosted/job_logs_as_artifact_trimmed.rb | 70 +++++++++++++++++++ test/e2e_support/api_mode.rb | 14 ++-- 19 files changed, 554 insertions(+), 37 deletions(-) create mode 100644 pkg/slices/slices.go create mode 100644 test/e2e/hosted/job_logs_as_artifact_always.rb create mode 100644 test/e2e/hosted/job_logs_as_artifact_default.rb create mode 100644 test/e2e/hosted/job_logs_as_artifact_never.rb create mode 100644 test/e2e/hosted/job_logs_as_artifact_not_trimmed.rb create mode 100644 test/e2e/hosted/job_logs_as_artifact_trimmed.rb diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 6f8a8776..c1a45292 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -169,9 +169,18 @@ blocks: - docker exec -ti agent cat /tmp/agent_log jobs: - - name: Test SSH jump point + - name: Hosted commands: - - "TEST_MODE=api make e2e TEST=hosted/ssh_jump_points" + - "TEST_MODE=api make e2e TEST=hosted/$TEST" + matrix: + - env_var: TEST + values: + - ssh_jump_points + - job_logs_as_artifact_default + - job_logs_as_artifact_always + - job_logs_as_artifact_never + - job_logs_as_artifact_not_trimmed + - job_logs_as_artifact_trimmed - name: "Self hosted E2E" dependencies: [] diff --git a/Dockerfile.test b/Dockerfile.test index 647771f8..91a680c2 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -13,6 +13,14 @@ RUN sed -i 's/#Port 22/Port 2222/g' /etc/ssh/sshd_config RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && \ install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl +# Semaphore toolbox should also be available +RUN curl -sL https://github.com/semaphoreci/toolbox/releases/latest/download/self-hosted-linux.tar -o toolbox.tar && \ + tar -xf toolbox.tar && \ + mv toolbox /root/.toolbox && \ + /root/.toolbox/install-toolbox && \ + echo 'source ~/.toolbox/toolbox' >> /root/.bash_profile && \ + rm toolbox.tar + ADD server.key /app/server.key ADD server.crt /app/server.crt ADD build/agent /app/agent diff --git a/main.go b/main.go index 87e7525f..1a5ee91f 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,7 @@ import ( jobs "github.com/semaphoreci/agent/pkg/jobs" listener "github.com/semaphoreci/agent/pkg/listener" server "github.com/semaphoreci/agent/pkg/server" + slices "github.com/semaphoreci/agent/pkg/slices" log "github.com/sirupsen/logrus" pflag "github.com/spf13/pflag" "github.com/spf13/viper" @@ -227,24 +228,14 @@ func loadConfigFile(configFile string) { } func validateConfiguration() { - contains := func(list []string, item string) bool { - for _, x := range list { - if x == item { - return true - } - } - - return false - } - for _, key := range viper.AllKeys() { - if !contains(config.ValidConfigKeys, key) { + if !slices.Contains(config.ValidConfigKeys, key) { log.Fatalf("Unrecognized option '%s'. Exiting...", key) } } uploadJobLogs := viper.GetString(config.UploadJobLogs) - if !contains(config.ValidUploadJobLogsCondition, uploadJobLogs) { + if !slices.Contains(config.ValidUploadJobLogsCondition, uploadJobLogs) { log.Fatalf( "Unsupported value '%s' for '%s'. Allowed values are: %v. Exiting...", uploadJobLogs, @@ -254,7 +245,7 @@ func validateConfiguration() { } imagePullPolicy := viper.GetString(config.KubernetesImagePullPolicy) - if !contains(config.ValidImagePullPolicies, imagePullPolicy) { + if !slices.Contains(config.ValidImagePullPolicies, imagePullPolicy) { log.Fatalf( "Unsupported value '%s' for '%s'. Allowed values are: %v. Exiting...", imagePullPolicy, diff --git a/pkg/api/job_request.go b/pkg/api/job_request.go index e2885041..facfcdaa 100644 --- a/pkg/api/job_request.go +++ b/pkg/api/job_request.go @@ -79,9 +79,10 @@ type Callbacks struct { } type Logger struct { - Method string `json:"method" yaml:"method"` - URL string `json:"url" yaml:"url"` - Token string `json:"token" yaml:"token"` + Method string `json:"method" yaml:"method"` + URL string `json:"url" yaml:"url"` + Token string `json:"token" yaml:"token"` + MaxSizeInBytes int `json:"max_size_in_bytes" yaml:"max_size_in_bytes"` } type PublicKey string @@ -107,6 +108,10 @@ type JobRequest struct { Logger Logger `json:"logger" yaml:"logger"` } +func (j *JobRequest) FindEnvVar(varName string) (string, error) { + return findEnvVar(j.EnvVars, varName) +} + func NewRequestFromJSON(content []byte) (*JobRequest, error) { jobRequest := &JobRequest{} @@ -184,7 +189,11 @@ func (c *ImagePullCredentials) FindFile(path string) (string, error) { } func (c *ImagePullCredentials) FindEnvVar(varName string) (string, error) { - for _, envVar := range c.EnvVars { + return findEnvVar(c.EnvVars, varName) +} + +func findEnvVar(envVars []EnvVar, varName string) (string, error) { + for _, envVar := range envVars { if envVar.Name == varName { v, err := envVar.Decode() if err != nil { diff --git a/pkg/eventlogger/default.go b/pkg/eventlogger/default.go index dfa82707..e35d8a31 100644 --- a/pkg/eventlogger/default.go +++ b/pkg/eventlogger/default.go @@ -16,7 +16,7 @@ const LoggerMethodPush = "push" func CreateLogger(request *api.JobRequest, refreshTokenFn func() (string, error)) (*Logger, error) { switch request.Logger.Method { case LoggerMethodPull: - return Default() + return Default(request) case LoggerMethodPush: return DefaultHTTP(request, refreshTokenFn) default: @@ -24,9 +24,15 @@ func CreateLogger(request *api.JobRequest, refreshTokenFn func() (string, error) } } -func Default() (*Logger, error) { +func Default(request *api.JobRequest) (*Logger, error) { path := filepath.Join(os.TempDir(), fmt.Sprintf("job_log_%d.json", time.Now().UnixNano())) - backend, err := NewFileBackend(path) + + maxSize := DefaultMaxSizeInBytes + if request.Logger.MaxSizeInBytes > 0 { + maxSize = request.Logger.MaxSizeInBytes + } + + backend, err := NewFileBackend(path, maxSize) if err != nil { return nil, err } diff --git a/pkg/eventlogger/filebackend.go b/pkg/eventlogger/filebackend.go index 35a2603b..d3b2da83 100644 --- a/pkg/eventlogger/filebackend.go +++ b/pkg/eventlogger/filebackend.go @@ -10,13 +10,18 @@ import ( log "github.com/sirupsen/logrus" ) +// We use 16MB as the default max size for a job log. +// This default value is used when the job request doesn't specify a limit. +const DefaultMaxSizeInBytes = 16777216 + type FileBackend struct { - path string - file *os.File + path string + file *os.File + maxSizeInBytes int } -func NewFileBackend(path string) (*FileBackend, error) { - return &FileBackend{path: path}, nil +func NewFileBackend(path string, maxSizeInBytes int) (*FileBackend, error) { + return &FileBackend{path: path, maxSizeInBytes: maxSizeInBytes}, nil } func (l *FileBackend) Open() error { @@ -58,6 +63,23 @@ func (l *FileBackend) CloseWithOptions(options CloseOptions) error { return err } + if options.OnClose != nil { + fileInfo, err := os.Stat(l.file.Name()) + if err != nil { + log.Errorf("Couldn't stat file '%s': %v", l.file.Name(), err) + } else { + trimmed := fileInfo.Size() >= int64(l.maxSizeInBytes) + log.Debugf( + "Log file has %d bytes - max bytes allowed are %d - trimmed=%v", + fileInfo.Size(), + int64(l.maxSizeInBytes), + trimmed, + ) + + options.OnClose(trimmed) + } + } + log.Debugf("Removing %s\n", l.file.Name()) if err := os.Remove(l.file.Name()); err != nil { log.Errorf("Error removing logger file %s: %v\n", l.file.Name(), err) diff --git a/pkg/eventlogger/filebackend_test.go b/pkg/eventlogger/filebackend_test.go index 2e04fb76..1cb83e5c 100644 --- a/pkg/eventlogger/filebackend_test.go +++ b/pkg/eventlogger/filebackend_test.go @@ -15,7 +15,7 @@ import ( func Test__LogsArePushedToFile(t *testing.T) { tmpFileName := filepath.Join(os.TempDir(), fmt.Sprintf("logs_%d.json", time.Now().UnixNano())) - fileBackend, err := NewFileBackend(tmpFileName) + fileBackend, err := NewFileBackend(tmpFileName, DefaultMaxSizeInBytes) assert.Nil(t, err) assert.Nil(t, fileBackend.Open()) @@ -48,3 +48,47 @@ func Test__LogsArePushedToFile(t *testing.T) { err = fileBackend.Close() assert.Nil(t, err) } + +func Test__CloseWithOptions(t *testing.T) { + + t.Run("trimmed logs", func(t *testing.T) { + tmpFileName := filepath.Join(os.TempDir(), fmt.Sprintf("logs_%d.json", time.Now().UnixNano())) + + // The max is 50 bytes + fileBackend, err := NewFileBackend(tmpFileName, 50) + assert.Nil(t, err) + assert.Nil(t, fileBackend.Open()) + + timestamp := int(time.Now().Unix()) + assert.Nil(t, fileBackend.Write(&JobStartedEvent{Timestamp: timestamp, Event: "job_started"})) + assert.Nil(t, fileBackend.Write(&CommandStartedEvent{Timestamp: timestamp, Event: "cmd_started", Directive: "echo hello"})) + assert.Nil(t, fileBackend.Write(&CommandOutputEvent{Timestamp: timestamp, Event: "cmd_output", Output: "hello\n"})) + + logsWereTrimmed := false + err = fileBackend.CloseWithOptions(CloseOptions{OnClose: func(b bool) { logsWereTrimmed = b }}) + assert.Nil(t, err) + assert.True(t, logsWereTrimmed) + }) + + t.Run("no trimmed logs", func(t *testing.T) { + tmpFileName := filepath.Join(os.TempDir(), fmt.Sprintf("logs_%d.json", time.Now().UnixNano())) + + // The max is 1M + fileBackend, err := NewFileBackend(tmpFileName, 1024*1024) + assert.Nil(t, err) + assert.Nil(t, fileBackend.Open()) + + timestamp := int(time.Now().Unix()) + assert.Nil(t, fileBackend.Write(&JobStartedEvent{Timestamp: timestamp, Event: "job_started"})) + assert.Nil(t, fileBackend.Write(&CommandStartedEvent{Timestamp: timestamp, Event: "cmd_started", Directive: "echo hello"})) + assert.Nil(t, fileBackend.Write(&CommandOutputEvent{Timestamp: timestamp, Event: "cmd_output", Output: "hello\n"})) + + logsWereTrimmed := false + err = fileBackend.CloseWithOptions(CloseOptions{OnClose: func(b bool) { + logsWereTrimmed = b + }}) + + assert.Nil(t, err) + assert.False(t, logsWereTrimmed) + }) +} diff --git a/pkg/eventlogger/httpbackend.go b/pkg/eventlogger/httpbackend.go index 79b411f4..0c8bdc53 100644 --- a/pkg/eventlogger/httpbackend.go +++ b/pkg/eventlogger/httpbackend.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "math" "net/http" "os" "path/filepath" @@ -49,7 +50,11 @@ func NewHTTPBackend(config HTTPBackendConfig) (*HTTPBackend, error) { } path := filepath.Join(os.TempDir(), fmt.Sprintf("job_log_%d.json", time.Now().UnixNano())) - fileBackend, err := NewFileBackend(path) + + // The API will instruct the HTTP backend when to stop + // streaming logs due to their size hitting the limits. + // We don't need to impose any limits on the underlying file backend. + fileBackend, err := NewFileBackend(path, math.MaxInt32) if err != nil { return nil, err } diff --git a/pkg/eventlogger/logger_test.go b/pkg/eventlogger/logger_test.go index ea034cef..8542f6b0 100644 --- a/pkg/eventlogger/logger_test.go +++ b/pkg/eventlogger/logger_test.go @@ -14,7 +14,7 @@ import ( func Test__GeneratePlainLogs(t *testing.T) { tmpFileName := filepath.Join(os.TempDir(), fmt.Sprintf("logs_%d.json", time.Now().UnixNano())) - backend, _ := NewFileBackend(tmpFileName) + backend, _ := NewFileBackend(tmpFileName, DefaultMaxSizeInBytes) assert.Nil(t, backend.Open()) logger, _ := NewLogger(backend) generateLogEvents(t, 10, backend) diff --git a/pkg/jobs/job.go b/pkg/jobs/job.go index 049fc08a..ca4cae8c 100644 --- a/pkg/jobs/job.go +++ b/pkg/jobs/job.go @@ -402,7 +402,14 @@ func (job *Job) teardownWithCallbacks(result string, callbackRetryAttempts int) } log.Debug("Archivator finished") - err = job.Logger.Close() + + // The job already finished, but executor is still open. + // We use the open executor to upload the job logs as + // an artifact, in case it is above the acceptable limit. + err = job.Logger.CloseWithOptions(eventlogger.CloseOptions{ + OnClose: job.uploadLogsAsArtifact, + }) + if err != nil { log.Errorf("Error closing logger: %+v", err) } diff --git a/pkg/server/server.go b/pkg/server/server.go index fe047b5f..edab6a9a 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -17,6 +17,7 @@ import ( api "github.com/semaphoreci/agent/pkg/api" "github.com/semaphoreci/agent/pkg/config" jobs "github.com/semaphoreci/agent/pkg/jobs" + slices "github.com/semaphoreci/agent/pkg/slices" log "github.com/sirupsen/logrus" ) @@ -204,6 +205,7 @@ func (s *Server) Run(w http.ResponseWriter, r *http.Request) { FileInjections: []config.FileInjection{}, SelfHosted: false, RefreshTokenFn: nil, + UploadJobLogs: s.resolveUploadJobsConfig(request), }) if err != nil { @@ -231,3 +233,25 @@ func (s *Server) Stop(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) } + +func (s *Server) resolveUploadJobsConfig(jobRequest *api.JobRequest) string { + value, err := jobRequest.FindEnvVar("SEMAPHORE_AGENT_UPLOAD_JOB_LOGS") + + // We use config.UploadJobLogsConditionNever, by default. + if err != nil { + return config.UploadJobLogsConditionNever + } + + // If the value specified is not a valid one, use the default. + if !slices.Contains(config.ValidUploadJobLogsCondition, value) { + log.Debugf( + "The value '%s' is not acceptable as SEMAPHORE_AGENT_UPLOAD_JOB_LOGS - using '%s'", + value, config.UploadJobLogsConditionNever, + ) + + return config.UploadJobLogsConditionNever + } + + // Otherwise, use the value specified by job definition. + return value +} diff --git a/pkg/slices/slices.go b/pkg/slices/slices.go new file mode 100644 index 00000000..ddaa4be6 --- /dev/null +++ b/pkg/slices/slices.go @@ -0,0 +1,11 @@ +package slices + +func Contains(slice []string, item string) bool { + for _, x := range slice { + if x == item { + return true + } + } + + return false +} diff --git a/test/e2e.rb b/test/e2e.rb index 7bd065cf..a2871110 100644 --- a/test/e2e.rb +++ b/test/e2e.rb @@ -85,6 +85,40 @@ def wait_for_agent_to_shutdown $strategy.wait_for_agent_to_shutdown end +def assert_artifact_is_available + puts "Checking if artifact is available" + + # We give 20s for the artifact to appear here, to give the agent enough time + # to realize the "archivator" has reached out for the logs, and can close the logger. + Timeout.timeout(20) do + loop do + `artifact pull job agent/job_logs.txt` + if $?.exitstatus == 0 + puts "sucess: agent/job_logs.txt exists!" + break + else + print "." + sleep 2 + end + end + end +end + +def assert_artifact_is_not_available + + # We sleep here to make sure the agent has enough time to realize + # the "archivator" has reached out for the logs, and can close the logger. + puts "Waiting 20s to check if artifact exists..." + sleep 20 + + `artifact pull job agent/job_logs.txt` + if $?.exitstatus == 0 + abort "agent/job_logs.txt artifact exists, but shouldn't!" + else + puts "sucess: agent/job_logs.txt does not exist" + end +end + def bad_callback_url "https://httpbin.org/status/500" end diff --git a/test/e2e/hosted/job_logs_as_artifact_always.rb b/test/e2e/hosted/job_logs_as_artifact_always.rb new file mode 100644 index 00000000..d0118421 --- /dev/null +++ b/test/e2e/hosted/job_logs_as_artifact_always.rb @@ -0,0 +1,69 @@ +#!/bin/ruby +# rubocop:disable all + +require_relative '../../e2e' + +# Here, we use the SEMAPHORE_JOB_ID as the job ID for this test. +# Additionally, we pass the artifact related environment variables +# to the job, so that it can upload the job logs as an artifact after the job is done. +start_job <<-JSON + { + "id": "#{ENV["SEMAPHORE_JOB_ID"]}", + "executor": "shell", + "env_vars": [ + { "name": "SEMAPHORE_JOB_ID", "value": "#{Base64.strict_encode64(ENV["SEMAPHORE_JOB_ID"])}" }, + { "name": "SEMAPHORE_ORGANIZATION_URL", "value": "#{Base64.strict_encode64(ENV["SEMAPHORE_ORGANIZATION_URL"])}" }, + { "name": "SEMAPHORE_ARTIFACT_TOKEN", "value": "#{Base64.strict_encode64(ENV["SEMAPHORE_ARTIFACT_TOKEN"])}" }, + { "name": "SEMAPHORE_AGENT_UPLOAD_JOB_LOGS", "value": "#{Base64.strict_encode64("always")}" } + ], + "files": [], + "commands": [ + { "directive": "for i in {1..10}; do echo \\\"[$i] this is some output, just for testing purposes\\\" && sleep 1; done" } + ], + "epilogue_always_commands": [], + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": { + "method": "pull" + } + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_AGENT_UPLOAD_JOB_LOGS\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_ARTIFACT_TOKEN\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_ID\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_ORGANIZATION_URL\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"for i in {1..10}; do echo \\"[$i] this is some output, just for testing purposes\\" && sleep 1; done"} + {"event":"cmd_output", "timestamp":"*", "output":"[1] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[2] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[3] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[4] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[5] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[6] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[7] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[8] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[9] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[10] this is some output, just for testing purposes\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"for i in {1..10}; do echo \\"[$i] this is some output, just for testing purposes\\" && sleep 1; done","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + + {"event":"job_finished", "timestamp":"*", "result":"passed"} +LOG + +assert_artifact_is_available \ No newline at end of file diff --git a/test/e2e/hosted/job_logs_as_artifact_default.rb b/test/e2e/hosted/job_logs_as_artifact_default.rb new file mode 100644 index 00000000..50747ebe --- /dev/null +++ b/test/e2e/hosted/job_logs_as_artifact_default.rb @@ -0,0 +1,68 @@ +#!/bin/ruby +# rubocop:disable all + +require_relative '../../e2e' + +# Here, we use the SEMAPHORE_JOB_ID as the job ID for this test. +# Additionally, we pass the artifact related environment variables +# to the job, so that it can upload the job logs as an artifact after the job is done. +start_job <<-JSON + { + "id": "#{ENV["SEMAPHORE_JOB_ID"]}", + "executor": "shell", + "env_vars": [ + { "name": "SEMAPHORE_JOB_ID", "value": "#{Base64.strict_encode64(ENV["SEMAPHORE_JOB_ID"])}" }, + { "name": "SEMAPHORE_ORGANIZATION_URL", "value": "#{Base64.strict_encode64(ENV["SEMAPHORE_ORGANIZATION_URL"])}" }, + { "name": "SEMAPHORE_ARTIFACT_TOKEN", "value": "#{Base64.strict_encode64(ENV["SEMAPHORE_ARTIFACT_TOKEN"])}" } + ], + "files": [], + "commands": [ + { "directive": "for i in {1..10}; do echo \\\"[$i] this is some output, just for testing purposes\\\" && sleep 1; done" } + ], + "epilogue_always_commands": [], + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": { + "method": "pull", + "max_size_in_bytes": 100 + } + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_ARTIFACT_TOKEN\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_ID\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_ORGANIZATION_URL\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"for i in {1..10}; do echo \\"[$i] this is some output, just for testing purposes\\" && sleep 1; done"} + {"event":"cmd_output", "timestamp":"*", "output":"[1] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[2] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[3] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[4] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[5] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[6] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[7] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[8] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[9] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[10] this is some output, just for testing purposes\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"for i in {1..10}; do echo \\"[$i] this is some output, just for testing purposes\\" && sleep 1; done","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + + {"event":"job_finished", "timestamp":"*", "result":"passed"} +LOG + +assert_artifact_is_not_available \ No newline at end of file diff --git a/test/e2e/hosted/job_logs_as_artifact_never.rb b/test/e2e/hosted/job_logs_as_artifact_never.rb new file mode 100644 index 00000000..e3f3b7e4 --- /dev/null +++ b/test/e2e/hosted/job_logs_as_artifact_never.rb @@ -0,0 +1,70 @@ +#!/bin/ruby +# rubocop:disable all + +require_relative '../../e2e' + +# Here, we use the SEMAPHORE_JOB_ID as the job ID for this test. +# Additionally, we pass the artifact related environment variables +# to the job, so that it can upload the job logs as an artifact after the job is done. +start_job <<-JSON + { + "id": "#{ENV["SEMAPHORE_JOB_ID"]}", + "executor": "shell", + "env_vars": [ + { "name": "SEMAPHORE_JOB_ID", "value": "#{Base64.strict_encode64(ENV["SEMAPHORE_JOB_ID"])}" }, + { "name": "SEMAPHORE_ORGANIZATION_URL", "value": "#{Base64.strict_encode64(ENV["SEMAPHORE_ORGANIZATION_URL"])}" }, + { "name": "SEMAPHORE_ARTIFACT_TOKEN", "value": "#{Base64.strict_encode64(ENV["SEMAPHORE_ARTIFACT_TOKEN"])}" }, + { "name": "SEMAPHORE_AGENT_UPLOAD_JOB_LOGS", "value": "#{Base64.strict_encode64("never")}" } + ], + "files": [], + "commands": [ + { "directive": "for i in {1..10}; do echo \\\"[$i] this is some output, just for testing purposes\\\" && sleep 1; done" } + ], + "epilogue_always_commands": [], + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": { + "method": "pull", + "max_size_in_bytes": 100 + } + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_AGENT_UPLOAD_JOB_LOGS\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_ARTIFACT_TOKEN\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_ID\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_ORGANIZATION_URL\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"for i in {1..10}; do echo \\"[$i] this is some output, just for testing purposes\\" && sleep 1; done"} + {"event":"cmd_output", "timestamp":"*", "output":"[1] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[2] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[3] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[4] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[5] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[6] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[7] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[8] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[9] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[10] this is some output, just for testing purposes\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"for i in {1..10}; do echo \\"[$i] this is some output, just for testing purposes\\" && sleep 1; done","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + + {"event":"job_finished", "timestamp":"*", "result":"passed"} +LOG + +assert_artifact_is_not_available \ No newline at end of file diff --git a/test/e2e/hosted/job_logs_as_artifact_not_trimmed.rb b/test/e2e/hosted/job_logs_as_artifact_not_trimmed.rb new file mode 100644 index 00000000..ede84d4d --- /dev/null +++ b/test/e2e/hosted/job_logs_as_artifact_not_trimmed.rb @@ -0,0 +1,70 @@ +#!/bin/ruby +# rubocop:disable all + +require_relative '../../e2e' + +# Here, we use the SEMAPHORE_JOB_ID as the job ID for this test. +# Additionally, we pass the artifact related environment variables +# to the job, so that it can upload the job logs as an artifact after the job is done. +start_job <<-JSON + { + "id": "#{ENV["SEMAPHORE_JOB_ID"]}", + "executor": "shell", + "env_vars": [ + { "name": "SEMAPHORE_JOB_ID", "value": "#{Base64.strict_encode64(ENV["SEMAPHORE_JOB_ID"])}" }, + { "name": "SEMAPHORE_ORGANIZATION_URL", "value": "#{Base64.strict_encode64(ENV["SEMAPHORE_ORGANIZATION_URL"])}" }, + { "name": "SEMAPHORE_ARTIFACT_TOKEN", "value": "#{Base64.strict_encode64(ENV["SEMAPHORE_ARTIFACT_TOKEN"])}" }, + { "name": "SEMAPHORE_AGENT_UPLOAD_JOB_LOGS", "value": "#{Base64.strict_encode64("when-trimmed")}" } + ], + "files": [], + "commands": [ + { "directive": "for i in {1..10}; do echo \\\"[$i] this is some output, just for testing purposes\\\" && sleep 1; done" } + ], + "epilogue_always_commands": [], + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": { + "method": "pull", + "max_size_in_bytes": 1048576 + } + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_AGENT_UPLOAD_JOB_LOGS\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_ARTIFACT_TOKEN\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_ID\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_ORGANIZATION_URL\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"for i in {1..10}; do echo \\"[$i] this is some output, just for testing purposes\\" && sleep 1; done"} + {"event":"cmd_output", "timestamp":"*", "output":"[1] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[2] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[3] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[4] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[5] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[6] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[7] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[8] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[9] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[10] this is some output, just for testing purposes\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"for i in {1..10}; do echo \\"[$i] this is some output, just for testing purposes\\" && sleep 1; done","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + + {"event":"job_finished", "timestamp":"*", "result":"passed"} +LOG + +assert_artifact_is_not_available \ No newline at end of file diff --git a/test/e2e/hosted/job_logs_as_artifact_trimmed.rb b/test/e2e/hosted/job_logs_as_artifact_trimmed.rb new file mode 100644 index 00000000..e8302a80 --- /dev/null +++ b/test/e2e/hosted/job_logs_as_artifact_trimmed.rb @@ -0,0 +1,70 @@ +#!/bin/ruby +# rubocop:disable all + +require_relative '../../e2e' + +# Here, we use the SEMAPHORE_JOB_ID as the job ID for this test. +# Additionally, we pass the artifact related environment variables +# to the job, so that it can upload the job logs as an artifact after the job is done. +start_job <<-JSON + { + "id": "#{ENV["SEMAPHORE_JOB_ID"]}", + "executor": "shell", + "env_vars": [ + { "name": "SEMAPHORE_JOB_ID", "value": "#{Base64.strict_encode64(ENV["SEMAPHORE_JOB_ID"])}" }, + { "name": "SEMAPHORE_ORGANIZATION_URL", "value": "#{Base64.strict_encode64(ENV["SEMAPHORE_ORGANIZATION_URL"])}" }, + { "name": "SEMAPHORE_ARTIFACT_TOKEN", "value": "#{Base64.strict_encode64(ENV["SEMAPHORE_ARTIFACT_TOKEN"])}" }, + { "name": "SEMAPHORE_AGENT_UPLOAD_JOB_LOGS", "value": "#{Base64.strict_encode64("when-trimmed")}" } + ], + "files": [], + "commands": [ + { "directive": "for i in {1..10}; do echo \\\"[$i] this is some output, just for testing purposes\\\" && sleep 1; done" } + ], + "epilogue_always_commands": [], + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": { + "method": "pull", + "max_size_in_bytes": 100 + } + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_AGENT_UPLOAD_JOB_LOGS\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_ARTIFACT_TOKEN\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_ID\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_ORGANIZATION_URL\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"for i in {1..10}; do echo \\"[$i] this is some output, just for testing purposes\\" && sleep 1; done"} + {"event":"cmd_output", "timestamp":"*", "output":"[1] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[2] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[3] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[4] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[5] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[6] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[7] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[8] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[9] this is some output, just for testing purposes\\n"} + {"event":"cmd_output", "timestamp":"*", "output":"[10] this is some output, just for testing purposes\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"for i in {1..10}; do echo \\"[$i] this is some output, just for testing purposes\\" && sleep 1; done","exit_code":0,"finished_at":"*","started_at":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} + {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} + + {"event":"job_finished", "timestamp":"*", "result":"passed"} +LOG + +assert_artifact_is_available \ No newline at end of file diff --git a/test/e2e_support/api_mode.rb b/test/e2e_support/api_mode.rb index 20242b3e..641fca00 100644 --- a/test/e2e_support/api_mode.rb +++ b/test/e2e_support/api_mode.rb @@ -11,13 +11,13 @@ def boot_up_agent system "docker stop $(docker ps -q)" system "docker rm $(docker ps -qa)" system "docker build -t agent -f Dockerfile.test ." - system "docker run --privileged --device /dev/ptmx --network=host -v /tmp/agent-temp-directory/:/tmp/agent-temp-directory -v /var/run/docker.sock:/var/run/docker.sock --name agent -tdi agent bash -c \"service ssh restart && nohup ./agent serve --port 30000 --auth-token-secret 'TzRVcspTmxhM9fUkdi1T/0kVXNETCi8UdZ8dLM8va4E' & sleep infinity\"" + system "docker run --privileged --device /dev/ptmx --network=host -v /tmp/agent-temp-directory/:/tmp/agent-temp-directory -v /var/run/docker.sock:/var/run/docker.sock --name agent -tdi agent bash -c \"service ssh restart && export SEMAPHORE_AGENT_LOG_LEVEL=debug && nohup ./agent serve --port 30000 --auth-token-secret 'TzRVcspTmxhM9fUkdi1T/0kVXNETCi8UdZ8dLM8va4E' & sleep infinity\"" pingable = nil until pingable puts "Waiting for agent to start" - `curl -H "Authorization: Bearer #{$TOKEN}" --fail -X GET -k "https://0.0.0.0:30000/is_alive"` + `curl -s -H "Authorization: Bearer #{$TOKEN}" --fail -X GET -k "https://0.0.0.0:30000/is_alive"` pingable = ($?.exitstatus == 0) end @@ -31,7 +31,7 @@ def start_job(request) puts "============================" puts "Sending job request to Agent" - output = `curl -H "Authorization: Bearer #{$TOKEN}" --fail -X POST -k "https://0.0.0.0:30000/jobs" --data @#{r.path}` + output = `curl -s -H "Authorization: Bearer #{$TOKEN}" --fail -X POST -k "https://0.0.0.0:30000/jobs" --data @#{r.path}` abort "Failed to send: #{output}" if $?.exitstatus != 0 end @@ -40,7 +40,7 @@ def stop_job puts "============================" puts "Stopping job..." - output = `curl -H "Authorization: Bearer #{$TOKEN}" --fail -X POST -k "https://0.0.0.0:30000/jobs/terminate"` + output = `curl -s -H "Authorization: Bearer #{$TOKEN}" --fail -X POST -k "https://0.0.0.0:30000/jobs/terminate"` abort "Failed to stob job: #{output}" if $?.exitstatus != 0 end @@ -51,7 +51,7 @@ def wait_for_command_to_start(cmd) Timeout.timeout(60 * 2) do loop do - `curl -H "Authorization: Bearer #{$TOKEN}" --fail -k "https://0.0.0.0:30000/job_logs" | grep "#{cmd}"` + `curl -s -H "Authorization: Bearer #{$TOKEN}" --fail -k "https://0.0.0.0:30000/job_logs" | grep "#{cmd}"` if $?.exitstatus == 0 break @@ -68,7 +68,7 @@ def wait_for_job_to_finish Timeout.timeout(60 * 3) do loop do - `curl -H "Authorization: Bearer #{$TOKEN}" --fail -k "https://0.0.0.0:30000/job_logs" | grep "job_finished"` + `curl -s -H "Authorization: Bearer #{$TOKEN}" --fail -k "https://0.0.0.0:30000/job_logs" | grep "job_finished"` if $?.exitstatus == 0 break @@ -91,7 +91,7 @@ def assert_job_log(expected_log) puts "=========================" puts "Asserting Job Logs" - actual_log = `curl -H "Authorization: Bearer #{$TOKEN}" -k "https://0.0.0.0:30000/jobs/#{$JOB_ID}/log"` + actual_log = `curl -s -H "X-Client-Name: archivator" -H "Authorization: Bearer #{$TOKEN}" -k "https://0.0.0.0:30000/jobs/#{$JOB_ID}/log"` puts "-----------------------------------" puts actual_log From c43c1b43e9e1f95d55dad7f0d5b9d62d0c4043f6 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Wed, 8 Feb 2023 17:43:41 -0300 Subject: [PATCH 053/130] fix: randomize size of TTY read buffer (#184) --- pkg/shell/process.go | 15 +++------ pkg/shell/process_test.go | 67 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 11 deletions(-) create mode 100644 pkg/shell/process_test.go diff --git a/pkg/shell/process.go b/pkg/shell/process.go index d78c7a2a..03f7a4ec 100644 --- a/pkg/shell/process.go +++ b/pkg/shell/process.go @@ -2,7 +2,6 @@ package shell import ( "encoding/base64" - "flag" "fmt" "io" "math/rand" @@ -348,18 +347,12 @@ func (p *Process) writeCommandToFile(cmdFilePath, command string) error { } func (p *Process) readBufferSize() int { - if flag.Lookup("test.v") == nil { - return 100 - } - // simulating the worst kind of baud rate - // random in size, and possibly very short - - // The implementation needs to handle everything. + // random in size, and possibly very short. rand.Seed(time.Now().UnixNano()) - min := 1 - max := 20 + min := 64 + max := 256 // #nosec return rand.Intn(max-min) + min @@ -402,7 +395,7 @@ func (p *Process) read() error { } p.inputBuffer = append(p.inputBuffer, buffer[0:n]...) - log.Debugf("reading data from shell. Input buffer: %#v", string(p.inputBuffer)) + log.Debugf("reading data (%d bytes) from shell. Input buffer: %#v", n, string(p.inputBuffer)) return nil } diff --git a/pkg/shell/process_test.go b/pkg/shell/process_test.go new file mode 100644 index 00000000..ca85dd90 --- /dev/null +++ b/pkg/shell/process_test.go @@ -0,0 +1,67 @@ +package shell + +import ( + "fmt" + "os" + "strings" + "testing" +) + +func Benchmark__CommandOutput_128Bytes(b *testing.B) { + p := createProcess(b, fmt.Sprintf("echo '%s'", strings.Repeat("x", 128))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + p.Run() + } +} + +func Benchmark__CommandOutput_1K(b *testing.B) { + p := createProcess(b, fmt.Sprintf("echo '%s'", strings.Repeat("x", 1024))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + p.Run() + } +} + +func Benchmark__CommandOutput_10K(b *testing.B) { + p := createProcess(b, fmt.Sprintf("for i in {0..10}; do echo '%s'; done", strings.Repeat("x", 1024))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + p.Run() + } +} + +func Benchmark__CommandOutput_100K(b *testing.B) { + p := createProcess(b, fmt.Sprintf("for i in {0..100}; do echo '%s'; done", strings.Repeat("x", 1024))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + p.Run() + } +} + +func Benchmark__CommandOutput_1M(b *testing.B) { + p := createProcess(b, fmt.Sprintf("for i in {0..1000}; do echo '%s'; done", strings.Repeat("x", 1024))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + p.Run() + } +} + +func createProcess(b *testing.B, command string) *Process { + s, err := NewShell(os.TempDir()) + if err != nil { + b.Fatalf("error creating shell: %v", err) + } + + err = s.Start() + if err != nil { + b.Fatalf("error creating shell: %v", err) + } + + return NewProcess(Config{ + Shell: s, + StoragePath: os.TempDir(), + Command: command, + OnOutput: func(string) { /* discard output */ }, + }) +} From 99c8f22e1811dbc1e9b7a613036f871173d61757 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Wed, 1 Mar 2023 11:22:27 -0300 Subject: [PATCH 054/130] fix: update test dependencies (#185) --- test/hub_reference/Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/hub_reference/Gemfile.lock b/test/hub_reference/Gemfile.lock index feb4efec..ef034f2b 100644 --- a/test/hub_reference/Gemfile.lock +++ b/test/hub_reference/Gemfile.lock @@ -5,7 +5,7 @@ GEM eventmachine (1.2.7) mustermann (3.0.0) ruby2_keywords (~> 0.0.1) - rack (2.2.4) + rack (2.2.6.2) rack-protection (3.0.4) rack ruby2_keywords (0.0.5) From 9c9fe43b412a4979051854f626d92002b60e2255 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Wed, 1 Mar 2023 18:32:07 -0300 Subject: [PATCH 055/130] fix: cancel k8s executor setup on job stop (#186) --- pkg/executors/kubernetes_executor.go | 13 ++++++++++- pkg/kubernetes/client.go | 4 ++-- pkg/kubernetes/client_test.go | 33 ++++++++++++++++++++++++++-- pkg/retry/retry.go | 9 ++++++++ pkg/retry/retry_test.go | 21 ++++++++++++++++++ 5 files changed, 75 insertions(+), 5 deletions(-) diff --git a/pkg/executors/kubernetes_executor.go b/pkg/executors/kubernetes_executor.go index 5c54784e..edcc150a 100644 --- a/pkg/executors/kubernetes_executor.go +++ b/pkg/executors/kubernetes_executor.go @@ -1,6 +1,7 @@ package executors import ( + "context" "encoding/base64" "fmt" "math/rand" @@ -26,6 +27,9 @@ type KubernetesExecutor struct { logger *eventlogger.Logger Shell *shell.Shell + // If the executor is stopped before it even starts, we need to cancel it. + cancelFunc context.CancelFunc + // We need to keep track if the initial environment has already // been exposed or not, because ExportEnvVars() gets called twice. initialEnvironmentExposed bool @@ -107,7 +111,10 @@ func (e *KubernetesExecutor) Start() int { e.logger.LogCommandFinished(directive, exitCode, commandStartedAt, commandFinishedAt) }() - err := e.k8sClient.WaitForPod(e.podName, func(msg string) { + ctx, cancel := context.WithCancel(context.TODO()) + e.cancelFunc = cancel + + err := e.k8sClient.WaitForPod(ctx, e.podName, func(msg string) { e.logger.LogCommandOutput(msg) log.Info(msg) }) @@ -334,6 +341,10 @@ func (e *KubernetesExecutor) RunCommandWithOptions(options CommandOptions) int { func (e *KubernetesExecutor) Stop() int { log.Debug("Starting the process killing procedure") + if e.cancelFunc != nil { + e.cancelFunc() + } + if e.Shell != nil { err := e.Shell.Close() if err != nil { diff --git a/pkg/kubernetes/client.go b/pkg/kubernetes/client.go index 513b0135..1e20c537 100644 --- a/pkg/kubernetes/client.go +++ b/pkg/kubernetes/client.go @@ -354,8 +354,8 @@ func (c *KubernetesClient) convertEnvVars(envVarsFromSemaphore []api.EnvVar) []c return k8sEnvVars } -func (c *KubernetesClient) WaitForPod(name string, logFn func(string)) error { - return retry.RetryWithConstantWait(retry.RetryOptions{ +func (c *KubernetesClient) WaitForPod(ctx context.Context, name string, logFn func(string)) error { + return retry.RetryWithConstantWaitAndContext(ctx, retry.RetryOptions{ Task: "Waiting for pod to be ready", MaxAttempts: c.config.PollingAttempts(), DelayBetweenAttempts: c.config.PollingInterval(), diff --git a/pkg/kubernetes/client_test.go b/pkg/kubernetes/client_test.go index 00c76ad0..aca6e751 100644 --- a/pkg/kubernetes/client_test.go +++ b/pkg/kubernetes/client_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "testing" + "time" "github.com/semaphoreci/agent/pkg/api" assert "github.com/stretchr/testify/assert" @@ -413,7 +414,7 @@ func Test__WaitForPod(t *testing.T) { }) client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", DefaultImage: "default-image"}) - assert.NoError(t, client.WaitForPod(podName, func(s string) {})) + assert.NoError(t, client.WaitForPod(context.TODO(), podName, func(s string) {})) }) t.Run("pod does not exist - error", func(t *testing.T) { @@ -433,7 +434,35 @@ func Test__WaitForPod(t *testing.T) { PodPollingAttempts: 2, }) - assert.Error(t, client.WaitForPod("somepodthatdoesnotexist", func(s string) {})) + assert.Error(t, client.WaitForPod(context.TODO(), "somepodthatdoesnotexist", func(s string) {})) + }) + + t.Run("pod does not exist and context is cancelled - error", func(t *testing.T) { + podName := "mypod" + clientset := newFakeClientset([]runtime.Object{ + &corev1.Pod{ + ObjectMeta: v1.ObjectMeta{Name: podName, Namespace: "default"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "main", Image: "whatever"}}, + }, + }, + }) + + client, _ := NewKubernetesClient(clientset, Config{ + Namespace: "default", + DefaultImage: "default-image", + PodPollingAttempts: 120, + }) + + ctx, cancel := context.WithCancel(context.TODO()) + + // Wait a little bit before cancelling + go func() { + time.Sleep(time.Second) + cancel() + }() + + assert.ErrorContains(t, client.WaitForPod(ctx, "somepodthatdoesnotexist", func(s string) {}), "context canceled") }) } diff --git a/pkg/retry/retry.go b/pkg/retry/retry.go index 69cd86f4..6a1b682c 100644 --- a/pkg/retry/retry.go +++ b/pkg/retry/retry.go @@ -1,6 +1,7 @@ package retry import ( + "context" "fmt" "time" @@ -16,11 +17,19 @@ type RetryOptions struct { } func RetryWithConstantWait(options RetryOptions) error { + return RetryWithConstantWaitAndContext(context.TODO(), options) +} + +func RetryWithConstantWaitAndContext(ctx context.Context, options RetryOptions) error { if options.Fn == nil { return fmt.Errorf("options.Fn cannot be nil") } for attempt := 1; ; attempt++ { + if ctx.Err() != nil { + return ctx.Err() + } + err := options.Fn() if err == nil { return nil diff --git a/pkg/retry/retry_test.go b/pkg/retry/retry_test.go index d0a06868..ea6f3c39 100644 --- a/pkg/retry/retry_test.go +++ b/pkg/retry/retry_test.go @@ -1,6 +1,7 @@ package retry import ( + "context" "errors" "testing" "time" @@ -39,3 +40,23 @@ func Test__GivesUpAfterMaxRetries(t *testing.T) { assert.Equal(t, attempts, 5) assert.NotNil(t, err) } + +func Test__GivesUpIfContextIsCancelled(t *testing.T) { + ctx, cancel := context.WithCancel(context.TODO()) + + go func() { + time.Sleep(200 * time.Millisecond) + cancel() + }() + + err := RetryWithConstantWaitAndContext(ctx, RetryOptions{ + Task: "test", + MaxAttempts: 10, + DelayBetweenAttempts: 100 * time.Millisecond, + Fn: func() error { + return errors.New("bad error") + }, + }) + + assert.ErrorContains(t, err, "context canceled") +} From 8b7c9b7a534a6646ffa0896d089752b2a74f99b3 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Fri, 3 Mar 2023 17:41:06 -0300 Subject: [PATCH 056/130] fix: stop waiting if image cannot be pulled (#187) --- pkg/executors/kubernetes_executor.go | 6 +- pkg/kubernetes/client.go | 111 ++++++++++++++++++++------- 2 files changed, 89 insertions(+), 28 deletions(-) diff --git a/pkg/executors/kubernetes_executor.go b/pkg/executors/kubernetes_executor.go index edcc150a..e5b52eac 100644 --- a/pkg/executors/kubernetes_executor.go +++ b/pkg/executors/kubernetes_executor.go @@ -115,7 +115,7 @@ func (e *KubernetesExecutor) Start() int { e.cancelFunc = cancel err := e.k8sClient.WaitForPod(ctx, e.podName, func(msg string) { - e.logger.LogCommandOutput(msg) + e.logger.LogCommandOutput(msg + "\n") log.Info(msg) }) @@ -126,7 +126,8 @@ func (e *KubernetesExecutor) Start() int { return exitCode } - e.logger.LogCommandOutput("Starting a new bash session in the pod\n") + e.logger.LogCommandOutput("Pod is ready.\n") + e.logger.LogCommandOutput("Starting a new bash session in the pod...\n") // #nosec executable := "kubectl" @@ -161,6 +162,7 @@ func (e *KubernetesExecutor) Start() int { return exitCode } + e.logger.LogCommandOutput("Shell session is ready.\n") e.Shell = shell return exitCode } diff --git a/pkg/kubernetes/client.go b/pkg/kubernetes/client.go index 1e20c537..be126dff 100644 --- a/pkg/kubernetes/client.go +++ b/pkg/kubernetes/client.go @@ -355,62 +355,121 @@ func (c *KubernetesClient) convertEnvVars(envVarsFromSemaphore []api.EnvVar) []c } func (c *KubernetesClient) WaitForPod(ctx context.Context, name string, logFn func(string)) error { - return retry.RetryWithConstantWaitAndContext(ctx, retry.RetryOptions{ + var r findPodResult + + err := retry.RetryWithConstantWaitAndContext(ctx, retry.RetryOptions{ Task: "Waiting for pod to be ready", MaxAttempts: c.config.PollingAttempts(), DelayBetweenAttempts: c.config.PollingInterval(), HideError: true, Fn: func() error { - _, err := c.findPod(name) - if err != nil { - logFn(fmt.Sprintf("Pod is not ready yet: %v\n", err)) - return err + r = c.findPod(name) + if r.continueWaiting { + if r.err != nil { + logFn(r.err.Error()) + } + + return r.err } - logFn("Pod is ready.\n") return nil }, }) + + // If we stopped the retrying, + // but still an error occurred, we need to report that + if !r.continueWaiting && r.err != nil { + return r.err + } + + return err +} + +type findPodResult struct { + continueWaiting bool + err error } -func (c *KubernetesClient) findPod(name string) (*corev1.Pod, error) { +func (c *KubernetesClient) findPod(name string) findPodResult { pod, err := c.clientset.CoreV1(). Pods(c.config.Namespace). Get(context.Background(), name, v1.GetOptions{}) if err != nil { - return nil, err + return findPodResult{continueWaiting: true, err: err} } // If the pod already finished, something went wrong. if pod.Status.Phase == corev1.PodFailed || pod.Status.Phase == corev1.PodSucceeded { - return nil, fmt.Errorf( - "pod '%s' already finished with status %s - reason: '%v', message: '%v', statuses: %v", - pod.Name, - pod.Status.Phase, - pod.Status.Reason, - pod.Status.Message, - c.getContainerStatuses(pod.Status.ContainerStatuses), - ) - } - - // if pod is pending, we need to wait - if pod.Status.Phase == corev1.PodPending { - return nil, fmt.Errorf("pod in pending state - statuses: %v", c.getContainerStatuses(pod.Status.ContainerStatuses)) + return findPodResult{ + continueWaiting: false, + err: fmt.Errorf( + "pod '%s' already finished with status %s - reason: '%v', message: '%v', statuses: %v", + pod.Name, + pod.Status.Phase, + pod.Status.Reason, + pod.Status.Message, + c.getContainerStatuses(pod.Status.ContainerStatuses), + ), + } } // if one of the pod's containers isn't ready, we need to wait for _, container := range pod.Status.ContainerStatuses { + + // If the reason for a container to be in the waiting state + // is Kubernetes not being able to pull its image, + // we should not wait for the whole pod start timeout until the job fails. + if c.failedToPullImage(container.State.Waiting) { + return findPodResult{ + continueWaiting: false, + err: fmt.Errorf( + "failed to pull image for '%s': %v", + container.Name, + c.getContainerStatuses(pod.Status.ContainerStatuses), + ), + } + } + + // If the container is just not ready yet, we wait. if !container.Ready { - return nil, fmt.Errorf( - "container '%s' is not ready yet - statuses: %v", - container.Name, + return findPodResult{ + continueWaiting: true, + err: fmt.Errorf( + "container '%s' is not ready yet - statuses: %v", + container.Name, + c.getContainerStatuses(pod.Status.ContainerStatuses), + ), + } + } + } + + // if we get here, all the containers are ready + // but the pod is still pending, so we need to wait too. + if pod.Status.Phase == corev1.PodPending { + return findPodResult{ + continueWaiting: true, + err: fmt.Errorf( + "pod in pending state - statuses: %v", c.getContainerStatuses(pod.Status.ContainerStatuses), - ) + ), } } - return pod, nil + // If we get here, everything is ready, we can start the job. + return findPodResult{continueWaiting: false, err: nil} +} + +func (c *KubernetesClient) failedToPullImage(state *corev1.ContainerStateWaiting) bool { + if state == nil { + return false + } + + if state.Reason == "ErrImagePull" || state.Reason == "ImagePullBackOff" { + return true + } + + return false } func (c *KubernetesClient) getContainerStatuses(statuses []corev1.ContainerStatus) []string { From a9eb6c5a479bcaf1b87235a0719837d61a044b89 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Fri, 3 Mar 2023 17:42:27 -0300 Subject: [PATCH 057/130] build: update dependencies (#188) --- go.mod | 33 +++++++++++++++-------------- go.sum | 65 +++++++++++++++++++++++++++++----------------------------- 2 files changed, 48 insertions(+), 50 deletions(-) diff --git a/go.mod b/go.mod index 72619b0f..a91353da 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module github.com/semaphoreci/agent require ( - github.com/cenkalti/backoff/v4 v4.1.3 + github.com/cenkalti/backoff/v4 v4.2.0 github.com/creack/pty v1.1.18 - github.com/golang-jwt/jwt/v4 v4.4.2 + github.com/golang-jwt/jwt/v4 v4.5.0 github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 github.com/hashicorp/go-version v1.6.0 @@ -11,13 +11,13 @@ require ( github.com/renderedtext/go-watchman v0.0.0-20220524201126-042727917d44 github.com/sirupsen/logrus v1.9.0 github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.14.0 - github.com/stretchr/testify v1.8.1 - golang.org/x/sys v0.3.0 + github.com/spf13/viper v1.15.0 + github.com/stretchr/testify v1.8.2 + golang.org/x/sys v0.5.0 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.26.0 - k8s.io/apimachinery v0.26.0 - k8s.io/client-go v0.26.0 + k8s.io/api v0.26.2 + k8s.io/apimachinery v0.26.2 + k8s.io/client-go v0.26.2 ) require ( @@ -39,25 +39,24 @@ require ( github.com/imdario/mergo v0.3.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/magiconair/properties v1.8.6 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.6 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.0.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.6 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/spf13/afero v1.9.2 // indirect + github.com/spf13/afero v1.9.3 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/subosito/gotenv v1.4.1 // indirect - golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10 // indirect + github.com/subosito/gotenv v1.4.2 // indirect + golang.org/x/net v0.7.0 // indirect golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect - golang.org/x/term v0.3.0 // indirect - golang.org/x/text v0.5.0 // indirect - golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect + golang.org/x/term v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect + golang.org/x/time v0.1.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/alexcesaro/statsd.v2 v2.0.0 // indirect diff --git a/go.sum b/go.sum index 19676959..10801d06 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= -github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= +github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -87,8 +87,8 @@ github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5F github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= -github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -184,8 +184,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= -github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= @@ -205,10 +205,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo/v2 v2.4.0 h1:+Ig9nvqgS5OBSACXNk15PLdp0U9XPYROt9CFzVdFGIs= github.com/onsi/gomega v1.23.0 h1:/oxKu9c2HVap+F3PfKort2Hw5DEU+HGlW8n+tguWsys= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= -github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= +github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= +github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= @@ -221,16 +219,16 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= -github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= +github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU= -github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As= +github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= +github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -243,10 +241,11 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= -github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -328,8 +327,8 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10 h1:Frnccbp+ok2GkUS2tC84yAq/U9Vg+0sIO7aRL3T4Xnc= -golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -387,24 +386,24 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U= -golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= +golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -579,12 +578,12 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.26.0 h1:IpPlZnxBpV1xl7TGk/X6lFtpgjgntCg8PJ+qrPHAC7I= -k8s.io/api v0.26.0/go.mod h1:k6HDTaIFC8yn1i6pSClSqIwLABIcLV9l5Q4EcngKnQg= -k8s.io/apimachinery v0.26.0 h1:1feANjElT7MvPqp0JT6F3Ss6TWDwmcjLypwoPpEf7zg= -k8s.io/apimachinery v0.26.0/go.mod h1:tnPmbONNJ7ByJNz9+n9kMjNP8ON+1qoAIIC70lztu74= -k8s.io/client-go v0.26.0 h1:lT1D3OfO+wIi9UFolCrifbjUUgu7CpLca0AD8ghRLI8= -k8s.io/client-go v0.26.0/go.mod h1:I2Sh57A79EQsDmn7F7ASpmru1cceh3ocVT9KlX2jEZg= +k8s.io/api v0.26.2 h1:dM3cinp3PGB6asOySalOZxEG4CZ0IAdJsrYZXE/ovGQ= +k8s.io/api v0.26.2/go.mod h1:1kjMQsFE+QHPfskEcVNgL3+Hp88B80uj0QtSOlj8itU= +k8s.io/apimachinery v0.26.2 h1:da1u3D5wfR5u2RpLhE/ZtZS2P7QvDgLZTi9wrNZl/tQ= +k8s.io/apimachinery v0.26.2/go.mod h1:ats7nN1LExKHvJ9TmwootT00Yz05MuYqPXEXaVeOy5I= +k8s.io/client-go v0.26.2 h1:s1WkVujHX3kTp4Zn4yGNFK+dlDXy1bAAkIl+cFAiuYI= +k8s.io/client-go v0.26.2/go.mod h1:u5EjOuSyBa09yqqyY7m3abZeovO/7D/WehVVlZ2qcqU= k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+OGxg8HsuBr/5f6tVAjDu6E= From b4b809487578595fc3cfff7c44e97ae5d6235207 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Fri, 24 Mar 2023 14:01:40 -0300 Subject: [PATCH 058/130] fix: update test dependencies (#190) --- test/hub_reference/Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/hub_reference/Gemfile.lock b/test/hub_reference/Gemfile.lock index ef034f2b..0ea23479 100644 --- a/test/hub_reference/Gemfile.lock +++ b/test/hub_reference/Gemfile.lock @@ -5,7 +5,7 @@ GEM eventmachine (1.2.7) mustermann (3.0.0) ruby2_keywords (~> 0.0.1) - rack (2.2.6.2) + rack (2.2.6.4) rack-protection (3.0.4) rack ruby2_keywords (0.0.5) From 30b455afe30fafe6230b92a928df68f9877ae20d Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Mon, 27 Mar 2023 18:19:09 -0300 Subject: [PATCH 059/130] feat: --name-from-env configuration parameter (#191) --- main.go | 19 +++++++++++++++++-- pkg/config/config.go | 2 ++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 1a5ee91f..c176c79f 100644 --- a/main.go +++ b/main.go @@ -108,6 +108,7 @@ func getLogFilePath() string { func RunListener(httpClient *http.Client, logfile io.Writer) { configFile := pflag.String(config.ConfigFile, "", "Config file") _ = pflag.String(config.Name, "", "Name to use for the agent. If not set, a default random one is used.") + _ = pflag.String(config.NameFromEnv, "", "Specify name to use for the agent, using an environment variable. If --name and --name-from-env are empty, a random one is generated.") _ = pflag.String(config.Endpoint, "", "Endpoint where agents are registered") _ = pflag.String(config.Token, "", "Registration token") _ = pflag.Bool(config.NoHTTPS, false, "Use http for communication") @@ -256,15 +257,29 @@ func validateConfiguration() { } func getAgentName() string { + // --name configuration parameter was specified. agentName := viper.GetString(config.Name) if agentName != "" { - if len(agentName) < 8 || len(agentName) > 64 { - log.Fatalf("The agent name should have between 8 and 64 characters. '%s' has %d.", agentName, len(agentName)) + if len(agentName) < 8 || len(agentName) > 80 { + log.Fatalf("The agent name should have between 8 and 80 characters. '%s' has %d.", agentName, len(agentName)) } return agentName } + // --name-from-env configuration parameter was passed. + // We need to fetch the actual name from the environment variable. + envVarName := viper.GetString(config.NameFromEnv) + if envVarName != "" { + agentName := os.Getenv(envVarName) + if len(agentName) < 8 || len(agentName) > 80 { + log.Fatalf("The agent name should have between 8 and 80 characters. '%s' has %d.", agentName, len(agentName)) + } + + return agentName + } + + // No name was specified - we generate a random one. log.Infof("Agent name was not assigned - using a random one.") randomName, err := randomName() if err != nil { diff --git a/pkg/config/config.go b/pkg/config/config.go index 45f9ee63..a6b084b2 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -5,6 +5,7 @@ import "os" const ( ConfigFile = "config-file" Name = "name" + NameFromEnv = "name-from-env" Endpoint = "endpoint" Token = "token" NoHTTPS = "no-https" @@ -58,6 +59,7 @@ var ValidUploadJobLogsCondition = []string{ var ValidConfigKeys = []string{ ConfigFile, Name, + NameFromEnv, Endpoint, Token, NoHTTPS, From 61e44b64bb6f17d07307c6aef8dd277c3b169a5e Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Wed, 29 Mar 2023 14:52:34 -0300 Subject: [PATCH 060/130] feat: --kubernetes-pod-spec parameter (#192) --- .semaphore/semaphore.yml | 4 +- docs/kubernetes-executor.md | 241 ++++++++++++ docs/requirments.md | 24 -- go.mod | 1 + go.sum | 2 + main.go | 18 +- pkg/config/config.go | 8 +- pkg/executors/kubernetes_executor.go | 57 ++- pkg/jobs/job.go | 14 +- pkg/kubernetes/client.go | 215 +++++++---- pkg/kubernetes/client_test.go | 359 ++++++++++++++---- pkg/listener/job_processor.go | 12 +- pkg/listener/listener.go | 4 +- .../kubernetes/docker_compose__env-vars.rb | 6 +- .../kubernetes/docker_compose__epilogue.rb | 6 +- .../docker_compose__file-injection.rb | 6 +- .../docker_compose__multiple-containers.rb | 6 +- .../private_image_ecr_no_account_id.rb | 6 +- .../private_image_ecr_with_account_id.rb | 6 +- test/e2e/kubernetes/private_image_gcr.rb | 6 +- test/e2e/kubernetes/private_image_generic.rb | 6 +- test/e2e/kubernetes/shell__env-vars.rb | 90 ----- test/e2e/kubernetes/shell__epilogue.rb | 67 ---- test/e2e/kubernetes/shell__file-injection.rb | 80 ---- test/e2e/kubernetes/shell__not-allowed.rb | 48 +++ 25 files changed, 803 insertions(+), 489 deletions(-) create mode 100644 docs/kubernetes-executor.md delete mode 100644 docs/requirments.md delete mode 100644 test/e2e/kubernetes/shell__env-vars.rb delete mode 100644 test/e2e/kubernetes/shell__epilogue.rb delete mode 100644 test/e2e/kubernetes/shell__file-injection.rb create mode 100644 test/e2e/kubernetes/shell__not-allowed.rb diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index c1a45292..1653cb27 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -256,9 +256,7 @@ blocks: matrix: - env_var: TEST values: - - shell__env-vars - - shell__epilogue - - shell__file-injection + - shell__not-allowed - docker_compose__env-vars - docker_compose__epilogue - docker_compose__file-injection diff --git a/docs/kubernetes-executor.md b/docs/kubernetes-executor.md new file mode 100644 index 00000000..9f69cbc1 --- /dev/null +++ b/docs/kubernetes-executor.md @@ -0,0 +1,241 @@ +The Kubernetes executor creates a new Kubernetes pod to run every job it receives. Usually, the agents themselves run inside the Kubernetes cluster, but you can also run Semaphore jobs in Kubernetes pods while the agent runs somewhere else. + +- [Requirements](#requirements) +- [Permissions](#permissions) +- [Limitations](#limitations) +- [Configuration](#configuration) + - [--kubernetes-executor](#--kubernetes-executor) + - [--kubernetes-pod-start-timeout](#--kubernetes-pod-start-timeout) + - [--kubernetes-pod-spec](#--kubernetes-pod-spec) +- [Examples](#examples) + - [Specifying containers](#specifying-containers) + - [Configuring image pull policies](#configuring-image-pull-policies) + - [Using environment variables](#using-environment-variables) + - [Using files](#using-files) + - [Enforcing resource constraints](#enforcing-resource-constraints) + - [Using volumes and volume mounts](#using-volumes-and-volume-mounts) + - [Use private images](#use-private-images) + - [Semaphore secrets](#semaphore-secrets) + - [Use a manually created Kubernetes secret](#use-a-manually-created-kubernetes-secret) + - [Restricting images used in jobs](#restricting-images-used-in-jobs) + +## Requirements + +- The `kubectl` CLI needs to be available in the pod/host where the agent is running. +- `bash` and `git` should be available in the main container used for the job. +- In self-hosted environments, the [Semaphore toolbox](https://github.com/semaphoreci/toolbox) is not automatically installed during the beginning of the job, so it should be already available in the image being used for the job. Alternatively, you can use a pre-job hook to install it. +- The [GitHub SSH keys](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints) should be available in the image used for the job, to avoid having to manually verifying GitHub hosts. + +## Permissions + +The Kubernetes permissions required by the agent to use the Kubernetes executor are: + +```yaml +- apiGroups: [""] + resources: ["pods"] + verbs: ["get", "create", "delete"] +- apiGroups: [""] + resources: ["pods/exec"] + verbs: ["create"] +- apiGroups: [""] + resources: ["secrets"] + verbs: ["create", "delete"] +``` + +## Limitations + +- Sidecar containers are addressable through localhost, and not through DNS names. +- Running system-level software such as systemd and Docker requires privileged access to the Kubernetes nodes, which is not safe. If you need to run those workflows, consider using the [agent-aws-stack](https://github.com/renderedtext/agent-aws-stack) or [sysbox](https://github.com/nestybox/sysbox). + +## Configuration + +### --kubernetes-executor + +A flag to enable the Kubernetes executor. If not specified, the agent will follow its default mode: using the shell executor to run jobs without containers, and the docker-compose executor to run jobs with containers. + +If the agent is running inside a Kubernetes pod, it uses the service account Kubernetes gives to pods to create the Kubernetes client. If the agent is not running inside a Kubernetes pod, it will use the `~/.kube/config` file to authenticate with the Kubernetes cluster. + +### --kubernetes-pod-start-timeout + +By default, the Kubernetes executor waits for 300s for the pod to be ready to run the Semaphore job. If the pod doesn't come up in time, the Semaphore job will fail. That value can be configured with the Semaphore agent `--kubernetes-pod-start-timeout` parameter, which accepts a number of seconds. + +### --kubernetes-pod-spec + +By default, all the information to create the pod comes from the Semaphore YAML. More specifically, from the containers specified in the Semaphore YAML. However, you might need to configure the pod and the containers in it further. You can do that with the `--kubernetes-pod-spec` parameter. + +That parameter receives the name of a Kubernetes config map with additional configuration for the main container, sidecar containers and the pod itself: + +```yaml +apiVersion: core/v1 +kind: ConfigMap +metadata: + name: pod-spec-decorator-for-semaphore-jobs +data: + mainContainer: "" + sidecarContainers: "" + pod: "" +``` + +Each of the keys in the config map decorate a specific part of the pod created for the Semaphore job, and receive a string containing a YAML document: +- The `mainContainer` key allows you to specify the fields in the [Kubernetes container](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#container-v1-core) where the job commands will execute. +- The `sidecarContainers` key allows you to specify the fields in the [Kubernetes containers](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#container-v1-core) used as sidecars. +- The `pod` key allows you to specify the fields in the [Kubernetes pod](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#pod-v1-core) created for the Semaphore job. + +> **Note**
+> The `--kubernetes-pod-spec` parameter does not override what comes from the Semaphore YAML. It only decorates it. If you want to reject jobs that use untrusted images, use the [--kubernetes-allowed-images](#configure-the-allowed-images) parameter. + +## Examples + +### Specifying containers + +The Kubernetes executor requires the Semaphore YAML to specify the containers to use. If no containers are specified in the YAML, the job will fail. Here are the configurable fields in a container definition in the Semaphore YAML: + +```yaml +containers: + + # The first container (main) is where the commands will run. + # The only thing we need for that container is that it remains up, + # so we can `kubectl exec` into it, create a PTY, and run the commands. + # For that reason, we don't allow configuring the `entrypoint` and `command` fields of that container. + - name: main + image: ruby:2.7 + env_vars: [] + + # For the additional containers, `entrypoint` and `command` can be configured as well. + - name: db + image: postgres:9.6 + command: "" + entrypoint: "" + env_vars: [] +``` + +More information about how to specify containers in the Semaphore YAML in the [public docs](https://docs.semaphoreci.com/ci-cd-environment/custom-ci-cd-environment-with-docker/). + +### Configuring image pull policies + +By default, no image pull policy is set on any of the containers in the pod. That means Kubernetes will use its default, which is `IfNotPresent`. You can use the `--kubernetes-pod-spec` parameter to specify them: + +```yaml +kind: ConfigMap +metadata: + name: pod-spec-decorator-for-semaphore-jobs +data: + mainContainer: | + imagePullPolicy: Never + sidecarContainers: | + imagePullPolicy: Never +``` + +### Using environment variables + +You can use [Semaphore secrets](https://docs.semaphoreci.com/essentials/using-secrets/) or the Semaphore YAML's [env_vars](https://docs.semaphoreci.com/essentials/environment-variables/) to pass environment variables to your jobs, just like in cloud jobs. + +If you want to provide additional environment variables configured on the agent side, you can use the `--kubernetes-pod-spec` agent configuration parameter: + +```yaml +kind: ConfigMap +metadata: + name: pod-spec-for-semaphore-job +data: + # Add environment variables to the main container. + # These will only be available in the container where the Semaphore job runs. + mainContainer: | + env: + - name: FOO + value: BAR + + # You can also add environment variables to the sidecar containers in the pod. + # These will be added to all sidecar containers. + sidecarContainers: | + env: + - name: FOO + value: BAR +``` + +The environment variables specified with this approach will be appended to the ones specified in the Semaphore YAML (if any). + +### Using files + +You can use [Semaphore secrets](https://docs.semaphoreci.com/essentials/using-secrets/) to provide files to your job, just like in cloud jobs. Additionally, if you want to provide files to jobs from the agent side, you can use the `--kubernetes-pod-spec` agent parameter to decorate the pod spec. + +For example, if you have a Kubernetes secret called `secret-file`, you can inject that secret into the main container running the job using the pod spec config map: + +```yaml +kind: ConfigMap +metadata: + name: pod-spec-for-semaphore-job +data: + mainContainer: | + volumeMounts: + - name: myfile + mountPath: /app/files + pod: | + volumes: + - name: myfile + secret: + secretName: secret-file +``` + +All keys in the `secret-file` Kubernetes secret will be injected into the `/app/files` directory as files. + +### Enforcing resource constraints + +Use the `--kubernetes-pod-spec` agent parameter: + +```yaml +kind: ConfigMap +metadata: + name: pod-spec-for-semaphore-job +data: + mainContainer: | + resources: + limits: + cpu: "0.5" + memory: 500Mi + requests: + cpu: "0.25" + memory: 250Mi + sidecarContainers: | + resources: + limits: + cpu: "0.1" + memory: 100Mi + requests: + cpu: "0.1" + memory: 100Mi +``` + +### Using volumes and volume mounts + +See the [files](#use-files) section. + +### Use private images + +If the image being used to run the job is private, authentication is required to pull it. + +#### Semaphore secrets + +You can create a Semaphore secret containing the credentials to authenticate to your registry, and use it in your Semaphore YAML's [image_pull_secrets](https://docs.semaphoreci.com/ci-cd-environment/custom-ci-cd-environment-with-docker/#pulling-private-docker-images-from-dockerhub). When using this appproach, the Kubernetes executor will create a temporary Kubernetes secret to store the credentials, and use it to pull the images. When the job finishes, the Kubernetes secret will be deleted. + +> **Note**
+> This is the only way to use ECR images, since ECR doesn't allow long-lived tokens for authentication. + +#### Use a manually created Kubernetes secret + +You can also manually create a Kubernetes secret with your registry's credentials, and use the `--kubernetes-pod-spec` agent configuration parameter to use it: + +```yaml +kind: ConfigMap +metadata: + name: pod-spec-decorator-for-semaphore-jobs +data: + pod: | + imagePullSecrets: + - my-k8s-registry-secret +``` + +### Restricting images used in jobs + +By default, the Kubernetes executor accepts all images specified in the Semaphore YAML. If you want to restrict the images used in the jobs executed by your agents, you can use the `--kubernetes-allowed-images`. + +That parameter takes a list of regular expressions. If the image specified in the Semaphore YAML matches one of the expressions, it is allowed. For example, if you want to restrict jobs to only use images from a `custom-registry-1.com` registry, you can use `--kubernetes-allowed-images ^custom-registry-1\.com\/(.+)` diff --git a/docs/requirments.md b/docs/requirments.md deleted file mode 100644 index d8e374d9..00000000 --- a/docs/requirments.md +++ /dev/null @@ -1,24 +0,0 @@ -# Requirments - -This doc lists and explains the reasons for deprecating our current Job Runner -in favor of Semaphore Agents. - -Topics: - -- Agents in enterprise installations -- Running compose style CI and executing commands without Net::SSH -- Log collection and Live log for Agents (exploration of the resumable log collection to increase stability) -- DevOps overhead with Ruby -- Running Agents inside vs. outside of KVM images -- Open source code base for Agents and how to handle proprietery KVM managment -- Installation of Agents and security -- Support for multiple & extendable Agent backends - - Kubernetes backend - - NoVM backend - - KVM backend - - Docker backend - - SSH backend - - Docker Compose backend - - Docker swarm backend - - iOS - - Windows diff --git a/go.mod b/go.mod index a91353da..c9db531d 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/semaphoreci/agent require ( github.com/cenkalti/backoff/v4 v4.2.0 github.com/creack/pty v1.1.18 + github.com/ghodss/yaml v1.0.0 github.com/golang-jwt/jwt/v4 v4.5.0 github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 diff --git a/go.sum b/go.sum index 10801d06..668c46b7 100644 --- a/go.sum +++ b/go.sum @@ -71,6 +71,8 @@ github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= diff --git a/main.go b/main.go index c176c79f..3bc9067c 100644 --- a/main.go +++ b/main.go @@ -123,9 +123,7 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { _ = pflag.String(config.UploadJobLogs, config.UploadJobLogsConditionNever, "When should the agent upload the job logs as a job artifact. Default is never.") _ = pflag.Bool(config.FailOnPreJobHookError, false, "Fail job if pre-job hook fails") _ = pflag.Bool(config.KubernetesExecutor, false, "Use Kubernetes executor") - _ = pflag.String(config.KubernetesDefaultImage, "", "Default image used for jobs that do not specify images, when using kubernetes executor") - _ = pflag.String(config.KubernetesImagePullPolicy, config.ImagePullPolicyNever, "Image pull policy to use for Kubernetes executor. Default is never.") - _ = pflag.StringSlice(config.KubernetesImagePullSecrets, []string{}, "Kubernetes secrets to use to pull images.") + _ = pflag.String(config.KubernetesPodSpec, "", "Use a Kubernetes configmap to decorate the pod created to run the Semaphore job.") _ = pflag.Int( config.KubernetesPodStartTimeout, config.DefaultKubernetesPodStartTimeout, @@ -201,9 +199,7 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { AgentVersion: VERSION, ExitOnShutdown: true, KubernetesExecutor: viper.GetBool(config.KubernetesExecutor), - KubernetesDefaultImage: viper.GetString(config.KubernetesDefaultImage), - KubernetesImagePullPolicy: viper.GetString(config.KubernetesImagePullPolicy), - KubernetesImagePullSecrets: viper.GetStringSlice(config.KubernetesImagePullSecrets), + KubernetesPodSpec: viper.GetString(config.KubernetesPodSpec), KubernetesPodStartTimeoutSeconds: viper.GetInt(config.KubernetesPodStartTimeout), } @@ -244,16 +240,6 @@ func validateConfiguration() { config.ValidUploadJobLogsCondition, ) } - - imagePullPolicy := viper.GetString(config.KubernetesImagePullPolicy) - if !slices.Contains(config.ValidImagePullPolicies, imagePullPolicy) { - log.Fatalf( - "Unsupported value '%s' for '%s'. Allowed values are: %v. Exiting...", - imagePullPolicy, - config.KubernetesImagePullPolicy, - config.ValidImagePullPolicies, - ) - } } func getAgentName() string { diff --git a/pkg/config/config.go b/pkg/config/config.go index a6b084b2..49f32567 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -20,9 +20,7 @@ const ( FailOnPreJobHookError = "fail-on-pre-job-hook-error" InterruptionGracePeriod = "interruption-grace-period" KubernetesExecutor = "kubernetes-executor" - KubernetesDefaultImage = "kubernetes-default-image" - KubernetesImagePullPolicy = "kubernetes-image-pull-policy" - KubernetesImagePullSecrets = "kubernetes-image-pull-secrets" + KubernetesPodSpec = "kubernetes-pod-spec" KubernetesPodStartTimeout = "kubernetes-pod-start-timeout" ) @@ -74,9 +72,7 @@ var ValidConfigKeys = []string{ FailOnPreJobHookError, InterruptionGracePeriod, KubernetesExecutor, - KubernetesDefaultImage, - KubernetesImagePullPolicy, - KubernetesImagePullSecrets, + KubernetesPodSpec, KubernetesPodStartTimeout, } diff --git a/pkg/executors/kubernetes_executor.go b/pkg/executors/kubernetes_executor.go index e5b52eac..7380268d 100644 --- a/pkg/executors/kubernetes_executor.go +++ b/pkg/executors/kubernetes_executor.go @@ -59,13 +59,33 @@ func NewKubernetesExecutor(jobRequest *api.JobRequest, logger *eventlogger.Logge } func (e *KubernetesExecutor) Prepare() int { + commandStartedAt := int(time.Now().Unix()) + directive := "Creating Kubernetes resources for job..." + exitCode := 0 + + e.logger.LogCommandStarted(directive) + + defer func() { + commandFinishedAt := int(time.Now().Unix()) + e.logger.LogCommandFinished(directive, exitCode, commandStartedAt, commandFinishedAt) + }() + + err := e.k8sClient.LoadPodSpec() + if err != nil { + log.Errorf("Failed to load pod spec: %v", err) + e.logger.LogCommandOutput(fmt.Sprintf("Failed to load pod spec: %v\n", err)) + exitCode = 1 + return exitCode + } + e.podName = e.randomPodName() e.envSecretName = fmt.Sprintf("%s-secret", e.podName) - - err := e.k8sClient.CreateSecret(e.envSecretName, e.jobRequest) + err = e.k8sClient.CreateSecret(e.envSecretName, e.jobRequest) if err != nil { - log.Errorf("Error creating secret '%s': %v", e.envSecretName, err) - return 1 + log.Errorf("Failed to create environment secret: %v", err) + e.logger.LogCommandOutput(fmt.Sprintf("Failed to create environment secret: %v\n", err)) + exitCode = 1 + return exitCode } // If image pull credentials are specified in the YAML, @@ -74,15 +94,19 @@ func (e *KubernetesExecutor) Prepare() int { e.imagePullSecret = fmt.Sprintf("%s-image-pull-secret", e.podName) err = e.k8sClient.CreateImagePullSecret(e.imagePullSecret, e.jobRequest.Compose.ImagePullCredentials) if err != nil { - log.Errorf("Error creating image pull credentials '%s': %v", e.envSecretName, err) - return 1 + log.Errorf("Failed to create temporary image pull secret: %v", err) + e.logger.LogCommandOutput(fmt.Sprintf("Failed to create temporary image pull secret: %v\n", err)) + exitCode = 1 + return exitCode } } err = e.k8sClient.CreatePod(e.podName, e.envSecretName, e.imagePullSecret, e.jobRequest) if err != nil { - log.Errorf("Error creating pod: %v", err) - return 1 + log.Errorf("Failed to create pod: %v", err) + e.logger.LogCommandOutput(fmt.Sprintf("Failed to create pod: %v\n", err)) + exitCode = 1 + return exitCode } return 0 @@ -366,22 +390,23 @@ func (e *KubernetesExecutor) Cleanup() int { } func (e *KubernetesExecutor) removeK8sResources() { - err := e.k8sClient.DeletePod(e.podName) - if err != nil { - log.Errorf("Error deleting pod '%s': %v\n", e.podName, err) + if e.podName != "" { + if err := e.k8sClient.DeletePod(e.podName); err != nil { + log.Errorf("Error deleting pod '%s': %v\n", e.podName, err) + } } - err = e.k8sClient.DeleteSecret(e.envSecretName) - if err != nil { - log.Errorf("Error deleting secret '%s': %v\n", e.envSecretName, err) + if e.envSecretName != "" { + if err := e.k8sClient.DeleteSecret(e.envSecretName); err != nil { + log.Errorf("Error deleting secret '%s': %v\n", e.envSecretName, err) + } } // Not all jobs create this temporary secret, // just the ones that send credentials to pull images // in the job definition, so we only delete it if it was previously created. if e.imagePullSecret != "" { - err = e.k8sClient.DeleteSecret(e.imagePullSecret) - if err != nil { + if err := e.k8sClient.DeleteSecret(e.imagePullSecret); err != nil { log.Errorf("Error deleting secret '%s': %v\n", e.imagePullSecret, err) } } diff --git a/pkg/jobs/job.go b/pkg/jobs/job.go index ca4cae8c..7c9be040 100644 --- a/pkg/jobs/job.go +++ b/pkg/jobs/job.go @@ -47,9 +47,7 @@ type JobOptions struct { FailOnMissingFiles bool SelfHosted bool UseKubernetesExecutor bool - KubernetesDefaultImage string - KubernetesImagePullPolicy string - KubernetesImagePullSecrets []string + PodSpecDecoratorConfigMap string KubernetesPodStartTimeoutSeconds int UploadJobLogs string RefreshTokenFn func() (string, error) @@ -120,12 +118,10 @@ func CreateExecutor(request *api.JobRequest, logger *eventlogger.Logger, jobOpti } return executors.NewKubernetesExecutor(request, logger, kubernetes.Config{ - Namespace: namespace, - DefaultImage: jobOptions.KubernetesDefaultImage, - ImagePullPolicy: jobOptions.KubernetesImagePullPolicy, - ImagePullSecrets: jobOptions.KubernetesImagePullSecrets, - PodPollingAttempts: jobOptions.KubernetesPodStartTimeoutSeconds, - PodPollingInterval: time.Second, + Namespace: namespace, + PodSpecDecoratorConfigMap: jobOptions.PodSpecDecoratorConfigMap, + PodPollingAttempts: jobOptions.KubernetesPodStartTimeoutSeconds, + PodPollingInterval: time.Second, }) } diff --git a/pkg/kubernetes/client.go b/pkg/kubernetes/client.go index be126dff..39c17bdf 100644 --- a/pkg/kubernetes/client.go +++ b/pkg/kubernetes/client.go @@ -10,11 +10,13 @@ import ( "path/filepath" "time" + "github.com/ghodss/yaml" "github.com/semaphoreci/agent/pkg/api" "github.com/semaphoreci/agent/pkg/config" "github.com/semaphoreci/agent/pkg/docker" "github.com/semaphoreci/agent/pkg/retry" "github.com/semaphoreci/agent/pkg/shell" + log "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" @@ -23,12 +25,10 @@ import ( ) type Config struct { - Namespace string - DefaultImage string - ImagePullPolicy string - ImagePullSecrets []string - PodPollingAttempts int - PodPollingInterval time.Duration + Namespace string + PodSpecDecoratorConfigMap string + PodPollingAttempts int + PodPollingInterval time.Duration } func (c *Config) PollingInterval() time.Duration { @@ -56,8 +56,11 @@ func (c *Config) Validate() error { } type KubernetesClient struct { - clientset kubernetes.Interface - config Config + clientset kubernetes.Interface + config Config + podSpec *corev1.PodSpec + mainContainerSpec *corev1.Container + sidecarContainerSpec *corev1.Container } func NewKubernetesClient(clientset kubernetes.Interface, config Config) (*KubernetesClient, error) { @@ -65,10 +68,12 @@ func NewKubernetesClient(clientset kubernetes.Interface, config Config) (*Kubern return nil, fmt.Errorf("config is invalid: %v", err) } - return &KubernetesClient{ + c := &KubernetesClient{ clientset: clientset, config: config, - }, nil + } + + return c, nil } func NewInClusterClientset() (kubernetes.Interface, error) { @@ -105,6 +110,67 @@ func NewClientsetFromConfig() (kubernetes.Interface, error) { return clientset, nil } +// We use github.com/ghodss/yaml here +// because it can deserialise from YAML by using the json +// struct tags that are defined in the K8s API object structs. +func (c *KubernetesClient) LoadPodSpec() error { + if c.config.PodSpecDecoratorConfigMap == "" { + return nil + } + + configMap, err := c.clientset.CoreV1(). + ConfigMaps(c.config.Namespace). + Get(context.TODO(), c.config.PodSpecDecoratorConfigMap, v1.GetOptions{}) + + if err != nil { + return fmt.Errorf("error finding configmap '%s': %v", c.config.PodSpecDecoratorConfigMap, err) + } + + podSpecRaw, exists := configMap.Data["pod"] + if !exists { + log.Infof("No 'pod' key in '%s' - skipping pod decoration", c.config.PodSpecDecoratorConfigMap) + c.podSpec = nil + } else { + var podSpec corev1.PodSpec + err = yaml.Unmarshal([]byte(podSpecRaw), &podSpec) + if err != nil { + return fmt.Errorf("error unmarshaling pod spec from configmap '%s': %v", c.config.PodSpecDecoratorConfigMap, err) + } + + c.podSpec = &podSpec + } + + mainContainerSpecRaw, exists := configMap.Data["mainContainer"] + if !exists { + log.Infof("No 'mainContainer' key in '%s' - skipping main container decoration", c.config.PodSpecDecoratorConfigMap) + c.mainContainerSpec = nil + } else { + var mainContainer corev1.Container + err = yaml.Unmarshal([]byte(mainContainerSpecRaw), &mainContainer) + if err != nil { + return fmt.Errorf("error unmarshaling main container spec from configmap '%s': %v", c.config.PodSpecDecoratorConfigMap, err) + } + + c.mainContainerSpec = &mainContainer + } + + sidecarContainerSpecRaw, exists := configMap.Data["sidecarContainers"] + if !exists { + log.Infof("No 'sidecarContainers' key in '%s' - skipping sidecar containers decoration", c.config.PodSpecDecoratorConfigMap) + c.sidecarContainerSpec = nil + } else { + var sidecarContainer corev1.Container + err = yaml.Unmarshal([]byte(sidecarContainerSpecRaw), &sidecarContainer) + if err != nil { + return fmt.Errorf("error unmarshaling sidecar containers spec from configmap '%s': %v", c.config.PodSpecDecoratorConfigMap, err) + } + + c.sidecarContainerSpec = &sidecarContainer + } + + return nil +} + func (c *KubernetesClient) CreateSecret(name string, jobRequest *api.JobRequest) error { environment, err := shell.CreateEnvironment(jobRequest.EnvVars, []config.HostEnvVar{}) if err != nil { @@ -230,24 +296,27 @@ func (c *KubernetesClient) podSpecFromJobRequest(podName string, envSecretName s return nil, fmt.Errorf("error building containers for pod spec: %v", err) } - spec := corev1.PodSpec{ - Containers: containers, - ImagePullSecrets: c.imagePullSecrets(imagePullSecret), - RestartPolicy: corev1.RestartPolicyNever, - Volumes: []corev1.Volume{ - { - Name: "environment", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: envSecretName, - }, - }, + var spec *corev1.PodSpec + if c.podSpec != nil { + spec = c.podSpec.DeepCopy() + } else { + spec = &corev1.PodSpec{} + } + + spec.Containers = containers + spec.ImagePullSecrets = append(spec.ImagePullSecrets, c.imagePullSecrets(imagePullSecret)...) + spec.RestartPolicy = corev1.RestartPolicyNever + spec.Volumes = append(spec.Volumes, corev1.Volume{ + Name: "environment", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: envSecretName, }, }, - } + }) return &corev1.Pod{ - Spec: spec, + Spec: *spec, ObjectMeta: v1.ObjectMeta{ Namespace: c.config.Namespace, Name: podName, @@ -261,11 +330,6 @@ func (c *KubernetesClient) podSpecFromJobRequest(podName string, envSecretName s func (c *KubernetesClient) imagePullSecrets(imagePullSecret string) []corev1.LocalObjectReference { secrets := []corev1.LocalObjectReference{} - // Use the secrets previously created, and passed to the agent through its configuration. - for _, s := range c.config.ImagePullSecrets { - secrets = append(secrets, corev1.LocalObjectReference{Name: s}) - } - // Use the temporary secret created for the credentials sent in the job definition. if imagePullSecret != "" { secrets = append(secrets, corev1.LocalObjectReference{Name: imagePullSecret}) @@ -274,72 +338,69 @@ func (c *KubernetesClient) imagePullSecrets(imagePullSecret string) []corev1.Loc return secrets } -func (c *KubernetesClient) containers(containers []api.Container) ([]corev1.Container, error) { - - // If the job specifies containers in the YAML, we use them. - if len(containers) > 0 { - return c.convertContainersFromSemaphore(containers), nil +func (c *KubernetesClient) containers(apiContainers []api.Container) ([]corev1.Container, error) { + if len(apiContainers) > 0 { + return c.convertContainersFromSemaphore(apiContainers), nil } - // For jobs which do not specify containers, we require the default image to be specified. - if c.config.DefaultImage == "" { - return []corev1.Container{}, fmt.Errorf("no default image specified") - } + return []corev1.Container{}, fmt.Errorf("no containers specified in Semaphore YAML") +} - return []corev1.Container{ - { - Name: "main", - Image: c.config.DefaultImage, - ImagePullPolicy: corev1.PullPolicy(c.config.ImagePullPolicy), - VolumeMounts: []corev1.VolumeMount{ - { - Name: "environment", - ReadOnly: true, - MountPath: "/tmp/injected", - }, - }, +func (c *KubernetesClient) buildMainContainer(mainContainerFromAPI *api.Container) corev1.Container { + var mainContainer *corev1.Container + if c.mainContainerSpec != nil { + mainContainer = c.mainContainerSpec.DeepCopy() + } else { + mainContainer = &corev1.Container{} + } + + mainContainer.Name = "main" + mainContainer.Image = mainContainerFromAPI.Image + mainContainer.Env = append(mainContainer.Env, c.convertEnvVars(mainContainerFromAPI.EnvVars)...) + mainContainer.Command = []string{"bash", "-c", "sleep infinity"} + + // We append the volume mount for the environment variables secret, + // to the list of volume mounts configured. + mainContainer.VolumeMounts = append(mainContainer.VolumeMounts, corev1.VolumeMount{ + Name: "environment", + ReadOnly: true, + MountPath: "/tmp/injected", + }) - // The k8s pod shouldn't finish, so we sleep infinitely to keep it up. - Command: []string{"bash", "-c", "sleep infinity"}, - }, - }, nil + return *mainContainer } -func (c *KubernetesClient) convertContainersFromSemaphore(containers []api.Container) []corev1.Container { - main, rest := containers[0], containers[1:] +func (c *KubernetesClient) convertContainersFromSemaphore(apiContainers []api.Container) []corev1.Container { + main, rest := apiContainers[0], apiContainers[1:] // The main container needs to be up forever, // so we 'sleep infinity' in its command. - k8sContainers := []corev1.Container{ - { - Name: main.Name, - Image: main.Image, - Env: c.convertEnvVars(main.EnvVars), - Command: []string{"bash", "-c", "sleep infinity"}, - ImagePullPolicy: corev1.PullPolicy(c.config.ImagePullPolicy), - VolumeMounts: []corev1.VolumeMount{ - { - Name: "environment", - ReadOnly: true, - MountPath: "/tmp/injected", - }, - }, - }, - } + k8sContainers := []corev1.Container{c.buildMainContainer(&main)} // The rest of the containers will just follow whatever // their images are already configured to do. for _, container := range rest { - k8sContainers = append(k8sContainers, corev1.Container{ - Name: container.Name, - Image: container.Image, - Env: c.convertEnvVars(container.EnvVars), - }) + c := c.buildSidecarContainer(container) + k8sContainers = append(k8sContainers, *c) } return k8sContainers } +func (c *KubernetesClient) buildSidecarContainer(apiContainer api.Container) *corev1.Container { + var sidecarContainer *corev1.Container + if c.sidecarContainerSpec != nil { + sidecarContainer = c.sidecarContainerSpec.DeepCopy() + } else { + sidecarContainer = &corev1.Container{} + } + + sidecarContainer.Name = apiContainer.Name + sidecarContainer.Image = apiContainer.Image + sidecarContainer.Env = append(sidecarContainer.Env, c.convertEnvVars(apiContainer.EnvVars)...) + return sidecarContainer +} + func (c *KubernetesClient) convertEnvVars(envVarsFromSemaphore []api.EnvVar) []corev1.EnvVar { k8sEnvVars := []corev1.EnvVar{} diff --git a/pkg/kubernetes/client_test.go b/pkg/kubernetes/client_test.go index aca6e751..74a46587 100644 --- a/pkg/kubernetes/client_test.go +++ b/pkg/kubernetes/client_test.go @@ -3,12 +3,14 @@ package kubernetes import ( "context" "encoding/base64" + "fmt" "testing" "time" "github.com/semaphoreci/agent/pkg/api" assert "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes" @@ -18,7 +20,7 @@ import ( func Test__CreateSecret(t *testing.T) { t.Run("stores .env file in secret", func(t *testing.T) { clientset := newFakeClientset([]runtime.Object{}) - client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", DefaultImage: "default-image"}) + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default"}) secretName := "mysecret" // create secret using job request @@ -44,7 +46,7 @@ func Test__CreateSecret(t *testing.T) { t.Run("stores files in secret, with base64-encoded keys", func(t *testing.T) { clientset := newFakeClientset([]runtime.Object{}) - client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", DefaultImage: "default-image"}) + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default"}) secretName := "mysecret" // create secret using job request @@ -89,7 +91,7 @@ func Test__CreateSecret(t *testing.T) { func Test__CreateImagePullSecret(t *testing.T) { t.Run("bad image pull credentials -> error", func(t *testing.T) { clientset := newFakeClientset([]runtime.Object{}) - client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", ImagePullPolicy: "Never"}) + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default"}) err := client.CreateImagePullSecret("badsecret", []api.ImagePullCredentials{ { EnvVars: []api.EnvVar{ @@ -104,7 +106,7 @@ func Test__CreateImagePullSecret(t *testing.T) { t.Run("good image pull credentials -> creates secret", func(t *testing.T) { clientset := newFakeClientset([]runtime.Object{}) - client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", ImagePullPolicy: "Never"}) + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default"}) secretName := "mysecretname" err := client.CreateImagePullSecret(secretName, []api.ImagePullCredentials{ @@ -132,9 +134,10 @@ func Test__CreateImagePullSecret(t *testing.T) { } func Test__CreatePod(t *testing.T) { - t.Run("no containers and no default image specified -> error", func(t *testing.T) { + t.Run("no containers from YAML -> error", func(t *testing.T) { clientset := newFakeClientset([]runtime.Object{}) - client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", ImagePullPolicy: "Never"}) + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default"}) + _ = client.LoadPodSpec() podName := "mypod" envSecretName := "mysecret" @@ -142,20 +145,23 @@ func Test__CreatePod(t *testing.T) { Compose: api.Compose{ Containers: []api.Container{}, }, - }), "no default image specified") + }), "no containers specified in Semaphore YAML") }) - t.Run("no containers specified in job uses default image", func(t *testing.T) { + t.Run("containers and no pod spec", func(t *testing.T) { clientset := newFakeClientset([]runtime.Object{}) - client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", DefaultImage: "default-image", ImagePullPolicy: "Never"}) + client, err := NewKubernetesClient(clientset, Config{Namespace: "default"}) + if !assert.NoError(t, err) { + return + } + + _ = client.LoadPodSpec() podName := "mypod" envSecretName := "mysecret" // create pod using job request assert.NoError(t, client.CreatePod(podName, envSecretName, "", &api.JobRequest{ - Compose: api.Compose{ - Containers: []api.Container{}, - }, + Compose: api.Compose{Containers: []api.Container{{Name: "main", Image: "my-image"}}}, })) pod, err := clientset.CoreV1(). @@ -164,39 +170,28 @@ func Test__CreatePod(t *testing.T) { assert.NoError(t, err) - // assert pod metadata - assert.Equal(t, pod.ObjectMeta.Name, podName) - assert.Equal(t, pod.ObjectMeta.Namespace, "default") - assert.Equal(t, pod.ObjectMeta.Labels, map[string]string{"app": "semaphore-agent"}) - - // assert pod spec - assert.Equal(t, pod.Spec.RestartPolicy, corev1.RestartPolicyNever) - assert.Empty(t, pod.Spec.ImagePullSecrets) - // assert pod spec containers if assert.Len(t, pod.Spec.Containers, 1) { assert.Equal(t, pod.Spec.Containers[0].Name, "main") - assert.Equal(t, pod.Spec.Containers[0].Image, "default-image") - assert.Equal(t, pod.Spec.Containers[0].ImagePullPolicy, corev1.PullNever) + assert.Equal(t, pod.Spec.Containers[0].Image, "my-image") + assert.Equal(t, pod.Spec.Containers[0].ImagePullPolicy, corev1.PullPolicy("")) assert.Equal(t, pod.Spec.Containers[0].Command, []string{"bash", "-c", "sleep infinity"}) assert.Empty(t, pod.Spec.Containers[0].Env) assert.Equal(t, pod.Spec.Containers[0].VolumeMounts, []corev1.VolumeMount{{Name: "environment", ReadOnly: true, MountPath: "/tmp/injected"}}) } - - // assert pod spec volumes - if assert.Len(t, pod.Spec.Volumes, 1) { - assert.Equal(t, pod.Spec.Volumes[0].Name, "environment") - assert.Equal(t, pod.Spec.Volumes[0].VolumeSource, corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: envSecretName, - }, - }) - } }) t.Run("1 container", func(t *testing.T) { - clientset := newFakeClientset([]runtime.Object{}) - client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", DefaultImage: "default-image", ImagePullPolicy: "Always"}) + clientset := newFakeClientset([]runtime.Object{ + podSpecWithImagePullPolicy("default", "Always"), + }) + + client, err := NewKubernetesClient(clientset, Config{Namespace: "default", PodSpecDecoratorConfigMap: "pod-spec"}) + if !assert.NoError(t, err) { + return + } + + _ = client.LoadPodSpec() podName := "mypod" envSecretName := "mysecret" @@ -231,7 +226,8 @@ func Test__CreatePod(t *testing.T) { t.Run("container with env vars", func(t *testing.T) { clientset := newFakeClientset([]runtime.Object{}) - client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", DefaultImage: "default-image", ImagePullPolicy: "Always"}) + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default"}) + _ = client.LoadPodSpec() podName := "mypod" envSecretName := "mysecret" @@ -261,16 +257,61 @@ func Test__CreatePod(t *testing.T) { if assert.Len(t, pod.Spec.Containers, 1) { assert.Equal(t, pod.Spec.Containers[0].Name, "main") assert.Equal(t, pod.Spec.Containers[0].Image, "custom-image") - assert.Equal(t, pod.Spec.Containers[0].ImagePullPolicy, corev1.PullAlways) + assert.Equal(t, pod.Spec.Containers[0].ImagePullPolicy, corev1.PullPolicy("")) assert.Equal(t, pod.Spec.Containers[0].Command, []string{"bash", "-c", "sleep infinity"}) assert.Equal(t, pod.Spec.Containers[0].Env, []corev1.EnvVar{{Name: "A", Value: "AAA"}}) assert.Equal(t, pod.Spec.Containers[0].VolumeMounts, []corev1.VolumeMount{{Name: "environment", ReadOnly: true, MountPath: "/tmp/injected"}}) } }) + t.Run("container with env vars + pod spec with env", func(t *testing.T) { + clientset := newFakeClientset([]runtime.Object{podSpecWithEnv("default")}) + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", PodSpecDecoratorConfigMap: "pod-spec"}) + _ = client.LoadPodSpec() + podName := "mypod" + envSecretName := "mysecret" + + // create pod using job request + assert.NoError(t, client.CreatePod(podName, envSecretName, "", &api.JobRequest{ + Compose: api.Compose{ + Containers: []api.Container{ + { + Name: "main", + Image: "custom-image", + EnvVars: []api.EnvVar{ + { + Name: "A", + Value: base64.StdEncoding.EncodeToString([]byte("AAA")), + }, + }, + }, + }, + }, + })) + + pod, err := clientset.CoreV1(). + Pods("default"). + Get(context.Background(), podName, v1.GetOptions{}) + + assert.NoError(t, err) + if assert.Len(t, pod.Spec.Containers, 1) { + assert.Equal(t, pod.Spec.Containers[0].Name, "main") + assert.Equal(t, pod.Spec.Containers[0].Image, "custom-image") + assert.Equal(t, pod.Spec.Containers[0].ImagePullPolicy, corev1.PullPolicy("")) + assert.Equal(t, pod.Spec.Containers[0].Command, []string{"bash", "-c", "sleep infinity"}) + assert.Equal(t, pod.Spec.Containers[0].VolumeMounts, []corev1.VolumeMount{{Name: "environment", ReadOnly: true, MountPath: "/tmp/injected"}}) + assert.Equal(t, pod.Spec.Containers[0].Env, []corev1.EnvVar{ + {Name: "FOO_1", Value: "BAR_1"}, + {Name: "FOO_2", Value: "BAR_2"}, + {Name: "A", Value: "AAA"}, + }) + } + }) + t.Run("multiple containers", func(t *testing.T) { clientset := newFakeClientset([]runtime.Object{}) - client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", DefaultImage: "default-image", ImagePullPolicy: "Always"}) + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default"}) + _ = client.LoadPodSpec() podName := "mypod" envSecretName := "mysecret" @@ -313,16 +354,21 @@ func Test__CreatePod(t *testing.T) { t.Run("no image pull secrets", func(t *testing.T) { clientset := newFakeClientset([]runtime.Object{}) - client, _ := NewKubernetesClient(clientset, Config{ - Namespace: "default", - DefaultImage: "default-image", - ImagePullPolicy: "Always", - }) - + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default"}) + _ = client.LoadPodSpec() podName := "mypod" // create pod using job request - assert.NoError(t, client.CreatePod(podName, "myenvsecret", "", &api.JobRequest{})) + assert.NoError(t, client.CreatePod(podName, "myenvsecret", "", &api.JobRequest{ + Compose: api.Compose{ + Containers: []api.Container{ + { + Name: "main", + Image: "custom-image", + }, + }, + }, + })) pod, err := clientset.CoreV1(). Pods("default"). @@ -332,19 +378,34 @@ func Test__CreatePod(t *testing.T) { assert.Len(t, pod.Spec.ImagePullSecrets, 0) }) - t.Run("with image pull secrets from config", func(t *testing.T) { - clientset := newFakeClientset([]runtime.Object{}) - client, _ := NewKubernetesClient(clientset, Config{ - Namespace: "default", - DefaultImage: "default-image", - ImagePullPolicy: "Always", - ImagePullSecrets: []string{"secret-1"}, + t.Run("with image pull secrets from pod spec", func(t *testing.T) { + clientset := newFakeClientset([]runtime.Object{ + podSpecWithImagePullSecret("default", "secret-1"), + }) + + client, err := NewKubernetesClient(clientset, Config{ + Namespace: "default", + PodSpecDecoratorConfigMap: "pod-spec", }) + if !assert.NoError(t, err) { + return + } + + _ = client.LoadPodSpec() podName := "mypod" // create pod using job request - assert.NoError(t, client.CreatePod(podName, "myenvsecret", "", &api.JobRequest{})) + assert.NoError(t, client.CreatePod(podName, "myenvsecret", "", &api.JobRequest{ + Compose: api.Compose{ + Containers: []api.Container{ + { + Name: "main", + Image: "custom-image", + }, + }, + }, + })) pod, err := clientset.CoreV1(). Pods("default"). @@ -354,18 +415,23 @@ func Test__CreatePod(t *testing.T) { assert.Equal(t, pod.Spec.ImagePullSecrets, []corev1.LocalObjectReference{{Name: "secret-1"}}) }) - t.Run("with image pull secret - ephemeral", func(t *testing.T) { + t.Run("with image pull secret from YAML", func(t *testing.T) { clientset := newFakeClientset([]runtime.Object{}) - client, _ := NewKubernetesClient(clientset, Config{ - Namespace: "default", - DefaultImage: "default-image", - ImagePullPolicy: "Always", - }) - + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default"}) + _ = client.LoadPodSpec() podName := "mypod" // create pod using job request - assert.NoError(t, client.CreatePod(podName, "myenvsecret", "my-image-pull-secret", &api.JobRequest{})) + assert.NoError(t, client.CreatePod(podName, "myenvsecret", "my-image-pull-secret", &api.JobRequest{ + Compose: api.Compose{ + Containers: []api.Container{ + { + Name: "main", + Image: "custom-image", + }, + }, + }, + })) pod, err := clientset.CoreV1(). Pods("default"). @@ -375,19 +441,30 @@ func Test__CreatePod(t *testing.T) { assert.Equal(t, pod.Spec.ImagePullSecrets, []corev1.LocalObjectReference{{Name: "my-image-pull-secret"}}) }) - t.Run("with image pull secret from config + ephemeral", func(t *testing.T) { - clientset := newFakeClientset([]runtime.Object{}) + t.Run("with image pull secret from pod spec and from YAML", func(t *testing.T) { + clientset := newFakeClientset([]runtime.Object{ + podSpecWithImagePullSecret("default", "secret-1"), + }) + client, _ := NewKubernetesClient(clientset, Config{ - Namespace: "default", - DefaultImage: "default-image", - ImagePullPolicy: "Always", - ImagePullSecrets: []string{"secret-1"}, + Namespace: "default", + PodSpecDecoratorConfigMap: "pod-spec", }) + _ = client.LoadPodSpec() podName := "mypod" // create pod using job request - assert.NoError(t, client.CreatePod(podName, "myenvsecret", "my-image-pull-secret", &api.JobRequest{})) + assert.NoError(t, client.CreatePod(podName, "myenvsecret", "my-image-pull-secret", &api.JobRequest{ + Compose: api.Compose{ + Containers: []api.Container{ + { + Name: "main", + Image: "custom-image", + }, + }, + }, + })) pod, err := clientset.CoreV1(). Pods("default"). @@ -399,6 +476,77 @@ func Test__CreatePod(t *testing.T) { {Name: "my-image-pull-secret"}, }) }) + + t.Run("with resources", func(t *testing.T) { + clientset := newFakeClientset([]runtime.Object{podSpecWithResources("default")}) + client, _ := NewKubernetesClient(clientset, Config{ + Namespace: "default", + PodSpecDecoratorConfigMap: "pod-spec", + }) + + _ = client.LoadPodSpec() + podName := "mypod" + + // create pod using job request + assert.NoError(t, client.CreatePod(podName, "myenvsecret", "my-image-pull-secret", &api.JobRequest{ + Compose: api.Compose{ + Containers: []api.Container{ + { + Name: "main", + Image: "custom-image", + }, + { + Name: "db", + Image: "postgres", + }, + { + Name: "cache", + Image: "redis", + }, + }, + }, + })) + + pod, err := clientset.CoreV1(). + Pods("default"). + Get(context.Background(), podName, v1.GetOptions{}) + + assert.NoError(t, err) + if assert.Len(t, pod.Spec.Containers, 3) { + assert.Equal(t, pod.Spec.Containers[0].Resources, corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("200Mi"), + corev1.ResourceCPU: resource.MustParse("200m"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("100Mi"), + corev1.ResourceCPU: resource.MustParse("100m"), + }, + }) + + assert.Equal(t, pod.Spec.Containers[1].Resources, corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("100Mi"), + corev1.ResourceCPU: resource.MustParse("100m"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("50Mi"), + corev1.ResourceCPU: resource.MustParse("50m"), + }, + }) + + assert.Equal(t, pod.Spec.Containers[2].Resources, corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("100Mi"), + corev1.ResourceCPU: resource.MustParse("100m"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("50Mi"), + corev1.ResourceCPU: resource.MustParse("50m"), + }, + }) + } + }) } func Test__WaitForPod(t *testing.T) { @@ -413,7 +561,7 @@ func Test__WaitForPod(t *testing.T) { }, }) - client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", DefaultImage: "default-image"}) + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default"}) assert.NoError(t, client.WaitForPod(context.TODO(), podName, func(s string) {})) }) @@ -430,7 +578,6 @@ func Test__WaitForPod(t *testing.T) { client, _ := NewKubernetesClient(clientset, Config{ Namespace: "default", - DefaultImage: "default-image", PodPollingAttempts: 2, }) @@ -450,7 +597,6 @@ func Test__WaitForPod(t *testing.T) { client, _ := NewKubernetesClient(clientset, Config{ Namespace: "default", - DefaultImage: "default-image", PodPollingAttempts: 120, }) @@ -477,7 +623,7 @@ func Test_DeletePod(t *testing.T) { }, }) - client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", DefaultImage: "default-image"}) + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default"}) assert.NoError(t, client.DeletePod(podName)) // pod does not exist anymore @@ -498,7 +644,7 @@ func Test_DeleteSecret(t *testing.T) { }, }) - client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", DefaultImage: "default-image"}) + client, _ := NewKubernetesClient(clientset, Config{Namespace: "default"}) assert.NoError(t, client.DeleteSecret(secretName)) // secret does not exist anymore @@ -512,3 +658,70 @@ func Test_DeleteSecret(t *testing.T) { func newFakeClientset(objects []runtime.Object) kubernetes.Interface { return fake.NewSimpleClientset(objects...) } + +func podSpecWithEnv(namespace string) *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: v1.ObjectMeta{Name: "pod-spec", Namespace: namespace}, + Data: map[string]string{ + "mainContainer": ` + env: + - name: FOO_1 + value: BAR_1 + - name: FOO_2 + value: BAR_2 + `, + }, + } +} + +func podSpecWithImagePullPolicy(namespace, pullPolicy string) *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: v1.ObjectMeta{Name: "pod-spec", Namespace: namespace}, + Data: map[string]string{ + "mainContainer": fmt.Sprintf(` + imagePullPolicy: %s + `, pullPolicy), + }, + } +} + +func podSpecWithImagePullSecret(namespace, secretName string) *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: v1.ObjectMeta{Name: "pod-spec", Namespace: namespace}, + Data: map[string]string{ + "mainContainer": ` + imagePullPolicy: Never + `, + "pod": fmt.Sprintf(` + imagePullSecrets: + - name: %s + `, secretName), + }, + } +} + +func podSpecWithResources(namespace string) *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: v1.ObjectMeta{Name: "pod-spec", Namespace: namespace}, + Data: map[string]string{ + "mainContainer": ` + resources: + limits: + cpu: 200m + memory: 200Mi + requests: + cpu: 100m + memory: 100Mi + `, + "sidecarContainers": ` + resources: + limits: + cpu: 100m + memory: 100Mi + requests: + cpu: 50m + memory: 50Mi + `, + }, + } +} diff --git a/pkg/listener/job_processor.go b/pkg/listener/job_processor.go index cbcae4ea..ad87fe1d 100644 --- a/pkg/listener/job_processor.go +++ b/pkg/listener/job_processor.go @@ -39,9 +39,7 @@ func StartJobProcessor(httpClient *http.Client, apiClient *selfhostedapi.API, co FailOnPreJobHookError: config.FailOnPreJobHookError, ExitOnShutdown: config.ExitOnShutdown, KubernetesExecutor: config.KubernetesExecutor, - KubernetesDefaultImage: config.KubernetesDefaultImage, - KubernetesImagePullPolicy: config.KubernetesImagePullPolicy, - KubernetesImagePullSecrets: config.KubernetesImagePullSecrets, + KubernetesPodSpec: config.KubernetesPodSpec, KubernetesPodStartTimeoutSeconds: config.KubernetesPodStartTimeoutSeconds, } @@ -81,9 +79,7 @@ type JobProcessor struct { FailOnPreJobHookError bool ExitOnShutdown bool KubernetesExecutor bool - KubernetesDefaultImage string - KubernetesImagePullPolicy string - KubernetesImagePullSecrets []string + KubernetesPodSpec string KubernetesPodStartTimeoutSeconds int } @@ -178,9 +174,7 @@ func (p *JobProcessor) RunJob(jobID string) { FailOnMissingFiles: p.FailOnMissingFiles, SelfHosted: true, UseKubernetesExecutor: p.KubernetesExecutor, - KubernetesDefaultImage: p.KubernetesDefaultImage, - KubernetesImagePullPolicy: p.KubernetesImagePullPolicy, - KubernetesImagePullSecrets: p.KubernetesImagePullSecrets, + PodSpecDecoratorConfigMap: p.KubernetesPodSpec, KubernetesPodStartTimeoutSeconds: p.KubernetesPodStartTimeoutSeconds, UploadJobLogs: p.UploadJobLogs, RefreshTokenFn: func() (string, error) { diff --git a/pkg/listener/listener.go b/pkg/listener/listener.go index 9a008e9d..5ad4e836 100644 --- a/pkg/listener/listener.go +++ b/pkg/listener/listener.go @@ -40,9 +40,7 @@ type Config struct { AgentVersion string AgentName string KubernetesExecutor bool - KubernetesDefaultImage string - KubernetesImagePullPolicy string - KubernetesImagePullSecrets []string + KubernetesPodSpec string KubernetesPodStartTimeoutSeconds int } diff --git a/test/e2e/kubernetes/docker_compose__env-vars.rb b/test/e2e/kubernetes/docker_compose__env-vars.rb index d2c11aa7..f98bc247 100644 --- a/test/e2e/kubernetes/docker_compose__env-vars.rb +++ b/test/e2e/kubernetes/docker_compose__env-vars.rb @@ -10,8 +10,7 @@ "env-vars" => [], "files" => [], "fail-on-missing-files" => false, - "kubernetes-executor" => true, - "kubernetes-image-pull-policy" => "IfNotPresent" + "kubernetes-executor" => true } require_relative '../../e2e' @@ -66,6 +65,9 @@ assert_job_log <<-LOG {"event":"job_started", "timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Creating Kubernetes resources for job..."} + {"event":"cmd_finished", "timestamp":"*", "directive":"Creating Kubernetes resources for job...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Starting shell session..."} *** LONG_OUTPUT *** {"event":"cmd_finished", "timestamp":"*", "directive":"Starting shell session...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} diff --git a/test/e2e/kubernetes/docker_compose__epilogue.rb b/test/e2e/kubernetes/docker_compose__epilogue.rb index f6772e6c..91c657cb 100644 --- a/test/e2e/kubernetes/docker_compose__epilogue.rb +++ b/test/e2e/kubernetes/docker_compose__epilogue.rb @@ -10,8 +10,7 @@ "env-vars" => [], "files" => [], "fail-on-missing-files" => false, - "kubernetes-executor" => true, - "kubernetes-image-pull-policy" => "IfNotPresent" + "kubernetes-executor" => true } require_relative '../../e2e' @@ -49,6 +48,9 @@ assert_job_log <<-LOG {"event":"job_started", "timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Creating Kubernetes resources for job..."} + {"event":"cmd_finished", "timestamp":"*", "directive":"Creating Kubernetes resources for job...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Starting shell session..."} *** LONG_OUTPUT *** {"event":"cmd_finished", "timestamp":"*", "directive":"Starting shell session...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} diff --git a/test/e2e/kubernetes/docker_compose__file-injection.rb b/test/e2e/kubernetes/docker_compose__file-injection.rb index c0522c26..21d2eee7 100644 --- a/test/e2e/kubernetes/docker_compose__file-injection.rb +++ b/test/e2e/kubernetes/docker_compose__file-injection.rb @@ -10,8 +10,7 @@ "env-vars" => [], "files" => [], "fail-on-missing-files" => false, - "kubernetes-executor" => true, - "kubernetes-image-pull-policy" => "IfNotPresent" + "kubernetes-executor" => true } require_relative '../../e2e' @@ -54,6 +53,9 @@ assert_job_log <<-LOG {"event":"job_started", "timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Creating Kubernetes resources for job..."} + {"event":"cmd_finished", "timestamp":"*", "directive":"Creating Kubernetes resources for job...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Starting shell session..."} *** LONG_OUTPUT *** {"event":"cmd_finished", "timestamp":"*", "directive":"Starting shell session...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} diff --git a/test/e2e/kubernetes/docker_compose__multiple-containers.rb b/test/e2e/kubernetes/docker_compose__multiple-containers.rb index e3407a7b..a63d0dce 100644 --- a/test/e2e/kubernetes/docker_compose__multiple-containers.rb +++ b/test/e2e/kubernetes/docker_compose__multiple-containers.rb @@ -10,8 +10,7 @@ "env-vars" => [], "files" => [], "fail-on-missing-files" => false, - "kubernetes-executor" => true, - "kubernetes-image-pull-policy" => "IfNotPresent" + "kubernetes-executor" => true } require_relative '../../e2e' @@ -58,6 +57,9 @@ assert_job_log <<-LOG {"event":"job_started", "timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Creating Kubernetes resources for job..."} + {"event":"cmd_finished", "timestamp":"*", "directive":"Creating Kubernetes resources for job...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Starting shell session..."} *** LONG_OUTPUT *** {"event":"cmd_finished", "timestamp":"*", "directive":"Starting shell session...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} diff --git a/test/e2e/kubernetes/private_image_ecr_no_account_id.rb b/test/e2e/kubernetes/private_image_ecr_no_account_id.rb index 13a96e66..bce51cc6 100644 --- a/test/e2e/kubernetes/private_image_ecr_no_account_id.rb +++ b/test/e2e/kubernetes/private_image_ecr_no_account_id.rb @@ -10,8 +10,7 @@ "env-vars" => [], "files" => [], "fail-on-missing-files" => false, - "kubernetes-executor" => true, - "kubernetes-image-pull-policy" => "IfNotPresent" + "kubernetes-executor" => true } require_relative '../../e2e' @@ -65,6 +64,9 @@ assert_job_log <<-LOG {"event":"job_started", "timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Creating Kubernetes resources for job..."} + {"event":"cmd_finished", "timestamp":"*", "directive":"Creating Kubernetes resources for job...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Starting shell session..."} *** LONG_OUTPUT *** {"event":"cmd_finished", "timestamp":"*", "directive":"Starting shell session...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} diff --git a/test/e2e/kubernetes/private_image_ecr_with_account_id.rb b/test/e2e/kubernetes/private_image_ecr_with_account_id.rb index 2fc19ec7..079efe68 100644 --- a/test/e2e/kubernetes/private_image_ecr_with_account_id.rb +++ b/test/e2e/kubernetes/private_image_ecr_with_account_id.rb @@ -10,8 +10,7 @@ "env-vars" => [], "files" => [], "fail-on-missing-files" => false, - "kubernetes-executor" => true, - "kubernetes-image-pull-policy" => "IfNotPresent" + "kubernetes-executor" => true } require_relative '../../e2e' @@ -68,6 +67,9 @@ assert_job_log <<-LOG {"event":"job_started", "timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Creating Kubernetes resources for job..."} + {"event":"cmd_finished", "timestamp":"*", "directive":"Creating Kubernetes resources for job...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Starting shell session..."} *** LONG_OUTPUT *** {"event":"cmd_finished", "timestamp":"*", "directive":"Starting shell session...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} diff --git a/test/e2e/kubernetes/private_image_gcr.rb b/test/e2e/kubernetes/private_image_gcr.rb index f2ae4349..fac0a708 100644 --- a/test/e2e/kubernetes/private_image_gcr.rb +++ b/test/e2e/kubernetes/private_image_gcr.rb @@ -10,8 +10,7 @@ "env-vars" => [], "files" => [], "fail-on-missing-files" => false, - "kubernetes-executor" => true, - "kubernetes-image-pull-policy" => "IfNotPresent" + "kubernetes-executor" => true } require_relative '../../e2e' @@ -66,6 +65,9 @@ assert_job_log <<-LOG {"event":"job_started","timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Creating Kubernetes resources for job..."} + {"event":"cmd_finished", "timestamp":"*", "directive":"Creating Kubernetes resources for job...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Starting shell session..."} *** LONG_OUTPUT *** {"event":"cmd_finished", "timestamp":"*", "directive":"Starting shell session...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} diff --git a/test/e2e/kubernetes/private_image_generic.rb b/test/e2e/kubernetes/private_image_generic.rb index a2f6594e..77478367 100644 --- a/test/e2e/kubernetes/private_image_generic.rb +++ b/test/e2e/kubernetes/private_image_generic.rb @@ -10,8 +10,7 @@ "env-vars" => [], "files" => [], "fail-on-missing-files" => false, - "kubernetes-executor" => true, - "kubernetes-image-pull-policy" => "IfNotPresent" + "kubernetes-executor" => true } require_relative '../../e2e' @@ -65,6 +64,9 @@ assert_job_log <<-LOG {"event":"job_started", "timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Creating Kubernetes resources for job..."} + {"event":"cmd_finished", "timestamp":"*", "directive":"Creating Kubernetes resources for job...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} + {"event":"cmd_started", "timestamp":"*", "directive":"Starting shell session..."} *** LONG_OUTPUT *** {"event":"cmd_finished", "timestamp":"*", "directive":"Starting shell session...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} diff --git a/test/e2e/kubernetes/shell__env-vars.rb b/test/e2e/kubernetes/shell__env-vars.rb deleted file mode 100644 index f2a62c7d..00000000 --- a/test/e2e/kubernetes/shell__env-vars.rb +++ /dev/null @@ -1,90 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -$AGENT_CONFIG = { - "endpoint" => "localhost:4567", - "token" => "321h1l2jkh1jk42341", - "no-https" => true, - "shutdown-hook-path" => "", - "disconnect-after-job" => false, - "env-vars" => [], - "files" => [], - "fail-on-missing-files" => false, - "kubernetes-executor" => true, - "kubernetes-default-image" => "ruby:3-slim", - "kubernetes-image-pull-policy" => "IfNotPresent" -} - -require_relative '../../e2e' - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - "executor": "shell", - "env_vars": [ - { "name": "A", "value": "#{`echo "hello" | base64 | tr -d '\n'`}" }, - { "name": "B", "value": "#{`echo "how are you?" | base64 | tr -d '\n'`}" }, - { "name": "C", "value": "#{`echo "quotes ' quotes" | base64 | tr -d '\n'`}" }, - { "name": "D", "value": "#{`echo '$PATH:/etc/a' | base64 | tr -d '\n'`}" } - ], - - "files": [], - - "commands": [ - { "directive": "echo $A" }, - { "directive": "echo $B" }, - { "directive": "echo $C" }, - { "directive": "echo $D" } - ], - - "epilogue_always_commands": [], - - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_job_to_finish - -assert_job_log <<-LOG - {"event":"job_started", "timestamp":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Starting shell session..."} - *** LONG_OUTPUT *** - {"event":"cmd_finished", "timestamp":"*", "directive":"Starting shell session...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting A\\n"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting B\\n"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting C\\n"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting D\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"echo $A"} - {"event":"cmd_output", "timestamp":"*", "output":"hello\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo $A","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"echo $B"} - {"event":"cmd_output", "timestamp":"*", "output":"how are you?\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo $B","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"echo $C"} - {"event":"cmd_output", "timestamp":"*", "output":"quotes ' quotes\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo $C","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"echo $D"} - {"event":"cmd_output", "timestamp":"*", "output":"$PATH:/etc/a\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo $D","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} - - {"event":"job_finished", "timestamp":"*", "result":"passed"} -LOG diff --git a/test/e2e/kubernetes/shell__epilogue.rb b/test/e2e/kubernetes/shell__epilogue.rb deleted file mode 100644 index f20af36d..00000000 --- a/test/e2e/kubernetes/shell__epilogue.rb +++ /dev/null @@ -1,67 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -$AGENT_CONFIG = { - "endpoint" => "localhost:4567", - "token" => "321h1l2jkh1jk42341", - "no-https" => true, - "shutdown-hook-path" => "", - "disconnect-after-job" => false, - "env-vars" => [], - "files" => [], - "fail-on-missing-files" => false, - "kubernetes-executor" => true, - "kubernetes-default-image" => "ruby:3-slim", - "kubernetes-image-pull-policy" => "IfNotPresent" -} - -require_relative '../../e2e' - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - "executor": "shell", - "env_vars": [], - "files": [], - "commands": [ - { "directive": "echo Hello World" } - ], - "epilogue_always_commands": [ - { "directive": "echo Hello Epilogue $SEMAPHORE_JOB_RESULT" } - ], - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_job_to_finish - -assert_job_log <<-LOG - {"event":"job_started", "timestamp":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Starting shell session..."} - *** LONG_OUTPUT *** - {"event":"cmd_finished", "timestamp":"*", "directive":"Starting shell session...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} - {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello World"} - {"event":"cmd_output", "timestamp":"*", "output":"Hello World\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello World","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"echo Hello Epilogue $SEMAPHORE_JOB_RESULT"} - {"event":"cmd_output", "timestamp":"*", "output":"Hello Epilogue passed\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"echo Hello Epilogue $SEMAPHORE_JOB_RESULT","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"job_finished", "timestamp":"*", "result":"passed"} -LOG diff --git a/test/e2e/kubernetes/shell__file-injection.rb b/test/e2e/kubernetes/shell__file-injection.rb deleted file mode 100644 index 330e0c67..00000000 --- a/test/e2e/kubernetes/shell__file-injection.rb +++ /dev/null @@ -1,80 +0,0 @@ -#!/bin/ruby -# rubocop:disable all - -$AGENT_CONFIG = { - "endpoint" => "localhost:4567", - "token" => "321h1l2jkh1jk42341", - "no-https" => true, - "shutdown-hook-path" => "", - "disconnect-after-job" => false, - "env-vars" => [], - "files" => [], - "fail-on-missing-files" => false, - "kubernetes-executor" => true, - "kubernetes-default-image" => "ruby:3-slim", - "kubernetes-image-pull-policy" => "IfNotPresent" -} - -require_relative '../../e2e' - -start_job <<-JSON - { - "id": "#{$JOB_ID}", - "executor": "shell", - "env_vars": [], - "files": [ - { "path": "test.txt", "content": "#{`echo "hello" | base64 | tr -d '\n'`}", "mode": "0644" }, - { "path": "/a/b/c", "content": "#{`echo "hello" | base64 | tr -d '\n'`}", "mode": "0644" }, - { "path": "/tmp/a", "content": "#{`echo "hello" | base64 | tr -d '\n'`}", "mode": "0600" } - ], - "commands": [ - { "directive": "cat test.txt" }, - { "directive": "cat /a/b/c" }, - { "directive": "stat -c '%a' /tmp/a" } - ], - - "epilogue_always_commands": [], - "callbacks": { - "finished": "#{finished_callback_url}", - "teardown_finished": "#{teardown_callback_url}" - }, - "logger": #{$LOGGER} - } -JSON - -wait_for_job_to_finish - -assert_job_log <<-LOG - {"event":"job_started", "timestamp":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Starting shell session..."} - *** LONG_OUTPUT *** - {"event":"cmd_finished", "timestamp":"*", "directive":"Starting shell session...","event":"cmd_finished","exit_code":0,"finished_at":"*","started_at":"*","timestamp":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Injecting Files"} - {"event":"cmd_output", "timestamp":"*", "output":"Injecting test.txt with file mode 0644\\n"} - {"event":"cmd_output", "timestamp":"*", "output":"Injecting /a/b/c with file mode 0644\\n"} - {"event":"cmd_output", "timestamp":"*", "output":"Injecting /tmp/a with file mode 0600\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Injecting Files","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"cat test.txt"} - {"event":"cmd_output", "timestamp":"*", "output":"hello\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"cat test.txt","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"cat /a/b/c"} - {"event":"cmd_output", "timestamp":"*", "output":"hello\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"cat /a/b/c","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"stat -c '%a' /tmp/a"} - {"event":"cmd_output", "timestamp":"*", "output":"600\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"stat -c '%a' /tmp/a","exit_code":0,"finished_at":"*","started_at":"*"} - - {"event":"cmd_started", "timestamp":"*", "directive":"Exporting environment variables"} - {"event":"cmd_output", "timestamp":"*", "output":"Exporting SEMAPHORE_JOB_RESULT\\n"} - {"event":"cmd_finished", "timestamp":"*", "directive":"Exporting environment variables","exit_code":0,"started_at":"*","finished_at":"*"} - - {"event":"job_finished", "timestamp":"*", "result":"passed"} -LOG diff --git a/test/e2e/kubernetes/shell__not-allowed.rb b/test/e2e/kubernetes/shell__not-allowed.rb new file mode 100644 index 00000000..59d40a23 --- /dev/null +++ b/test/e2e/kubernetes/shell__not-allowed.rb @@ -0,0 +1,48 @@ +#!/bin/ruby +# rubocop:disable all + +$AGENT_CONFIG = { + "endpoint" => "localhost:4567", + "token" => "321h1l2jkh1jk42341", + "no-https" => true, + "shutdown-hook-path" => "", + "disconnect-after-job" => false, + "env-vars" => [], + "files" => [], + "fail-on-missing-files" => false, + "kubernetes-executor" => true +} + +require_relative '../../e2e' + +start_job <<-JSON + { + "id": "#{$JOB_ID}", + "executor": "shell", + "env_vars": [], + "files": [], + "commands": [ + { "directive": "echo hello" } + ], + + "epilogue_always_commands": [], + + "callbacks": { + "finished": "#{finished_callback_url}", + "teardown_finished": "#{teardown_callback_url}" + }, + "logger": #{$LOGGER} + } +JSON + +wait_for_job_to_finish + +assert_job_log <<-LOG + {"event":"job_started", "timestamp":"*"} + + {"event":"cmd_started", "timestamp":"*", "directive":"Creating Kubernetes resources for job..."} + {"event":"cmd_output", "timestamp":"*", "output":"Failed to create pod: error building pod spec: error building containers for pod spec: no containers specified in Semaphore YAML\\n"} + {"event":"cmd_finished", "timestamp":"*", "directive":"Creating Kubernetes resources for job...","event":"cmd_finished","exit_code":1,"finished_at":"*","started_at":"*","timestamp":"*"} + + {"event":"job_finished", "timestamp":"*", "result":"failed"} +LOG From 95500df14ad9fecbfb7330b560cf31c1c92ee964 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Wed, 29 Mar 2023 19:01:22 -0300 Subject: [PATCH 061/130] feat: add --kubernetes-allowed-images (#193) --- main.go | 14 +++++- pkg/config/config.go | 2 + pkg/jobs/job.go | 2 + pkg/kubernetes/client.go | 5 ++ pkg/kubernetes/client_test.go | 64 ++++++++++++++++++++++---- pkg/kubernetes/image_validator.go | 50 ++++++++++++++++++++ pkg/kubernetes/image_validator_test.go | 51 ++++++++++++++++++++ pkg/listener/job_processor.go | 4 ++ pkg/listener/listener.go | 2 + 9 files changed, 185 insertions(+), 9 deletions(-) create mode 100644 pkg/kubernetes/image_validator.go create mode 100644 pkg/kubernetes/image_validator_test.go diff --git a/main.go b/main.go index 3bc9067c..1d537ac2 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ import ( "github.com/semaphoreci/agent/pkg/config" "github.com/semaphoreci/agent/pkg/eventlogger" jobs "github.com/semaphoreci/agent/pkg/jobs" + "github.com/semaphoreci/agent/pkg/kubernetes" listener "github.com/semaphoreci/agent/pkg/listener" server "github.com/semaphoreci/agent/pkg/server" slices "github.com/semaphoreci/agent/pkg/slices" @@ -123,7 +124,8 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { _ = pflag.String(config.UploadJobLogs, config.UploadJobLogsConditionNever, "When should the agent upload the job logs as a job artifact. Default is never.") _ = pflag.Bool(config.FailOnPreJobHookError, false, "Fail job if pre-job hook fails") _ = pflag.Bool(config.KubernetesExecutor, false, "Use Kubernetes executor") - _ = pflag.String(config.KubernetesPodSpec, "", "Use a Kubernetes configmap to decorate the pod created to run the Semaphore job.") + _ = pflag.String(config.KubernetesPodSpec, "", "Use a Kubernetes configmap to decorate the pod created to run the Semaphore job") + _ = pflag.StringSlice(config.KubernetesAllowedImages, []string{}, "List of regexes for allowed images to use for the Kubernetes executor") _ = pflag.Int( config.KubernetesPodStartTimeout, config.DefaultKubernetesPodStartTimeout, @@ -200,6 +202,7 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { ExitOnShutdown: true, KubernetesExecutor: viper.GetBool(config.KubernetesExecutor), KubernetesPodSpec: viper.GetString(config.KubernetesPodSpec), + KubernetesImageValidator: createImageValidator(viper.GetStringSlice(config.KubernetesAllowedImages)), KubernetesPodStartTimeoutSeconds: viper.GetInt(config.KubernetesPodStartTimeout), } @@ -213,6 +216,15 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { select {} } +func createImageValidator(expressions []string) *kubernetes.ImageValidator { + imageValidator, err := kubernetes.NewImageValidator(expressions) + if err != nil { + log.Panicf("Error creating image validator: %v", err) + } + + return imageValidator +} + func loadConfigFile(configFile string) { viper.SetConfigFile(configFile) if err := viper.ReadInConfig(); err != nil { diff --git a/pkg/config/config.go b/pkg/config/config.go index 49f32567..e82b1094 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -21,6 +21,7 @@ const ( InterruptionGracePeriod = "interruption-grace-period" KubernetesExecutor = "kubernetes-executor" KubernetesPodSpec = "kubernetes-pod-spec" + KubernetesAllowedImages = "kubernetes-allowed-images" KubernetesPodStartTimeout = "kubernetes-pod-start-timeout" ) @@ -73,6 +74,7 @@ var ValidConfigKeys = []string{ InterruptionGracePeriod, KubernetesExecutor, KubernetesPodSpec, + KubernetesAllowedImages, KubernetesPodStartTimeout, } diff --git a/pkg/jobs/job.go b/pkg/jobs/job.go index 7c9be040..e6d7f05d 100644 --- a/pkg/jobs/job.go +++ b/pkg/jobs/job.go @@ -49,6 +49,7 @@ type JobOptions struct { UseKubernetesExecutor bool PodSpecDecoratorConfigMap string KubernetesPodStartTimeoutSeconds int + KubernetesImageValidator *kubernetes.ImageValidator UploadJobLogs string RefreshTokenFn func() (string, error) } @@ -119,6 +120,7 @@ func CreateExecutor(request *api.JobRequest, logger *eventlogger.Logger, jobOpti return executors.NewKubernetesExecutor(request, logger, kubernetes.Config{ Namespace: namespace, + ImageValidator: jobOptions.KubernetesImageValidator, PodSpecDecoratorConfigMap: jobOptions.PodSpecDecoratorConfigMap, PodPollingAttempts: jobOptions.KubernetesPodStartTimeoutSeconds, PodPollingInterval: time.Second, diff --git a/pkg/kubernetes/client.go b/pkg/kubernetes/client.go index 39c17bdf..74cc49fd 100644 --- a/pkg/kubernetes/client.go +++ b/pkg/kubernetes/client.go @@ -26,6 +26,7 @@ import ( type Config struct { Namespace string + ImageValidator *ImageValidator PodSpecDecoratorConfigMap string PodPollingAttempts int PodPollingInterval time.Duration @@ -340,6 +341,10 @@ func (c *KubernetesClient) imagePullSecrets(imagePullSecret string) []corev1.Loc func (c *KubernetesClient) containers(apiContainers []api.Container) ([]corev1.Container, error) { if len(apiContainers) > 0 { + if err := c.config.ImageValidator.Validate(apiContainers); err != nil { + return []corev1.Container{}, fmt.Errorf("error validating images: %v", err) + } + return c.convertContainersFromSemaphore(apiContainers), nil } diff --git a/pkg/kubernetes/client_test.go b/pkg/kubernetes/client_test.go index 74a46587..af81dd50 100644 --- a/pkg/kubernetes/client_test.go +++ b/pkg/kubernetes/client_test.go @@ -136,7 +136,12 @@ func Test__CreateImagePullSecret(t *testing.T) { func Test__CreatePod(t *testing.T) { t.Run("no containers from YAML -> error", func(t *testing.T) { clientset := newFakeClientset([]runtime.Object{}) - client, _ := NewKubernetesClient(clientset, Config{Namespace: "default"}) + imageValidator, _ := NewImageValidator([]string{}) + client, _ := NewKubernetesClient(clientset, Config{ + Namespace: "default", + ImageValidator: imageValidator, + }) + _ = client.LoadPodSpec() podName := "mypod" envSecretName := "mysecret" @@ -150,7 +155,12 @@ func Test__CreatePod(t *testing.T) { t.Run("containers and no pod spec", func(t *testing.T) { clientset := newFakeClientset([]runtime.Object{}) - client, err := NewKubernetesClient(clientset, Config{Namespace: "default"}) + imageValidator, _ := NewImageValidator([]string{}) + client, err := NewKubernetesClient(clientset, Config{ + Namespace: "default", + ImageValidator: imageValidator, + }) + if !assert.NoError(t, err) { return } @@ -186,7 +196,13 @@ func Test__CreatePod(t *testing.T) { podSpecWithImagePullPolicy("default", "Always"), }) - client, err := NewKubernetesClient(clientset, Config{Namespace: "default", PodSpecDecoratorConfigMap: "pod-spec"}) + imageValidator, _ := NewImageValidator([]string{}) + client, err := NewKubernetesClient(clientset, Config{ + Namespace: "default", + PodSpecDecoratorConfigMap: "pod-spec", + ImageValidator: imageValidator, + }) + if !assert.NoError(t, err) { return } @@ -226,7 +242,12 @@ func Test__CreatePod(t *testing.T) { t.Run("container with env vars", func(t *testing.T) { clientset := newFakeClientset([]runtime.Object{}) - client, _ := NewKubernetesClient(clientset, Config{Namespace: "default"}) + imageValidator, _ := NewImageValidator([]string{}) + client, _ := NewKubernetesClient(clientset, Config{ + Namespace: "default", + ImageValidator: imageValidator, + }) + _ = client.LoadPodSpec() podName := "mypod" envSecretName := "mysecret" @@ -266,7 +287,13 @@ func Test__CreatePod(t *testing.T) { t.Run("container with env vars + pod spec with env", func(t *testing.T) { clientset := newFakeClientset([]runtime.Object{podSpecWithEnv("default")}) - client, _ := NewKubernetesClient(clientset, Config{Namespace: "default", PodSpecDecoratorConfigMap: "pod-spec"}) + imageValidator, _ := NewImageValidator([]string{}) + client, _ := NewKubernetesClient(clientset, Config{ + Namespace: "default", + PodSpecDecoratorConfigMap: "pod-spec", + ImageValidator: imageValidator, + }) + _ = client.LoadPodSpec() podName := "mypod" envSecretName := "mysecret" @@ -310,7 +337,12 @@ func Test__CreatePod(t *testing.T) { t.Run("multiple containers", func(t *testing.T) { clientset := newFakeClientset([]runtime.Object{}) - client, _ := NewKubernetesClient(clientset, Config{Namespace: "default"}) + imageValidator, _ := NewImageValidator([]string{}) + client, _ := NewKubernetesClient(clientset, Config{ + Namespace: "default", + ImageValidator: imageValidator, + }) + _ = client.LoadPodSpec() podName := "mypod" envSecretName := "mysecret" @@ -354,7 +386,12 @@ func Test__CreatePod(t *testing.T) { t.Run("no image pull secrets", func(t *testing.T) { clientset := newFakeClientset([]runtime.Object{}) - client, _ := NewKubernetesClient(clientset, Config{Namespace: "default"}) + imageValidator, _ := NewImageValidator([]string{}) + client, _ := NewKubernetesClient(clientset, Config{ + Namespace: "default", + ImageValidator: imageValidator, + }) + _ = client.LoadPodSpec() podName := "mypod" @@ -383,9 +420,11 @@ func Test__CreatePod(t *testing.T) { podSpecWithImagePullSecret("default", "secret-1"), }) + imageValidator, _ := NewImageValidator([]string{}) client, err := NewKubernetesClient(clientset, Config{ Namespace: "default", PodSpecDecoratorConfigMap: "pod-spec", + ImageValidator: imageValidator, }) if !assert.NoError(t, err) { @@ -417,7 +456,12 @@ func Test__CreatePod(t *testing.T) { t.Run("with image pull secret from YAML", func(t *testing.T) { clientset := newFakeClientset([]runtime.Object{}) - client, _ := NewKubernetesClient(clientset, Config{Namespace: "default"}) + imageValidator, _ := NewImageValidator([]string{}) + client, _ := NewKubernetesClient(clientset, Config{ + Namespace: "default", + ImageValidator: imageValidator, + }) + _ = client.LoadPodSpec() podName := "mypod" @@ -446,9 +490,11 @@ func Test__CreatePod(t *testing.T) { podSpecWithImagePullSecret("default", "secret-1"), }) + imageValidator, _ := NewImageValidator([]string{}) client, _ := NewKubernetesClient(clientset, Config{ Namespace: "default", PodSpecDecoratorConfigMap: "pod-spec", + ImageValidator: imageValidator, }) _ = client.LoadPodSpec() @@ -479,9 +525,11 @@ func Test__CreatePod(t *testing.T) { t.Run("with resources", func(t *testing.T) { clientset := newFakeClientset([]runtime.Object{podSpecWithResources("default")}) + imageValidator, _ := NewImageValidator([]string{}) client, _ := NewKubernetesClient(clientset, Config{ Namespace: "default", PodSpecDecoratorConfigMap: "pod-spec", + ImageValidator: imageValidator, }) _ = client.LoadPodSpec() diff --git a/pkg/kubernetes/image_validator.go b/pkg/kubernetes/image_validator.go new file mode 100644 index 00000000..1574a241 --- /dev/null +++ b/pkg/kubernetes/image_validator.go @@ -0,0 +1,50 @@ +package kubernetes + +import ( + "fmt" + "regexp" + + "github.com/semaphoreci/agent/pkg/api" +) + +type ImageValidator struct { + Expressions []regexp.Regexp +} + +func NewImageValidator(expressions []string) (*ImageValidator, error) { + regexes := []regexp.Regexp{} + for _, exp := range expressions { + r, err := regexp.Compile(exp) + if err != nil { + return nil, err + } + + regexes = append(regexes, *r) + } + + return &ImageValidator{Expressions: regexes}, nil +} + +func (v *ImageValidator) Validate(containers []api.Container) error { + for _, container := range containers { + if err := v.validateImage(container.Image); err != nil { + return err + } + } + + return nil +} + +func (v *ImageValidator) validateImage(image string) error { + if len(v.Expressions) == 0 { + return nil + } + + for _, expression := range v.Expressions { + if expression.MatchString(image) { + return nil + } + } + + return fmt.Errorf("image '%s' is not allowed", image) +} diff --git a/pkg/kubernetes/image_validator_test.go b/pkg/kubernetes/image_validator_test.go new file mode 100644 index 00000000..081d2df0 --- /dev/null +++ b/pkg/kubernetes/image_validator_test.go @@ -0,0 +1,51 @@ +package kubernetes + +import ( + "testing" + + "github.com/semaphoreci/agent/pkg/api" + "github.com/stretchr/testify/assert" +) + +func Test__ImageValidator(t *testing.T) { + t.Run("bad expression => error creating validator", func(t *testing.T) { + _, err := NewImageValidator([]string{"(.*)\\((.*)\\) ?(? U)"}) + assert.Error(t, err) + }) + + t.Run("no expressions => no restrictions", func(t *testing.T) { + imageValidator, err := NewImageValidator([]string{}) + assert.NoError(t, err) + assert.NoError(t, imageValidator.Validate([]api.Container{ + {Image: "registry.semaphoreci.com/ruby:2.7"}, + {Image: "docker.io/redis"}, + {Image: "postgres/9.6"}, + })) + }) + + t.Run("single regex with all invalid images", func(t *testing.T) { + imageValidator, err := NewImageValidator([]string{ + "^custom-registry-1\\.com\\/.+", + }) + + assert.NoError(t, err) + assert.ErrorContains(t, imageValidator.Validate([]api.Container{ + {Image: "registry.semaphoreci.com/ruby:2.7"}, + {Image: "docker.io/redis"}, + {Image: "postgres/9.6"}, + }), "image 'registry.semaphoreci.com/ruby:2.7' is not allowed") + }) + + t.Run("single regex with some invalid images", func(t *testing.T) { + imageValidator, err := NewImageValidator([]string{ + "^registry\\.semaphoreci\\.com\\/.+", + }) + + assert.NoError(t, err) + assert.ErrorContains(t, imageValidator.Validate([]api.Container{ + {Image: "registry.semaphoreci.com/ruby:2.7"}, + {Image: "docker.io/redis"}, + {Image: "postgres/9.6"}, + }), "image 'docker.io/redis' is not allowed") + }) +} diff --git a/pkg/listener/job_processor.go b/pkg/listener/job_processor.go index ad87fe1d..e8a7d861 100644 --- a/pkg/listener/job_processor.go +++ b/pkg/listener/job_processor.go @@ -14,6 +14,7 @@ import ( "github.com/semaphoreci/agent/pkg/api" "github.com/semaphoreci/agent/pkg/config" jobs "github.com/semaphoreci/agent/pkg/jobs" + "github.com/semaphoreci/agent/pkg/kubernetes" selfhostedapi "github.com/semaphoreci/agent/pkg/listener/selfhostedapi" "github.com/semaphoreci/agent/pkg/random" "github.com/semaphoreci/agent/pkg/retry" @@ -40,6 +41,7 @@ func StartJobProcessor(httpClient *http.Client, apiClient *selfhostedapi.API, co ExitOnShutdown: config.ExitOnShutdown, KubernetesExecutor: config.KubernetesExecutor, KubernetesPodSpec: config.KubernetesPodSpec, + KubernetesImageValidator: config.KubernetesImageValidator, KubernetesPodStartTimeoutSeconds: config.KubernetesPodStartTimeoutSeconds, } @@ -80,6 +82,7 @@ type JobProcessor struct { ExitOnShutdown bool KubernetesExecutor bool KubernetesPodSpec string + KubernetesImageValidator *kubernetes.ImageValidator KubernetesPodStartTimeoutSeconds int } @@ -176,6 +179,7 @@ func (p *JobProcessor) RunJob(jobID string) { UseKubernetesExecutor: p.KubernetesExecutor, PodSpecDecoratorConfigMap: p.KubernetesPodSpec, KubernetesPodStartTimeoutSeconds: p.KubernetesPodStartTimeoutSeconds, + KubernetesImageValidator: p.KubernetesImageValidator, UploadJobLogs: p.UploadJobLogs, RefreshTokenFn: func() (string, error) { return p.APIClient.RefreshToken() diff --git a/pkg/listener/listener.go b/pkg/listener/listener.go index 5ad4e836..460be43d 100644 --- a/pkg/listener/listener.go +++ b/pkg/listener/listener.go @@ -7,6 +7,7 @@ import ( "time" "github.com/semaphoreci/agent/pkg/config" + "github.com/semaphoreci/agent/pkg/kubernetes" selfhostedapi "github.com/semaphoreci/agent/pkg/listener/selfhostedapi" osinfo "github.com/semaphoreci/agent/pkg/osinfo" "github.com/semaphoreci/agent/pkg/retry" @@ -41,6 +42,7 @@ type Config struct { AgentName string KubernetesExecutor bool KubernetesPodSpec string + KubernetesImageValidator *kubernetes.ImageValidator KubernetesPodStartTimeoutSeconds int } From 0ca88aa7c8eb3f0bf16c0325095ebd73d8324e94 Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Thu, 6 Apr 2023 11:16:12 -0300 Subject: [PATCH 062/130] fix: filebackend.Read() should not return double newlines (#194) --- pkg/eventlogger/filebackend.go | 2 +- pkg/eventlogger/filebackend_test.go | 43 +++++++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/pkg/eventlogger/filebackend.go b/pkg/eventlogger/filebackend.go index d3b2da83..c9d70af7 100644 --- a/pkg/eventlogger/filebackend.go +++ b/pkg/eventlogger/filebackend.go @@ -118,7 +118,7 @@ func (l *FileBackend) Read(startingLineNumber, maxLines int, writer io.Writer) ( // Otherwise, we advance to the next line and stream the current line. lineNumber++ - fmt.Fprintln(writer, line) + fmt.Fprint(writer, line) linesStreamed++ // if we have streamed the number of lines we want, we stop. diff --git a/pkg/eventlogger/filebackend_test.go b/pkg/eventlogger/filebackend_test.go index 1cb83e5c..917529d1 100644 --- a/pkg/eventlogger/filebackend_test.go +++ b/pkg/eventlogger/filebackend_test.go @@ -1,6 +1,7 @@ package eventlogger import ( + "bytes" "fmt" "io/ioutil" "os" @@ -9,7 +10,6 @@ import ( "testing" "time" - testsupport "github.com/semaphoreci/agent/test/support" "github.com/stretchr/testify/assert" ) @@ -43,7 +43,46 @@ func Test__LogsArePushedToFile(t *testing.T) { fmt.Sprintf(`{"event":"cmd_output","timestamp":%d,"output":"hello\n"}`, timestamp), fmt.Sprintf(`{"event":"cmd_finished","timestamp":%d,"directive":"echo hello","exit_code":0,"started_at":%d,"finished_at":%d}`, timestamp, timestamp, timestamp), fmt.Sprintf(`{"event":"job_finished","timestamp":%d,"result":"passed"}`, timestamp), - }, testsupport.FilterEmpty(logs)) + "", // newline at the end of the file + }, logs) + + err = fileBackend.Close() + assert.Nil(t, err) +} + +func Test__ReadDoesNotIncludeDoubleNewlines(t *testing.T) { + tmpFileName := filepath.Join(os.TempDir(), fmt.Sprintf("logs_%d.json", time.Now().UnixNano())) + fileBackend, err := NewFileBackend(tmpFileName, DefaultMaxSizeInBytes) + assert.Nil(t, err) + assert.Nil(t, fileBackend.Open()) + + timestamp := int(time.Now().Unix()) + assert.Nil(t, fileBackend.Write(&JobStartedEvent{Timestamp: timestamp, Event: "job_started"})) + assert.Nil(t, fileBackend.Write(&CommandStartedEvent{Timestamp: timestamp, Event: "cmd_started", Directive: "echo hello"})) + assert.Nil(t, fileBackend.Write(&CommandOutputEvent{Timestamp: timestamp, Event: "cmd_output", Output: "hello\n"})) + assert.Nil(t, fileBackend.Write(&CommandFinishedEvent{ + Timestamp: timestamp, + Event: "cmd_finished", + Directive: "echo hello", + ExitCode: 0, + StartedAt: timestamp, + FinishedAt: timestamp, + })) + assert.Nil(t, fileBackend.Write(&JobFinishedEvent{Timestamp: timestamp, Event: "job_finished", Result: "passed"})) + + w := new(bytes.Buffer) + _, err = fileBackend.Read(0, 1000, w) + assert.NoError(t, err) + logs := strings.Split(w.String(), "\n") + + assert.Equal(t, []string{ + fmt.Sprintf(`{"event":"job_started","timestamp":%d}`, timestamp), + fmt.Sprintf(`{"event":"cmd_started","timestamp":%d,"directive":"echo hello"}`, timestamp), + fmt.Sprintf(`{"event":"cmd_output","timestamp":%d,"output":"hello\n"}`, timestamp), + fmt.Sprintf(`{"event":"cmd_finished","timestamp":%d,"directive":"echo hello","exit_code":0,"started_at":%d,"finished_at":%d}`, timestamp, timestamp, timestamp), + fmt.Sprintf(`{"event":"job_finished","timestamp":%d,"result":"passed"}`, timestamp), + "", // newline at the end of the file + }, logs) err = fileBackend.Close() assert.Nil(t, err) From b7509bc7d38ac2d12ed3ced85362594b764070ed Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Wed, 12 Apr 2023 16:59:51 -0300 Subject: [PATCH 063/130] feat: public self-hosted agent Docker image (#189) --- .semaphore/semaphore.yml | 4 ++++ Dockerfile.self_hosted | 33 +++++++++++++++++++++++++++------ Makefile | 16 ++++++++++++---- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 1653cb27..b0093534 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -48,6 +48,10 @@ blocks: - name: Check code commands: - make check.static + - name: Check self-hosted image + commands: + - make docker.build + - make check.docker - name: "Tests" dependencies: [] diff --git a/Dockerfile.self_hosted b/Dockerfile.self_hosted index 987767f3..ef17690d 100644 --- a/Dockerfile.self_hosted +++ b/Dockerfile.self_hosted @@ -1,12 +1,33 @@ FROM ubuntu:20.04 -RUN apt-get update -qy -RUN apt-get install -y ca-certificates openssh-client +ARG AGENT_VERSION + +ARG USERNAME=semaphore +ARG USER_UID=1000 +ARG USER_GID=$USER_UID + +# Create the user +RUN groupadd --gid $USER_GID $USERNAME && \ + useradd --uid $USER_UID --gid $USER_GID -m $USERNAME + +RUN apt-get update -y && apt-get install -y ca-certificates curl RUN update-ca-certificates -ADD build/agent /app/agent -RUN chmod +x /app/agent +# kubectl is required to be present in the container running the agent +RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" +RUN install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl + +# Install Semaphore agent +RUN mkdir -p /opt/semaphore && \ + curl -L https://github.com/semaphoreci/agent/releases/download/${AGENT_VERSION}/agent_Linux_x86_64.tar.gz -o /opt/semaphore/agent.tar.gz && \ + tar -xf /opt/semaphore/agent.tar.gz -C /opt/semaphore && \ + rm /opt/semaphore/agent.tar.gz && \ + rm /opt/semaphore/README.md && \ + rm /opt/semaphore/install.sh && \ + chown ${USER_UID}:${USER_GID} /opt/semaphore -WORKDIR /app +USER $USERNAME +WORKDIR /home/semaphore +HEALTHCHECK NONE -CMD /app/agent start --endpoint $ENDPOINT --token $TOKEN +CMD ["/opt/semaphore/agent", "start", "--config-file", "/opt/semaphore/semaphore-agent.yml"] diff --git a/Makefile b/Makefile index b1ad2517..66c22c09 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,8 @@ AGENT_SSH_PORT_IN_TESTS=2222 SECURITY_TOOLBOX_BRANCH ?= master SECURITY_TOOLBOX_TMP_DIR ?= /tmp/security-toolbox +LATEST_VERSION=$(shell git tag | sort --version-sort | tail -n 1) + check.prepare: rm -rf $(SECURITY_TOOLBOX_TMP_DIR) git clone git@github.com:renderedtext/security-toolbox.git $(SECURITY_TOOLBOX_TMP_DIR) && (cd $(SECURITY_TOOLBOX_TMP_DIR) && git checkout $(SECURITY_TOOLBOX_BRANCH) && cd -) @@ -21,6 +23,13 @@ check.deps: check.prepare registry.semaphoreci.com/ruby:2.7 \ bash -c 'cd /app && $(SECURITY_TOOLBOX_TMP_DIR)/dependencies --language go -d' +check.docker: check.prepare + docker run -it -v $$(pwd):/app \ + -v $(SECURITY_TOOLBOX_TMP_DIR):$(SECURITY_TOOLBOX_TMP_DIR) \ + -v /var/run/docker.sock:/var/run/docker.sock \ + registry.semaphoreci.com/ruby:2.7 \ + bash -c 'cd /app && $(SECURITY_TOOLBOX_TMP_DIR)/docker -d --image semaphoreci/agent:latest --skip-files Dockerfile.ecr,Dockerfile.test,test/hub_reference/Dockerfile,Dockerfile.empty_ubuntu' + lint: revive -formatter friendly -config lint.toml ./... @@ -74,12 +83,11 @@ ecr.test.push: # Docker Release # docker.build: - $(MAKE) build - docker build -f Dockerfile.self_hosted -t semaphoreci/agent:latest . + docker build --build-arg AGENT_VERSION=$(LATEST_VERSION) -f Dockerfile.self_hosted -t semaphoreci/agent:latest . docker.push: - docker tag semaphoreci/agent:latest semaphoreci/agent:$$(git rev-parse HEAD) - docker push semaphoreci/agent:$$(git rev-parse HEAD) + docker tag semaphoreci/agent:latest semaphoreci/agent:$(LATEST_VERSION) + docker push semaphoreci/agent:$(LATEST_VERSION) docker push semaphoreci/agent:latest release.major: From 91bdeccd15991e35a5bc8772ac4199557dfc30da Mon Sep 17 00:00:00 2001 From: Lucas Pinheiro Date: Fri, 14 Apr 2023 11:45:12 -0300 Subject: [PATCH 064/130] feat: add --source-pre-job-hook parameter (#195) --- main.go | 2 ++ pkg/config/config.go | 2 ++ pkg/jobs/job.go | 11 +++++++++++ pkg/listener/job_processor.go | 3 +++ pkg/listener/listener.go | 1 + 5 files changed, 19 insertions(+) diff --git a/main.go b/main.go index 1d537ac2..fd184c34 100644 --- a/main.go +++ b/main.go @@ -123,6 +123,7 @@ func RunListener(httpClient *http.Client, logfile io.Writer) { _ = pflag.Bool(config.FailOnMissingFiles, false, "Fail job if files specified using --files are missing") _ = pflag.String(config.UploadJobLogs, config.UploadJobLogsConditionNever, "When should the agent upload the job logs as a job artifact. Default is never.") _ = pflag.Bool(config.FailOnPreJobHookError, false, "Fail job if pre-job hook fails") + _ = pflag.Bool(config.SourcePreJobHook, false, "Execute pre-job hook in the current shell (using 'source