From cc9fb0410cce56a856db249c33bd233e8d764ccc Mon Sep 17 00:00:00 2001 From: Marco Streich Date: Mon, 11 Jan 2021 11:03:19 +0100 Subject: [PATCH] Add first version of the deployment manager for testing --- deploymentagent/.dockerignore | 3 + deploymentagent/Makefile | 29 +++ deploymentagent/README.md | 161 ++++++++++++ deploymentagent/docker-compose.yml | 34 +++ .../src/deploymentagent/Dockerfile | 23 ++ .../src/deploymentagent/containers.go | 60 +++++ .../src/deploymentagent/containers_test.go | 33 +++ deploymentagent/src/deploymentagent/go.mod | 28 +++ deploymentagent/src/deploymentagent/go.sum | 205 ++++++++++++++++ deploymentagent/src/deploymentagent/main.go | 24 ++ deploymentagent/src/deploymentagent/server.go | 232 ++++++++++++++++++ .../src/deploymentagent/server_test.go | 64 +++++ deploymentagent/src/podman/Dockerfile | 8 + deploymentagent/src/podman/start.sh | 5 + 14 files changed, 909 insertions(+) create mode 100644 deploymentagent/.dockerignore create mode 100644 deploymentagent/Makefile create mode 100644 deploymentagent/README.md create mode 100644 deploymentagent/docker-compose.yml create mode 100644 deploymentagent/src/deploymentagent/Dockerfile create mode 100644 deploymentagent/src/deploymentagent/containers.go create mode 100644 deploymentagent/src/deploymentagent/containers_test.go create mode 100644 deploymentagent/src/deploymentagent/go.mod create mode 100644 deploymentagent/src/deploymentagent/go.sum create mode 100644 deploymentagent/src/deploymentagent/main.go create mode 100644 deploymentagent/src/deploymentagent/server.go create mode 100644 deploymentagent/src/deploymentagent/server_test.go create mode 100644 deploymentagent/src/podman/Dockerfile create mode 100644 deploymentagent/src/podman/start.sh diff --git a/deploymentagent/.dockerignore b/deploymentagent/.dockerignore new file mode 100644 index 0000000..f01ab13 --- /dev/null +++ b/deploymentagent/.dockerignore @@ -0,0 +1,3 @@ +*.sw* +Makefile +README.md diff --git a/deploymentagent/Makefile b/deploymentagent/Makefile new file mode 100644 index 0000000..7693fee --- /dev/null +++ b/deploymentagent/Makefile @@ -0,0 +1,29 @@ +IMAGE=deploymentagent +VERSION=latest +KEY_NAME=/path/to/private-key + +.DEFAULT_GOAL := help +.PHONY: build run clean test + +all: build + +build: ## Build the container + export SSH_KEY="$$(base64 $(KEY_NAME))" && \ + export VERSION=$(VERSION) && \ + export IMAGE=$(IMAGE) && \ + docker-compose build + +run: ## Launch testing environment + export SSH_KEY= && \ + export VERSION=$(VERSION) && \ + export IMAGE=$(IMAGE) && \ + docker-compose up + +clean: ## Remove containers and volumes + docker-compose down --remove-orphans && docker volume rm podman-socket + +test: ## Run unit tests (be sure to build and launch the testing environment first) + cd src/deploymentagent; go test -v + +help: + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/deploymentagent/README.md b/deploymentagent/README.md new file mode 100644 index 0000000..30203f8 --- /dev/null +++ b/deploymentagent/README.md @@ -0,0 +1,161 @@ +# Deployment API + +``` +. +|-- Makefile +|-- README.md +|-- docker-compose.yml +`-- src + |-- deploymentagent # Code and tests for the deployment agent + `-- podman # Podman inside of Docker replicating behavior on the live server for local testing +``` + +Accepts a deployment specification to produce a deployment state file. + +## Setup + +This project makes use of `gitlab.com/infektcommon/settings`, in order to be able to build it locally with Docker, you will need to provide the *path* to an ssh private key with access to Gitlab in the `Makefile`, like so: + +```Makefile +$ cat Makefile +IMAGE=deploymentagent +VERSION=latest +KEY_NAME=/home/user/.ssh/my-key +``` + +## Testing +### Launch the testing environment +```sh +make build +make run +``` +### Launch unit tests in a separate shell +```sh +make test +``` +### Cleanup after you are done +Removes dangling containers and volumes +```sh +make clean +``` + +## Data formats + +### Deployment specification + +```json +[ + { + "app" : "foo", + "version" : "latest" + }, + { + "app" : "bar", + "version" : "latest" + } +] +``` +If the same app is specified multiple times, the last entry in the list take precedence. + +### Deployment state + +```json +{ + "foo": "latest", + "bar": "latest", +} +``` +The deployment state file is intended to be read by Nix, which will produce a systemd unit file that configures the Podman container. + +## Commands + +```sh +curl -u'testuser:testpass' -i -XPOST localhost:8080/deploy -d '[{"app":"alpine", "version": "3.12"}]' +{"Message": "Queued for deployment", "DeploymentId": "1609855921147639"} +``` + +```sh +curl -u'testuser:testpass' -XGET localhost:8080/status | json_pp +``` +```json +[ + { + "Id": 1609855921147639, + "Status": "queued", + "StatusMessage": "Queued for deployment", + "DeploymentSpec": [ + { + "app": "alpine", + "version": "3.12" + } + ], + "DeploymentState": null + } +] +``` +.. deployment in progress .. +```sh +curl -u'testuser:testpass' -XGET localhost:8080/status | json_pp +``` +```json +[ + { + "Id": 1610356267147639, + "Status": "deployed", + "StatusMessage": "Deployment successful", + "DeploymentSpec": [ + { + "app": "alpine", + "version": "3.12" + } + ], + "DeploymentState": { + "alpine": "3.12" + } + } +] +``` + +```sh +curl -u'testuser:testpass' -i -XPOST localhost:8080/deploy -d '[{"app":"alpine", "version": "latest"}, {"app":"busybox", "version": "latest"}]' +``` + +```json +[ + { + "Id": 1610356267147639, + "Status": "deployed", + "StatusMessage": "Deployment successful", + "DeploymentSpec": [ + { + "app": "alpine", + "version": "3.12" + } + ], + "DeploymentState": { + "alpine": "3.12" + } + }, + { + "Id": 1610356267266631, + "Status": "deployed", + "StatusMessage": "Deployment successful", + "DeploymentSpec": [ + { + "app": "alpine", + "version": "latst" + }, + { + "app": "alpine", + "version": "latest" + } + ], + "DeploymentState": { + "alpine": "latest", + "busybox": "latest" + } + } +] +``` + +To test with `api`, `web`, `filestore`, `html2pdf`, the Podman container would need to be authenticated with the Gitlab registry of the glv5 project. diff --git a/deploymentagent/docker-compose.yml b/deploymentagent/docker-compose.yml new file mode 100644 index 0000000..f60e6bc --- /dev/null +++ b/deploymentagent/docker-compose.yml @@ -0,0 +1,34 @@ +services: + deploymentagent: + image: ${IMAGE}:${VERSION} + build: + args: + SSH_KEY: ${SSH_KEY} + context: ./src/deploymentagent + environment: + - BASIC_AUTH_USERNAME=testuser + - BASIC_AUTH_PASSWORD=testpass + - CONTAINERS_SOCKET=/tmp/podman/podman.sock + ports: + - 8080:5000 + volumes: + - podman-socket:/tmp/podman + depends_on: + - podman + podman: + image: podman:test + privileged: true + build: + context: ./src/podman + environment: + - CONTAINERS_SOCKET=/tmp/podman/podman.sock + - PORT=8090 + ports: + - 8090:8090 + volumes: + - podman-socket:/tmp/podman + +volumes: + podman-socket: + +version: "3.4" diff --git a/deploymentagent/src/deploymentagent/Dockerfile b/deploymentagent/src/deploymentagent/Dockerfile new file mode 100644 index 0000000..77f5542 --- /dev/null +++ b/deploymentagent/src/deploymentagent/Dockerfile @@ -0,0 +1,23 @@ +FROM golang:1.15-alpine3.12 as build + +WORKDIR /go/src/app + +COPY . . + +RUN apk add --no-cache openssh git && \ + git config --global url."git@gitlab.com:".insteadOf "https://gitlab.com/" && \ + mkdir /root/.ssh && \ + ssh-keyscan gitlab.com >> /root/.ssh/known_hosts + +ARG SSH_KEY +ENV SSH_KEY=${SSH_KEY} + +RUN printf "$SSH_KEY" | base64 -d > /root/.ssh/id_rsa && chmod 600 /root/.ssh/id_rsa && \ + go build -o deploymentagent && \ + rm /root/.ssh/id_rsa + +FROM alpine:3.12 + +COPY --from=build /go/src/app/deploymentagent /usr/local/bin/deploymentagent + +ENTRYPOINT ["/usr/local/bin/deploymentagent"] diff --git a/deploymentagent/src/deploymentagent/containers.go b/deploymentagent/src/deploymentagent/containers.go new file mode 100644 index 0000000..9ae99b3 --- /dev/null +++ b/deploymentagent/src/deploymentagent/containers.go @@ -0,0 +1,60 @@ +package main + +import ( + "errors" + "fmt" + "io/ioutil" + "net" + "net/http" + + "gitlab.com/infektcommon/settings" +) + +var ( + isGuidelinesApp = map[string]bool{"api": true, "web": true, "filestore": true, "html2pdf": true} + guidelinesRegistry = "registry.gitlab.com/infektweb/glv5/" + containersSocket string + socketHttpTransport = &http.Transport{ + Dial: func(n, a string) (conn net.Conn, err error) { return net.Dial("unix", containersSocket) }, + } +) + +func init() { + containersSocket = settings.GetSetting("CONTAINERS_SOCKET", "/tmp/podman/podman.sock") +} + +type podmanClient struct { + httpClient *http.Client +} + +func (pc *podmanClient) pullImages(deploymentSpec DeploymentSpec, httpClient *http.Client, api string) error { + for _, spec := range deploymentSpec { + app := spec.App + version := spec.Version + + if version == "" { + version = "latest" + } + + if isGuidelinesApp[app] { + app = guidelinesRegistry + app + } + + pc.httpClient = httpClient + + if response, err := pc.httpClient.Post(fmt.Sprintf("%s/images/pull?reference=%s:%s", api, app, version), jsonContentType, nil); err == nil { + responseBody, _ := ioutil.ReadAll(response.Body) + //ioutil.ReadAll(response.Body) + fmt.Println(string(responseBody)) + if response, err := pc.httpClient.Get(fmt.Sprintf("%s/images/%s:%s/exists", api, app, version)); err != nil { + return err + } else if response.StatusCode != 204 { + return errors.New(fmt.Sprintf("The Podman API has not returned an error, but the image \"%s:%s\" could not be found on this server. Likely the image could not be pulled from remote because it does not exist. It's best to check the logs of the deployment agent.", app, version)) + } + } else { + return err + } + } + + return nil +} diff --git a/deploymentagent/src/deploymentagent/containers_test.go b/deploymentagent/src/deploymentagent/containers_test.go new file mode 100644 index 0000000..1c6244d --- /dev/null +++ b/deploymentagent/src/deploymentagent/containers_test.go @@ -0,0 +1,33 @@ +package main + +import ( + "fmt" + "net/http" + "testing" +) + +func TestContainerPullImages(t *testing.T) { + testDeploymentSpecs := []DeploymentSpec{ + { // multiple existing images + {App: "busybox", Version: "latest"}, + {App: "alpine", Version: "3.12"}, + }, + { // non existing image + {App: "busybox", Version: "unknown"}, + }, + } + + client := &podmanClient{} + + t.Run("pulling multiple images", func(t *testing.T) { + if err := client.pullImages(testDeploymentSpecs[0], &http.Client{}, "http://localhost:8090/v1.0.0/libpod"); err != nil { + t.Errorf(fmt.Sprintf("%s", err)) + } + }) + + t.Run("pulling non existing images", func(t *testing.T) { + if err := client.pullImages(testDeploymentSpecs[1], &http.Client{}, "http://localhost:8090/v1.0.0/libpod"); err == nil { + t.Errorf("Expected to get an error") + } + }) +} diff --git a/deploymentagent/src/deploymentagent/go.mod b/deploymentagent/src/deploymentagent/go.mod new file mode 100644 index 0000000..ee33618 --- /dev/null +++ b/deploymentagent/src/deploymentagent/go.mod @@ -0,0 +1,28 @@ +module deploymentagent + +go 1.15 + +require ( + github.com/fatih/color v1.10.0 // indirect + github.com/frankban/quicktest v1.11.3 // indirect + github.com/golang/snappy v0.0.2 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-hclog v0.15.0 // indirect + github.com/hashicorp/go-multierror v1.1.0 // indirect + github.com/hashicorp/go-retryablehttp v0.6.8 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/mitchellh/mapstructure v1.4.0 // indirect + github.com/pierrec/lz4 v2.6.0+incompatible // indirect + github.com/stretchr/testify v1.6.1 // indirect + gitlab.com/infektcommon/settings v0.0.0-20210104082753-8cdac1bf58b6 + golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect + golang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect + golang.org/x/sys v0.0.0-20201231184435-2d18734c6014 // indirect + golang.org/x/text v0.3.4 // indirect + golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/square/go-jose.v2 v2.5.1 // indirect + gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect +) diff --git a/deploymentagent/src/deploymentagent/go.sum b/deploymentagent/src/deploymentagent/go.sum new file mode 100644 index 0000000..273278e --- /dev/null +++ b/deploymentagent/src/deploymentagent/go.sum @@ -0,0 +1,205 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/frankban/quicktest v1.11.3 h1:8sXhOn0uLys67V8EsXLc6eszDs8VXWxL3iRvebPhedY= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= +github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +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/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw= +github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.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 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= +github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v0.15.0 h1:qMuK0wxsoW4D0ddCCYwPSTm4KQv1X1ke3WmPWZ0Mvsk= +github.com/hashicorp/go-hclog v0.15.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= +github.com/hashicorp/go-retryablehttp v0.5.4 h1:1BZvpawXoJCWX6pNtow9+rpEj+3itIlutiqnntI6jOE= +github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-retryablehttp v0.6.8 h1:92lWxgpa+fF3FozM4B3UZtHZMJX8T5XT+TFdCxsPyWs= +github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-rootcerts v1.0.1 h1:DMo4fmknnz0E0evoNYnV48RjWndOsmd6OW+09R3cEP8= +github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +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-version v1.1.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= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/vault/api v1.0.4 h1:j08Or/wryXT4AcHj1oCbMd7IijXcKzYUGw59LGu9onU= +github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q= +github.com/hashicorp/vault/sdk v0.1.13 h1:mOEPeOhT7jl0J4AMl1E705+BcmeRs1VmKNb9F0sMLy8= +github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/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 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.0 h1:7ks8ZkOP5/ujthUsT07rNv+nkLXCQWKNHuwzOAesEks= +github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/lz4 v2.6.0+incompatible h1:Ix9yFKn1nSPBLFl/yZknTp8TU5G4Ps0JDmguYK6iH1A= +github.com/pierrec/lz4 v2.6.0+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +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/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +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 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gitlab.com/infektcommon/settings v0.0.0-20210104082753-8cdac1bf58b6 h1:s1eS+fZ5ZngKZyw1oQ5Ubb4CnavabQXTVMlFURJoQWs= +gitlab.com/infektcommon/settings v0.0.0-20210104082753-8cdac1bf58b6/go.mod h1:vwzpYzrKQTrUbX7bEssArGT7U1jwZUqQxnvnCJArso0= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +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-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +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-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-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +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/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-20190129075346-302c3dd5f1cc/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-20190403152447-81d4e9dc473e/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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201231184435-2d18734c6014 h1:joucsQqXmyBVxViHCPFjG3hx8JzIFSaym3l3MM/Jsdg= +golang.org/x/sys v0.0.0-20201231184435-2d18734c6014/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db h1:6/JqlYfC1CCaLnGceQTI+sDGhC9UBSPAsBqI0Gun6kU= +golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/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 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/deploymentagent/src/deploymentagent/main.go b/deploymentagent/src/deploymentagent/main.go new file mode 100644 index 0000000..dbf9c7b --- /dev/null +++ b/deploymentagent/src/deploymentagent/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "log" + "net/http" +) + +var ( + inputQueue *DeploymentQueue // queued + processQueue *DeploymentQueue // deploying, failed, deployed +) + +func main() { + server := newDeploymentAgentServer() + + inputQueue = new(DeploymentQueue) + processQueue = new(DeploymentQueue) + + go processDeploymentSpecs(&podmanClient{}, &http.Client{Transport: socketHttpTransport}, "http://podman/v1.0.0/libpod") + + if err := http.ListenAndServe(":5000", server); err != nil { + log.Fatalf("could not listen on port 5000 %v", err) + } +} diff --git a/deploymentagent/src/deploymentagent/server.go b/deploymentagent/src/deploymentagent/server.go new file mode 100644 index 0000000..6b9c410 --- /dev/null +++ b/deploymentagent/src/deploymentagent/server.go @@ -0,0 +1,232 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "sort" + "time" + + "gitlab.com/infektcommon/settings" +) + +const ( + jsonContentType = "application/json" +) + +var ( + basicAuthUsername string + basicAuthPassword string + deploymentStateFile string +) + +func init() { + basicAuthUsername = settings.GetSetting("BASIC_AUTH_USERNAME", "default") + basicAuthPassword = settings.GetSetting("BASIC_AUTH_PASSWORD", "default") + deploymentStateFile = settings.GetSetting("DEPLOYMENT_STATE_FILE", "./guidelines.json") +} + +type DeploymentAgentServer struct { + http.Handler +} + +type DeploymentSpec []struct { + App string `json:"app"` + Version string `json:"version"` +} + +type QueuedDeploymentSpec struct { + Id int64 + Status string // one of: queued (input queue), deploying, failed, deployed (process queue) + StatusMessage string // status or error message + DeploymentSpec DeploymentSpec + DeploymentState map[string]string +} + +type DeploymentQueue struct { + items []QueuedDeploymentSpec +} + +func (q *DeploymentQueue) Enqueue(id int64, status, statusMessage string, deploymentSpec DeploymentSpec) { + q.items = append(q.items, QueuedDeploymentSpec{ + Id: id, + Status: status, + StatusMessage: statusMessage, + DeploymentSpec: deploymentSpec, + }) + +} + +func (q *DeploymentQueue) Dequeue() QueuedDeploymentSpec { + item := q.items[0] + q.items = q.items[1:len(q.items)] + return item +} + +func (q *DeploymentQueue) GetAll() []QueuedDeploymentSpec { + return q.items +} + +func (q *DeploymentQueue) updateStatus(id int64, status, statusMessage string, deploymentState map[string]string) { + for i := range q.items { + if q.items[i].Id == id { + p := &q.items[i] + p.Status = status + p.StatusMessage = statusMessage + p.DeploymentState = getDeploymentState() + break + } + } +} + +func (q *DeploymentQueue) String() string { + return fmt.Sprint(q.items) +} + +func newDeploymentAgentServer() *DeploymentAgentServer { + server := new(DeploymentAgentServer) + router := http.NewServeMux() + + router.Handle("/deploy", http.HandlerFunc(server.deploymentHandler)) + router.Handle("/status", http.HandlerFunc(server.statusHandler)) + + server.Handler = router + + return server +} + +func isAuthenticated(w http.ResponseWriter, r http.Request) (http.ResponseWriter, bool) { + user, pass, ok := r.BasicAuth() + if !ok { + w.WriteHeader(http.StatusUnauthorized) + w.Header().Add("WWW-Authenticate", `Basic realm=""`) + w.Write([]byte(`{"Error": "No credentials present"}`)) + return w, false + } + + if (user != basicAuthUsername && user != "default") || (pass != basicAuthPassword && pass != "default") { + w.WriteHeader(http.StatusUnauthorized) + w.Header().Add("WWW-Authenticate", `Basic realm=""`) + w.Write([]byte(`{"Error": "Invalid credentials"}`)) + return w, false + } + + return w, true +} + +func (s *DeploymentAgentServer) deploymentHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", jsonContentType) + if authenticatedResponse, ok := isAuthenticated(w, *r); ok { + w = authenticatedResponse + } else { + return + } + + switch r.Method { + case http.MethodPost: + queueDeploymentSpec(w, r) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + w.Write([]byte(`{"Error": "Method not allowed"}`)) + } +} + +func (s *DeploymentAgentServer) statusHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", jsonContentType) + if authenticatedResponse, ok := isAuthenticated(w, *r); ok { + w = authenticatedResponse + } else { + return + } + + switch r.Method { + case http.MethodGet: + var status []QueuedDeploymentSpec + status = append(status, inputQueue.GetAll()...) + status = append(status, processQueue.GetAll()...) + sort.Slice(status, func(i, j int) bool { + return status[i].Id < status[j].Id + }) + + w.WriteHeader(http.StatusAccepted) + json.NewEncoder(w).Encode(status) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + w.Write([]byte(`{"Error": "Method not allowed"}`)) + } +} + +func queueDeploymentSpec(w http.ResponseWriter, r *http.Request) { + var error struct{ Error string } + var deploymentSpec DeploymentSpec + + body, _ := ioutil.ReadAll(r.Body) + err := json.Unmarshal(body, &deploymentSpec) + if err != nil { + error.Error = err.Error() + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(&error) + } else { + message := "Queued for deployment" + deploymentId := time.Now().UnixNano() / 1000 + inputQueue.Enqueue(deploymentId, "queued", message, deploymentSpec) + w.WriteHeader(http.StatusAccepted) + w.Write([]byte(fmt.Sprintf(`{"Message": %q, "DeploymentId": "%d"}`, message, deploymentId))) + } +} + +func processDeploymentSpecs(pc *podmanClient, hc *http.Client, url string) { + fmt.Println("Server ready to schedule deployments") + for { + time.Sleep(5 * time.Second) + if len(inputQueue.GetAll()) > 0 { + dequequedDeploymentSpec := inputQueue.Dequeue() + processQueue.Enqueue(dequequedDeploymentSpec.Id, "deploying", "Deployment in progress", dequequedDeploymentSpec.DeploymentSpec) + err := pc.pullImages(dequequedDeploymentSpec.DeploymentSpec, hc, url) + if err != nil { + processQueue.updateStatus(dequequedDeploymentSpec.Id, "failed", err.Error(), nil) + } else { + err := mergeDeploymentState(dequequedDeploymentSpec.DeploymentSpec) + if err != nil { + processQueue.updateStatus(dequequedDeploymentSpec.Id, "failed", err.Error(), getDeploymentState()) + } else { + processQueue.updateStatus(dequequedDeploymentSpec.Id, "deployed", "Deployment successful", getDeploymentState()) + } + } + fmt.Println(processQueue.GetAll()) + } + } +} + +func getDeploymentState() map[string]string { + state := make(map[string]string) + + file, _ := ioutil.ReadFile(deploymentStateFile) + err := json.Unmarshal([]byte(file), &state) + if err != nil { + fmt.Println(err.Error()) + } + return state +} + +func mergeDeploymentState(deploymentSpec DeploymentSpec) error { + var deployedApps = []string{} + deploymentState := getDeploymentState() + + for k := range deploymentState { + deployedApps = append(deployedApps, k) + } + + for _, app := range deploymentSpec { + deploymentState[app.App] = app.Version + } + + file, _ := json.MarshalIndent(deploymentState, "", " ") + err := ioutil.WriteFile(deploymentStateFile, file, 0644) + if err != nil { + return err + } + + return nil +} diff --git a/deploymentagent/src/deploymentagent/server_test.go b/deploymentagent/src/deploymentagent/server_test.go new file mode 100644 index 0000000..5f99b80 --- /dev/null +++ b/deploymentagent/src/deploymentagent/server_test.go @@ -0,0 +1,64 @@ +package main + +import ( + "bytes" + "fmt" + "net/http" + "net/http/httptest" + "testing" +) + +var apiTests = []struct { + path string + method string + username string + password string + payload string + wantResponseCode int +}{ + {"/deploy", "POST", "testuser", "testpass", `[{"app":"busybox", "version": "latest"}]`, 202}, + {"/deploy", "POST", "testuser", "testpass", `[invalid{"app":"foo", "version": "bar"}]`, 400}, + {"/deploy", "GET", "testuser", "", "", 401}, + {"/deploy", "GET", "", "", "", 401}, + {"/deploy", "GET", "testuser", "testpass", "", 405}, +} + +func TestDeploymentAgentServer(t *testing.T) { + handler := http.HandlerFunc(newDeploymentAgentServer().deploymentHandler) + + inputQueue = new(DeploymentQueue) + processQueue = new(DeploymentQueue) + + go processDeploymentSpecs(&podmanClient{}, &http.Client{}, "http://localhost:8090/v1.0.0/libpod/") + + basicAuthUsername = "testuser" + basicAuthPassword = "testpass" + + for _, test := range apiTests { + t.Run(fmt.Sprintf("%q returns %d on %s with payload %q authenticated as %s:%s", + test.path, + test.wantResponseCode, + test.method, + test.payload, + test.username, + test.password), + func(t *testing.T) { + payload := []byte(test.payload) + request, _ := http.NewRequest(test.method, test.path, bytes.NewBuffer(payload)) + request.SetBasicAuth(test.username, test.password) + response := httptest.NewRecorder() + + handler.ServeHTTP(response, request) + + assertStatus(t, response.Code, test.wantResponseCode) + }) + } +} + +func assertStatus(t *testing.T, got, want int) { + t.Helper() + if got != want { + t.Errorf("got %d, want %d", got, want) + } + +} diff --git a/deploymentagent/src/podman/Dockerfile b/deploymentagent/src/podman/Dockerfile new file mode 100644 index 0000000..4c273e7 --- /dev/null +++ b/deploymentagent/src/podman/Dockerfile @@ -0,0 +1,8 @@ +FROM fedora:33 + +RUN dnf -y install --enablerepo updates-testing podman e2fsprogs socat +RUN dd if=/dev/zero of=/containers-disk iflag=fullblock bs=1M count=300 && mkfs.ext4 /containers-disk + +COPY start.sh /start.sh +RUN ["chmod", "+x", "/start.sh"] +ENTRYPOINT ["/start.sh"] diff --git a/deploymentagent/src/podman/start.sh b/deploymentagent/src/podman/start.sh new file mode 100644 index 0000000..4c76332 --- /dev/null +++ b/deploymentagent/src/podman/start.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +(mount -o loop /containers-disk /var/lib/containers/ && podman system service -t 0 unix://"$CONTAINERS_SOCKET") & + +socat tcp-listen:"$PORT",fork,reuseaddr unix-connect:"$CONTAINERS_SOCKET"