Repository: Clivern/Beetle Branch: main Commit: fddfc896762b Files: 111 Total size: 281.8 KB Directory structure: gitextract_u0vtr08u/ ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── auto-merge.yml │ ├── boring-cyborg.yml │ └── workflows/ │ ├── build.yml │ ├── release.yml │ └── release_pkg.yml ├── .gitignore ├── .go-version ├── .goreleaser.yml ├── .mergify.yml ├── .poodle.toml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── assets/ │ └── img/ │ └── chart.drawio ├── beetle.go ├── beetle_test.go ├── bin/ │ └── release.sh ├── config.dist.yml ├── config.testing.yml ├── config.toml ├── core/ │ ├── cmd/ │ │ ├── apps.go │ │ ├── deploy.go │ │ ├── license.go │ │ ├── root.go │ │ ├── serve.go │ │ └── version.go │ ├── controller/ │ │ ├── application.go │ │ ├── applications.go │ │ ├── cluster.go │ │ ├── clusters.go │ │ ├── daemon.go │ │ ├── deployment.go │ │ ├── health_check.go │ │ ├── health_check_test.go │ │ ├── job.go │ │ ├── jobs.go │ │ ├── metrics.go │ │ ├── namespace.go │ │ ├── namespaces.go │ │ ├── ready_check.go │ │ ├── ready_check_test.go │ │ └── worker.go │ ├── kubernetes/ │ │ ├── application.go │ │ ├── cluster.go │ │ ├── cluster_test.go │ │ ├── config.go │ │ ├── configmap.go │ │ ├── deployment.go │ │ ├── deployment_strategy.go │ │ ├── namespace.go │ │ ├── namespace_test.go │ │ └── pod.go │ ├── middleware/ │ │ ├── auth.go │ │ ├── correlation.go │ │ ├── log.go │ │ └── metric.go │ ├── migration/ │ │ └── schema.go │ ├── model/ │ │ ├── application.go │ │ ├── cluster.go │ │ ├── configmap.go │ │ ├── configs.go │ │ ├── dsn.go │ │ ├── dsn_test.go │ │ ├── job.go │ │ ├── message.go │ │ ├── metric.go │ │ ├── migration.go │ │ ├── namespace.go │ │ ├── patch.go │ │ └── request.go │ ├── module/ │ │ ├── database.go │ │ ├── database_test.go │ │ ├── file_system.go │ │ ├── http.go │ │ ├── http_test.go │ │ ├── prometheus.go │ │ ├── remote.go │ │ └── remote_test.go │ └── util/ │ ├── helpers.go │ └── helpers_test.go ├── deployment/ │ ├── docker/ │ │ ├── README.md │ │ ├── docker-compose.yml │ │ └── prometheus.yml │ └── k8s/ │ └── incluster/ │ ├── README.md │ ├── beetle.yaml │ └── sample_app.yaml ├── go.mod ├── go.sum ├── pkg/ │ ├── expect.go │ └── server_mock.go ├── renovate.json ├── sdk/ │ ├── application.go │ ├── application_test.go │ ├── client.go │ ├── cluster.go │ ├── cluster_test.go │ ├── deployment.go │ ├── deployment_test.go │ ├── job.go │ ├── job_test.go │ ├── namespace.go │ └── namespace_test.go └── swagger.yaml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ docs/* linguist-vendored ================================================ FILE: .github/CODEOWNERS ================================================ # Docs: https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners * @clivern ================================================ FILE: .github/FUNDING.yml ================================================ github: # clivern custom: clivern.com/sponsor/ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve --- **Describe the bug** A clear and concise description of what the bug is. **Development or production environment** - OS: [e.g. Ubuntu 18.04] - Go 1.13 **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/auto-merge.yml ================================================ # https://github.com/bobvanderlinden/probot-auto-merge blockingLabels: - blocking rules: - minApprovals: OWNER: 1 MEMBER: 2 - requiredLabels: - merge ================================================ FILE: .github/boring-cyborg.yml ================================================ --- firstIssueWelcomeComment: "Thanks for opening your first issue here! Be sure to follow the issue template!" firstPRMergeComment: "Awesome work, congrats on your first merged pull request!" firstPRWelcomeComment: "Thanks for opening this pull request! Please check out our contributing guidelines." labelPRBasedOnFilePath: "🚧 CI": - .github/workflows/* "🚧 CSS": - "**/*.css" "🚧 Configuration": - .github/* "🚧 Dependencies": - Dockerfile* - composer.* - package.json - package-lock.json - yarn.lock - go.mod - go.sum - build.gradle - Cargo.toml - Cargo.lock - Gemfile.lock - Gemfile "🚧 Docker": - Dockerfile* - .docker/**/* "🚧 Documentation": - README.md - CONTRIBUTING.md "🚧 Go": - "**/*.go" "🚧 Rust": - "**/*.rs" "🚧 Java": - "**/*.java" "🚧 Ruby": - "**/*.rb" "🚧 HTML": - "**/*.htm" - "**/*.html" "🚧 Image": - "**/*.gif" - "**/*.jpg" - "**/*.jpeg" - "**/*.png" - "**/*.webp" "🚧 JSON": - "**/*.json" "🚧 JavaScript": - "**/*.js" - package.json - package-lock.json - yarn.lock "🚧 MarkDown": - "**/*.md" "🚧 PHP": - "**/*.php" - composer.* "🚧 Source": - src/**/* "🚧 TOML": - "**/*.toml" "🚧 Templates": - "**/*.twig" - "**/*.tpl" "🚧 Tests": - tests/**/* "🚧 YAML": - "**/*.yml" - "**/*.yaml" ================================================ FILE: .github/workflows/build.yml ================================================ name: Build on: push: pull_request: jobs: build: runs-on: ubuntu-latest strategy: fail-fast: false matrix: go: ['1.19', '1.20.4'] name: Go ${{ matrix.go }} run steps: - uses: actions/checkout@v4 - name: Setup go uses: actions/setup-go@v4 with: go-version: ${{ matrix.go }} - name: Get dependencies run: | export PATH=${PATH}:`go env GOPATH`/bin make install_revive - name: Run make ci run: | export PATH=${PATH}:`go env GOPATH`/bin go get -t . make ci git status git diff > diff.log cat diff.log git clean -fd git reset --hard make verify ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: tags: - '*' jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v4 with: go-version: 1.19 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v3 with: version: latest args: release --rm-dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/release_pkg.yml ================================================ name: ReleasePkg on: push: tags: - '*' jobs: release: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v4 with: go-version: 1.19 - name: Update checksum database run: | ./bin/release.sh ================================================ FILE: .gitignore ================================================ # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out config.prod.yml # dist dir dist ================================================ FILE: .go-version ================================================ 1.20.4 ================================================ FILE: .goreleaser.yml ================================================ # This is an example goreleaser.yaml file with some sane defaults. # Make sure to check the documentation at http://goreleaser.com --- archives: - replacements: 386: i386 amd64: x86_64 darwin: Darwin linux: Linux windows: Windows before: hooks: - "go mod download" - "go generate ./..." builds: - env: - CGO_ENABLED=0 goos: - linux - darwin - windows changelog: filters: exclude: - "^docs:" - "^test:" sort: asc checksum: name_template: checksums.txt snapshot: name_template: "{{ .Tag }}-next" project_name: beetle ================================================ FILE: .mergify.yml ================================================ --- pull_request_rules: - actions: merge: method: squash conditions: - author!=Clivern - approved-reviews-by=Clivern - label=release name: "Automatic Merge 🚀" - actions: merge: method: merge conditions: - author=Clivern - label=release name: "Automatic Merge 🚀" - actions: merge: method: squash conditions: - "author=renovate[bot]" - label=release name: "Automatic Merge for Renovate PRs 🚀" - actions: comment: message: "Nice! PR merged successfully." conditions: - merged name: "Merge Done 🚀" ================================================ FILE: .poodle.toml ================================================ # API Definition For Beetle # -------------------------- # # In order to use this file: # 1. Check & Install https://github.com/Clivern/Poodle # # 2. Now you can use poodle to interact with your local or hosted beetle. # $ poodle call -f .poodle.toml # [Main] id = "clivern_beetle" name = "Clivern - Beetle" description = "Beetle API Definitions" timeout = "30s" service_url = "{$serviceURL:http://127.0.0.1:8080}" # These headers will be applied to all endpoints http calls headers = [] [Security] # Supported Types are basic, bearer and api_key and none scheme = "none" [Security.Basic] username = "{$authUsername:default}" password = "{$authPassword:default}" header = ["Authorization", "Basic base64(username:password)"] [Security.ApiKey] header = ["X-API-KEY", "{$authApiKey:default}"] # In case of bearer authentication, it is recommended to create another # service or endpoint to generate the bearer tokens [Security.Bearer] header = ["Authorization", "Bearer {$authBearerToken:default}"] [[Endpoint]] id = "GetSystemHealth" name = "Get System Health" description = "" method = "get" headers = [ ["Content-Type", "application/json"] ] parameters = [] public = true uri = "/_health" body = "" [[Endpoint]] id = "GetSystemReadiness" name = "Get System Readiness" description = "" method = "get" headers = [ ["Content-Type", "application/json"] ] parameters = [] public = true uri = "/_ready" body = "" [[Endpoint]] id = "GetMetrics" name = "Get Metrics" description = "" method = "get" headers = [] parameters = [] public = true uri = "/metrics" body = "" [[Endpoint]] id = "GetClusters" name = "Get Clusters" description = "" method = "get" headers = [ ["Content-Type", "application/json"] ] parameters = [] public = true uri = "/api/v1/cluster" body = "" [[Endpoint]] id = "GetCluster" name = "Get Cluster" description = "" method = "get" headers = [ ["Content-Type", "application/json"] ] parameters = [] public = true uri = "/api/v1/cluster/{$clusterName}" body = "" [[Endpoint]] id = "GetClusterNamespaces" name = "Get Cluster Namespaces" description = "" method = "get" headers = [ ["Content-Type", "application/json"] ] parameters = [] public = true uri = "/api/v1/cluster/{$clusterName}/namespace" body = "" [[Endpoint]] id = "GetClusterNamespace" name = "Get Cluster Namespace" description = "" method = "get" headers = [ ["Content-Type", "application/json"] ] parameters = [] public = true uri = "/api/v1/cluster/{$clusterName}/namespace/{$namespaceName}" body = "" [[Endpoint]] id = "GetNamespaceApplications" name = "Get Namespace Applications" description = "" method = "get" headers = [ ["Content-Type", "application/json"] ] parameters = [] public = true uri = "/api/v1/cluster/{$clusterName}/namespace/{$namespaceName}/app" body = "" [[Endpoint]] id = "GetNamespaceApplicationByID" name = "Get Namespace Application by ID" description = "" method = "get" headers = [ ["Content-Type", "application/json"] ] parameters = [] public = true uri = "/api/v1/cluster/{$clusterName}/namespace/{$namespaceName}/app/{$applicationId}" body = "" [[Endpoint]] id = "GetJobs" name = "Get Jobs" description = "" method = "get" headers = [ ["Content-Type", "application/json"] ] parameters = [] public = true uri = "/api/v1/job" body = "" [[Endpoint]] id = "GetJobByUUID" name = "Get Job by UUID" description = "" method = "get" headers = [ ["Content-Type", "application/json"] ] parameters = [] uri = "/api/v1/job/{$jobUUID}" body = "" [[Endpoint]] id = "DeleteJobByUUID" name = "Delete Job by UUID" description = "" method = "delete" headers = [ ["Content-Type", "application/json"] ] parameters = [] uri = "/api/v1/job/{$jobUUID}" body = "" [[Endpoint]] id = "DeployApplicationById" name = "Deploy Application By ID" description = "" method = "post" headers = [ ["Content-Type", "application/json"] ] parameters = [] uri = "/api/v1/cluster/{$clusterName}/namespace/{$namespaceName}/app/{$applicationId}/deployment" body = """ { "version":"{$version}", "strategy":"{$strategy:recreate}" } """ ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@clivern.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: CONTRIBUTING.md ================================================ ## Contributing - With issues: - Use the search tool before opening a new issue. - Please provide source code and commit sha if you found a bug. - Review existing issues and provide feedback or react to them. - With pull requests: - Open your pull request against `main` - Your pull request should have no more than two commits, if not you should squash them. - It should pass all tests in the available continuous integrations systems such as TravisCI. - You should add/modify tests to cover your proposed code changes. - If your pull request contains a new feature, please document it on the README. ================================================ FILE: Dockerfile ================================================ FROM golang:1.20.2 ARG BEETLE_VERSION=1.0.2 ENV GO111MODULE=on RUN mkdir -p /app/configs RUN mkdir -p /app/var/logs RUN mkdir -p /app/var/storage RUN apt-get update WORKDIR /app RUN curl -sL https://github.com/Clivern/Beetle/releases/download/v${BEETLE_VERSION}/beetle_${BEETLE_VERSION}_Linux_x86_64.tar.gz | tar xz RUN rm LICENSE RUN rm README.md COPY ./config.dist.yml /app/configs/ EXPOSE 8080 VOLUME /app/configs VOLUME /app/var RUN ./beetle version CMD ["./beetle", "serve", "-c", "/app/configs/config.dist.yml"] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Clivern Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ GO ?= go GOFMT ?= $(GO)fmt pkgs = ./... HUGO ?= hugo help: Makefile @echo @echo " Choose a command run in Beetle:" @echo @sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /' @echo ## install_revive: Install revive for linting. install_revive: @echo ">> ============= Install Revive ============= <<" $(GO) install github.com/mgechev/revive@latest ## style: Check code style. style: @echo ">> ============= Checking Code Style ============= <<" @fmtRes=$$($(GOFMT) -d $$(find . -path ./vendor -prune -o -name '*.go' -print)); \ if [ -n "$${fmtRes}" ]; then \ echo "gofmt checking failed!"; echo "$${fmtRes}"; echo; \ echo "Please ensure you are using $$($(GO) version) for formatting code."; \ exit 1; \ fi ## check_license: Check if license header on all files. check_license: @echo ">> ============= Checking License Header ============= <<" @licRes=$$(for file in $$(find . -type f -iname '*.go' ! -path './vendor/*') ; do \ awk 'NR<=3' $$file | grep -Eq "(Copyright|generated|GENERATED)" || echo $$file; \ done); \ if [ -n "$${licRes}" ]; then \ echo "license header checking failed:"; echo "$${licRes}"; \ exit 1; \ fi ## test_short: Run test cases with short flag. test_short: @echo ">> ============= Running Short Tests ============= <<" $(GO) test -mod=readonly -short $(pkgs) ## test: Run test cases. test: @echo ">> ============= Running All Tests ============= <<" $(GO) test -mod=readonly -v -cover $(pkgs) ## lint: Lint the code. lint: @echo ">> ============= Lint All Files ============= <<" revive -config config.toml -exclude vendor/... -formatter friendly ./... ## verify: Verify dependencies verify: @echo ">> ============= List Dependencies ============= <<" $(GO) list -m all @echo ">> ============= Verify Dependencies ============= <<" $(GO) mod verify ## format: Format the code. format: @echo ">> ============= Formatting Code ============= <<" $(GO) fmt $(pkgs) ## vet: Examines source code and reports suspicious constructs. vet: @echo ">> ============= Vetting Code ============= <<" $(GO) vet $(pkgs) ## coverage: Create HTML coverage report coverage: @echo ">> ============= Coverage ============= <<" rm -f coverage.html cover.out $(GO) test -mod=readonly -coverprofile=cover.out $(pkgs) go tool cover -html=cover.out -o coverage.html ## ci: Run all CI tests. ci: style check_license test vet lint @echo "\n==> All quality checks passed" ## run: Run the service run: -cp -n config.dist.yml config.prod.yml $(GO) run beetle.go serve -c config.prod.yml .PHONY: help ================================================ FILE: README.md ================================================

Beetle

Kubernetes multi-cluster deployment automation service


:unicorn: Check out the demo!


Application deployment and management should be automated, auditable, and easy to understand and that's what beetle tries to achieve in a simple manner. Beetle automates the deployment and rollback of your applications in a multi-cluster, multi-namespaces kubernetes environments. Easy to integrate with through API endpoints & webhooks to fit a variety of workflows. ## Documentation ## Deployment ### On a Linux Server Download [the latest beetle binary.](https://github.com/Clivern/Beetle/releases) ```zsh $ curl -sL https://github.com/Clivern/Beetle/releases/download/vx.x.x/beetle_x.x.x_OS.tar.gz | tar xz ``` Create your config file as explained on [development part](#development) and run beetle with systemd or anything else you prefer. ```zsh $ ./beetle serve -c /custom/path/config.prod.yml ``` ## Development Beetle uses Go Modules to manage dependencies. First Create a prod config file. ```zsh $ git clone https://github.com/Clivern/Beetle.git $ cp config.dist.yml config.prod.yml ``` Then add your default configs. You probably wondering how the following configs even work! let's pick one and explain. The item mode: `${BEETLE_APP_MODE:-dev}` means that the mode is dev unless environment variable `BEETLE_APP_MODE` is defined. so you can always override the value by defining the environment variable `export BEETLE_APP_MODE=prod`. and same for others ```yaml # App configs app: # Env mode (dev or prod) mode: ${BEETLE_APP_MODE:-dev} # HTTP port port: ${BEETLE_API_PORT:-8080} # App URL domain: ${BEETLE_APP_DOMAIN:-http://127.0.0.1:8080} # TLS configs tls: status: ${BEETLE_API_TLS_STATUS:-off} pemPath: ${BEETLE_API_TLS_PEMPATH:-cert/server.pem} keyPath: ${BEETLE_API_TLS_KEYPATH:-cert/server.key} # Message Broker Configs broker: # Broker driver (native) driver: ${BEETLE_BROKER_DRIVER:-native} # Native driver configs native: # Queue max capacity capacity: ${BEETLE_BROKER_NATIVE_CAPACITY:-5000} # Number of concurrent workers workers: ${BEETLE_BROKER_NATIVE_WORKERS:-4} # API Configs api: key: ${BEETLE_API_KEY:- } # Runtime, Requests/Response and Beetle Metrics metrics: prometheus: # Route for the metrics endpoint endpoint: ${BEETLE_METRICS_PROM_ENDPOINT:-/metrics} # Application Database database: # Database driver (sqlite3, mysql) driver: ${BEETLE_DATABASE_DRIVER:-sqlite3} # Database Host host: ${BEETLE_DATABASE_MYSQL_HOST:-localhost} # Database Port port: ${BEETLE_DATABASE_MYSQL_PORT:-3306} # Database Name name: ${BEETLE_DATABASE_MYSQL_DATABASE:-beetle.db} # Database Username username: ${BEETLE_DATABASE_MYSQL_USERNAME:-root} # Database Password password: ${BEETLE_DATABASE_MYSQL_PASSWORD:-root} # Kubernetes Clusters clusters: - name: ${BEETLE_KUBE_CLUSTER_01_NAME:-production} inCluster: ${BEETLE_KUBE_CLUSTER_01_IN_CLUSTER:-false} kubeconfig: ${BEETLE_KUBE_CLUSTER_01_CONFIG_FILE:-/app/configs/production-cluster-kubeconfig.yaml} - name: ${BEETLE_KUBE_CLUSTER_02_NAME:-staging} inCluster: ${BEETLE_KUBE_CLUSTER_02_IN_CLUSTER:-false} kubeconfig: ${BEETLE_KUBE_CLUSTER_02_CONFIG_FILE:-/app/configs/staging-cluster-kubeconfig.yaml} # HTTP Webhook webhook: url: ${BEETLE_WEBHOOK_URL:- } retry: ${BEETLE_WEBHOOK_RETRY:-3} apiKey: ${BEETLE_WEBHOOK_API_KEY:- } # Log configs log: # Log level, it can be debug, info, warn, error, panic, fatal level: ${BEETLE_LOG_LEVEL:-info} # output can be stdout or abs path to log file /var/logs/beetle.log output: ${BEETLE_LOG_OUTPUT:-stdout} # Format can be json format: ${BEETLE_LOG_FORMAT:-json} ``` And then run the application. ```zsh $ go build beetle.go $ ./beetle serve -c /custom/path/config.prod.yml // OR $ make run // OR $ go run beetle.go serve -c /custom/path/config.prod.yml ``` ## API Documentation Go to https://editor.swagger.io/ and import this file https://raw.githubusercontent.com/Clivern/Beetle/main/swagger.yaml. ## Versioning For transparency into our release cycle and in striving to maintain backward compatibility, Beetle is maintained under the [Semantic Versioning guidelines](https://semver.org/) and release process is predictable and business-friendly. See the [Releases section of our GitHub project](https://github.com/clivern/beetle/releases) for changelogs for each release version of Beetle. It contains summaries of the most noteworthy changes made in each release. ## Bug tracker If you have any suggestions, bug reports, or annoyances please report them to our issue tracker at https://github.com/clivern/beetle/issues ## Security Issues If you discover a security vulnerability within Beetle, please send an email to [hello@clivern.com](mailto:hello@clivern.com) ## Contributing We are an open source, community-driven project so please feel free to join us. see the [contributing guidelines](CONTRIBUTING.md) for more details. ## License © 2020, clivern. Released under [MIT License](https://opensource.org/licenses/mit-license.php). **Beetle** is authored and maintained by [@clivern](http://github.com/clivern). ================================================ FILE: assets/img/chart.drawio ================================================ 7Vxdd5s6Fv01XmvmwSyQ+HyMk7jtNOnkXvfetvclC4Nsq8HIA3IS99ePBAKDhG2SgJMmcddqjBBCaG9tHZ1z8ACeLu8/JP5qcUlCFA2AHt4P4NkAAMPQbfaHl2zyEgc6ecE8waGotC2Y4F9IFOqidI1DlNYqUkIiilf1woDEMQporcxPEnJXrzYjUf2uK3+OlIJJ4Edq6Tcc0kXxXLa3PfER4flC3NoF4vmmfnAzT8g6FvcbADjLPvnppV+0JR40XfghuasUwfMBPE0Iofm35f0pivjYFsOWXzfecbbsd4Ji2uYCN55MJ+58/un6P2jpe7/Qt0syNGwBX0o3xYigkA2QOCQJXZA5if3ofFs6yp4a8XZ1drStc0HIihUarPAnonQj0PbXlLCiBV1G4iy6x/Q7v1yzxNGPypmze9FydrARB+rziiHg/a0UiKf/gMgS0WTDKiQo8im+rQPuC97My3rlpVcEs1sAXXDchVZ+iWA48PR6EylZJwESV1UReGhD1E/miCoNsS+V59kWZQA/CGznHew3A7YD8/vd+tEaFRIloc80acW/Lu/nXN21ebACWkjW0wj9mYntqFQtjldKE3JTyiQvmeEoOiURSdhxTGLOlxmJaVHENFHPPqz8FiUUM909ifA85vfEYZgRLG9VamUnAXgz6H4vBcRZx5IQsEUTd1udB7pAZVGReFvfzZoaTA/HxGzAxI6oGLZsmStGzv7fmi8NbCSg4fN/1SJ7zv9eJSRcBxSTeMDFP29omhSnP6+nKIkRZcsr0E+jdUpRUtRi/c/vmFdVmBH66aKc9GLt9afZWb0+vasUYH21DNccw12wNhLupuzndcS0hVVjuFPRfORPUXRFUpw9JjxLcpxKOl1I50ta+YJnEZrRvfRb+QGO5xdZtTNLZbCXfUS5UDkDNHL0wEw8TN0KNU1LZaYJNVsUP1Hn6lPD0WzH0rcfqTkym6WoJ5Wy3lWqBgUs1olnVCn7USolxlBRqTN0iyKyWqJMbvwlsxpG8TRd5VXeNetlapbVmshvT7OcN69ZZl2zAHx2zXI71awJ9edsgr3r1W+jV05rEr85vXLBYb2qkM1PV7mjbYbvOT13siyXL73Cg4ChxPivMoFyX4FMYzhyz8bGLsaGPpsMfoquQQeKZQNd86qfGjSmDRT9gp5KDas3+XJbbNbbQ8TG1gVTaNvd4BRayA3NXTjNkZ8wTfHDbtYWy3Q0WF9ddE9BxwWao+Lj2Jru9gZRi51KTdHJmkY4ZqAUjmtJ1BUwZBlWUJJ1eEooJcudMMpw3aEp1/5Um2O6WE/3WBQd4MhUR5N2NoUgHhJguzcIm4yEHYYbXmaBgypgzaN8EJwM1lEZLaiMt4gXwFF2s5NiUutNM1z052xBKY+MnPChAOMgjA0NM9thhuMQJVrA7gjGXDrZH17OzJLxNPHjMB3+JFHEBnGcBQrGU0yn6+AG0WFKAuxHQ2Yc3JHkZpjVHnKiDA3gaitmBfVMFL1Ok4YNsNVgS/YnxkUH9tGkcXLvVuFdiloTjIPmlkzAkmKKlFRYe7fAFE2YAcZ7fseEQLKy9OJYPJyxtdfYEWzQEf8uNbVV7nv8FPAujlZJ/qVeh7Fxych0zUfqGvOuzbJudEEb6NTX8CZ1sQyVNhD0Rpsm565Em8PhFtkArgZS4vCERzw58hEJbr4ucJwXj3FUVKqHa+r7SpDXFs1nKzplu4DascRgaJ64FpSZymBLNt+rBzykIwI8/HAb1cmODoZ18mDIXgNfGAUHA0AV/I1iD/pEQ96GkkSZrlZsgR8aF3JatNV3aMhrYcq8M/XRTLWekalAUkXZ49KaprKLR28XvmSw+5tKtRWvkO7psAcaO7yrX3J9U4zRdorkPeh4wjR5xF/nhNlJ/BchvI9WXZnOFtQsWPEHSHuOjtjtgFa33dnrB13eF/dbeLtfCfe3mSqm7Q62uSqZ72iwL1+FH1yhBLPR5vb3/pmUk3bfmLvPaeyY8n5Mcqm0nXNyQ6YlNdTRJLNcW1oSwMP6Va/f1zRq4Xt4JdPoRS0hJpQsoifY7laLtnq33b3DPPrtfFjmQR9W5rgccs9U7s1iRZbBh9cwzesPmLIOZoGt67zikTxXsouzKcXsqL6rMur6LjPHlRm2cZHd3ZZelDxUZ6AjbYosqT8drZxsYBvvs7Nf++v3s3KCIlL6TuljU1qWN9PwHktpU5dmh9uPP0G+j1l/K6IvioJ3ij4PRS15BfYerbqmLsWdQV8UhVKXj6Kiqs/3cjP544IVnYlMC4WyzA6idR42poI0pKodTDQoY1pN4ak6kXu04GzgaK6xKy3EArZizUFDgw0xa6M/g051PY4QomzsgH5y9Yn9P0HJLU86+63Aq0Yc9Q6QND2oeXXrBLhq3ogDNOOYQUGgq96zEr7TPDzKvl3gmBd8JST6zXDsAjrZyWur2aR2wyYKWr2Bpvpq/kZJmr9nc8pGIuE46ZNNStHy7QFmKCFMFTCzATB5Pe0OMEPd9f7F1zN991yjhK98aBWRbO2eZXm7+p8oQm9yJQSy19dW1z7DKK2tI8Gq2iwFYi8Sn1quZZ9miwxWQ/79kSdgiyDRQSfjQTelgpWSmfRrnSAt8ld5/cwSR8n5LeIGueBFDaN6WlYQICvzZap7IRt6sKMMV2jJmxarWHAOuA1hf/i9RyeeZQMLpeRHS06zaL15NY7kX5HuUxhv/W5eDTXocUGYPMjv55zHtzghcf6q4ZteIqBtyCJjms+8SIAWsYmX9x5K/iMz12lu7Pe8rhvl9rS0w1TUoK5ZsAE47+nA/TcdXXy8/Pvzp5+jzeU/11/+ALPvw7fj3NwmgEAXDmoJILA4bp8Ass093KYb/qiee2ju4XOsUJbjap5j6s0uMuhamucCu0xKeqTv1ZK2lKbTV66IZDm7+zOw5PqmyMhq/Rz1+k9eHhvnZ4vkxFeYETD1UxwM13gIhlG2pR9zL96Y9SoahoQOY0LxjD0C73h+sBkmOJ6XuQFP12tLNoYKttTEWlXqLuz4RiaoVvwPNgFZyZfKYBx+qZl7lHHAvSSf2DrK1kKKwuyp2EMB/V+TiME+AKyLusY+iAb/fqHWVp9eS8su3xMu4wXqNs6wGt9U7CJe0MgAQ12sP379esVKvqHpgpCbF4pUF7NRdkt6xzN4L04i95/F5dnKuwtC/8vwr89Ld/iet/A8FosrhYSLX6R8sFEiNQTll4s7M0qa79OV0dBIzhZG/Ss0GtAtjobZwXCdomS4fSkRjPlOa8wmAMUpe4q0SzvBBpr0c4DlO1hVcTJVcSrKOhcnW/XYXiW87QVap694kbA9BYoGq+2o64Sterfqv10jrLIg4fMxe3ub35hxtO0vzbwM+HgXx/4SR3zgP6LoFvGWOsLVM2Rcm5JxO8KVHW5/uDdX5O2vI8Pz/wM= ================================================ FILE: beetle.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package main import ( "github.com/clivern/beetle/core/cmd" ) var ( version = "dev" commit = "none" date = "unknown" builtBy = "unknown" ) func main() { // Expose build info to cmd subpackage to avoid custom ldflags cmd.Version = version cmd.Commit = commit cmd.Date = date cmd.BuiltBy = builtBy cmd.Execute() } ================================================ FILE: beetle_test.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package main import ( "bytes" "fmt" "io/ioutil" "os" "path/filepath" "testing" "github.com/clivern/beetle/core/module" "github.com/clivern/beetle/pkg" "github.com/drone/envsubst" "github.com/spf13/viper" ) var testingConfig = "config.testing.yml" // TestMain test cases func TestMain(t *testing.T) { // LoadConfigFile t.Run("LoadConfigFile", func(t *testing.T) { fs := module.FileSystem{} dir, _ := os.Getwd() configFile := fmt.Sprintf("%s/%s", dir, testingConfig) for { if fs.FileExists(configFile) { break } dir = filepath.Dir(dir) configFile = fmt.Sprintf("%s/%s", dir, testingConfig) } t.Logf("Load Config File %s", configFile) configUnparsed, _ := ioutil.ReadFile(configFile) configParsed, _ := envsubst.EvalEnv(string(configUnparsed)) viper.SetConfigType("yaml") viper.ReadConfig(bytes.NewBuffer([]byte(configParsed))) pkg.Expect(t, viper.GetString("app.mode"), "test") }) } ================================================ FILE: bin/release.sh ================================================ #!/bin/bash # Fetch latest version export LATEST_VERSION=$(curl --silent "https://api.github.com/repos/clivern/beetle/releases/latest" | jq '.tag_name' | sed -E 's/.*"([^"]+)".*/\1/') # Update go checksum database (sum.golang.org) immediately after release curl --silent https://sum.golang.org/lookup/github.com/clivern/beetle@{$LATEST_VERSION} ================================================ FILE: config.dist.yml ================================================ # App configs app: # Env mode (dev or prod) mode: ${BEETLE_APP_MODE:-dev} # HTTP port port: ${BEETLE_API_PORT:-8080} # App URL domain: ${BEETLE_APP_DOMAIN:-http://127.0.0.1:8080} # TLS configs tls: status: ${BEETLE_API_TLS_STATUS:-off} pemPath: ${BEETLE_API_TLS_PEMPATH:-cert/server.pem} keyPath: ${BEETLE_API_TLS_KEYPATH:-cert/server.key} # Message Broker Configs broker: # Broker driver (native) driver: ${BEETLE_BROKER_DRIVER:-native} # Native driver configs native: # Queue max capacity capacity: ${BEETLE_BROKER_NATIVE_CAPACITY:-5000} # Number of concurrent workers workers: ${BEETLE_BROKER_NATIVE_WORKERS:-4} # API Configs api: key: ${BEETLE_API_KEY:- } # Runtime, Requests/Response and Beetle Metrics metrics: prometheus: # Route for the metrics endpoint endpoint: ${BEETLE_METRICS_PROM_ENDPOINT:-/metrics} # Application Database database: # Database driver (sqlite3, mysql) driver: ${BEETLE_DATABASE_DRIVER:-sqlite3} # Hostname host: ${BEETLE_DATABASE_MYSQL_HOST:-localhost} # Port port: ${BEETLE_DATABASE_MYSQL_PORT:-3306} # Database name: ${BEETLE_DATABASE_MYSQL_DATABASE:-beetle.db} # Username username: ${BEETLE_DATABASE_MYSQL_USERNAME:-root} # Password password: ${BEETLE_DATABASE_MYSQL_PASSWORD:-root} # Kubernetes Clusters clusters: - name: ${BEETLE_KUBE_CLUSTER_01_NAME:-production} inCluster: ${BEETLE_KUBE_CLUSTER_01_IN_CLUSTER:-false} kubeconfig: ${BEETLE_KUBE_CLUSTER_01_CONFIG_FILE:-/app/configs/production-cluster-kubeconfig.yaml} - name: ${BEETLE_KUBE_CLUSTER_02_NAME:-staging} inCluster: ${BEETLE_KUBE_CLUSTER_02_IN_CLUSTER:-false} kubeconfig: ${BEETLE_KUBE_CLUSTER_02_CONFIG_FILE:-/app/configs/staging-cluster-kubeconfig.yaml} # HTTP Webhook webhook: url: ${BEETLE_WEBHOOK_URL:- } retry: ${BEETLE_WEBHOOK_RETRY:-3} apiKey: ${BEETLE_WEBHOOK_API_KEY:- } # Log configs log: # Log level, it can be debug, info, warn, error, panic, fatal level: ${BEETLE_LOG_LEVEL:-info} # output can be stdout or abs path to log file /var/logs/beetle.log output: ${BEETLE_LOG_OUTPUT:-stdout} # Format can be json format: ${BEETLE_LOG_FORMAT:-json} ================================================ FILE: config.testing.yml ================================================ # App configs app: # Env mode (dev or prod) mode: ${BEETLE_APP_MODE:-test} # HTTP port port: ${BEETLE_API_PORT:-8080} # App URL domain: ${BEETLE_APP_DOMAIN:-http://127.0.0.1:8080} # TLS configs tls: status: ${BEETLE_API_TLS_STATUS:-off} pemPath: ${BEETLE_API_TLS_PEMPATH:-cert/server.pem} keyPath: ${BEETLE_API_TLS_KEYPATH:-cert/server.key} # Message Broker Configs broker: # Broker driver (native) driver: ${BEETLE_BROKER_DRIVER:-native} # Native driver configs native: # Queue max capacity capacity: ${BEETLE_BROKER_NATIVE_CAPACITY:-5000} # Number of concurrent workers workers: ${BEETLE_BROKER_NATIVE_WORKERS:-4} # API Configs api: key: ${BEETLE_API_KEY:- } # Runtime, Requests/Response and Beetle Metrics metrics: prometheus: # Route for the metrics endpoint endpoint: ${BEETLE_METRICS_PROM_ENDPOINT:-/metrics} # Application Database database: # Database driver (sqlite3, mysql) driver: ${BEETLE_DATABASE_DRIVER:-sqlite3} # Hostname host: ${BEETLE_DATABASE_MYSQL_HOST:-localhost} # Port port: ${BEETLE_DATABASE_MYSQL_PORT:-3306} # Database name: ${BEETLE_DATABASE_MYSQL_DATABASE:-/tmp/beetle.db} # Username username: ${BEETLE_DATABASE_MYSQL_USERNAME:-root} # Password password: ${BEETLE_DATABASE_MYSQL_PASSWORD:- } # Kubernetes Clusters clusters: - name: ${BEETLE_KUBE_CLUSTER_01_NAME:-production} inCluster: ${BEETLE_KUBE_CLUSTER_01_IN_CLUSTER:-false} kubeconfig: ${BEETLE_KUBE_CLUSTER_01_CONFIG_FILE:-/app/configs/production-cluster-kubeconfig.yaml} - name: ${BEETLE_KUBE_CLUSTER_02_NAME:-staging} inCluster: ${BEETLE_KUBE_CLUSTER_02_IN_CLUSTER:-false} kubeconfig: ${BEETLE_KUBE_CLUSTER_02_CONFIG_FILE:-/app/configs/staging-cluster-kubeconfig.yaml} # HTTP Webhook webhook: url: ${BEETLE_WEBHOOK_URL:- } retry: ${BEETLE_WEBHOOK_RETRY:-3} apiKey: ${BEETLE_WEBHOOK_API_KEY:- } # Log configs log: # Log level, it can be debug, info, warn, error, panic, fatal level: ${BEETLE_LOG_LEVEL:-info} # output can be stdout or abs path to log file /var/logs/beetle.log output: ${BEETLE_LOG_OUTPUT:-stdout} # Format can be json format: ${BEETLE_LOG_FORMAT:-json} ================================================ FILE: config.toml ================================================ ignoreGeneratedHeader = false severity = "warning" confidence = 0.8 errorCode = 0 warningCode = 0 [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] [rule.empty-block] [rule.superfluous-else] [rule.unused-parameter] [rule.unreachable-code] [rule.redefines-builtin-id] ================================================ FILE: core/cmd/apps.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package cmd import ( "context" "fmt" "os" "github.com/clivern/beetle/core/module" "github.com/clivern/beetle/sdk" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" ) var ( // Beetle API Server URL apiURL string // Beetle API Server API Key apiKey string // The Kubernetes Cluster cluster string // The Kubernetes Cluster Namespace namespace string ) var getCmd = &cobra.Command{ Use: "get", Short: "Get resources", Run: func(cmd *cobra.Command, args []string) { fmt.Println(`You must specify the type of resource to get. Current supported resources are (apps).`) }, } var appsCmd = &cobra.Command{ Use: "apps", Short: "Get a list of applications with cluster id and namespace", Run: func(cmd *cobra.Command, aras []string) { // Usage // $ ./beetle get apps -u "http://localhost:8080" -k "" -c "production" -n "default" data := [][]string{} client := sdk.Client{} client.SetHTTPClient(module.NewHTTPClient(20)) client.SetAPIURL(apiURL) client.SetAPIKey(apiKey) apps, err := client.GetApplications(context.TODO(), cluster, namespace) if err != nil { data = append(data, []string{ fmt.Sprintf("Error: %s", err.Error()), "", "", "", }) } else { for _, app := range apps.Applications { version := "N/A" if len(app.Containers) > 0 { version = app.Containers[0].Version } data = append(data, []string{ app.ID, app.Name, fmt.Sprintf("%d", len(app.Containers)), version, }) } } table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"ID", "Name", "Containers", "Version"}) table.SetAutoWrapText(false) table.SetAutoFormatHeaders(true) table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) table.SetAlignment(tablewriter.ALIGN_LEFT) table.SetCenterSeparator("") table.SetColumnSeparator("") table.SetRowSeparator("") table.SetHeaderLine(false) table.SetBorder(false) table.SetTablePadding("\t") table.SetNoWhiteSpace(true) table.AppendBulk(data) table.Render() }, } func init() { rootCmd.AddCommand(getCmd) appsCmd.Flags().StringVarP(&namespace, "namespace", "n", "default", "The Kubernetes Cluster Namespace (eg. default)") appsCmd.MarkFlagRequired("namespace") appsCmd.Flags().StringVarP(&cluster, "cluster", "c", "", "The Kubernetes Cluster (eg. production)") appsCmd.MarkFlagRequired("cluster") appsCmd.Flags().StringVarP(&apiKey, "api_key", "k", "", "API Key of the Beetle API Server") appsCmd.MarkFlagRequired("api_key") appsCmd.Flags().StringVarP(&apiURL, "api_url", "u", "", "Beetle API Server URL (eg. https://example.com/)") appsCmd.MarkFlagRequired("api_url") getCmd.AddCommand(appsCmd) } ================================================ FILE: core/cmd/deploy.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package cmd import ( "context" "fmt" "time" "github.com/clivern/beetle/core/model" "github.com/clivern/beetle/core/module" "github.com/clivern/beetle/sdk" "github.com/briandowns/spinner" "github.com/logrusorgru/aurora/v3" "github.com/spf13/cobra" ) var ( // Application ID application string // Application Version version string // Deployment Strategy strategy string // Ramped Strategy MaxSurge maxSurge string // Ramped Strategy MaxUnavailable maxUnavailable string // Whether to watch the deployment watch bool ) var deployCmd = &cobra.Command{ Use: "deploy", Short: "Deploy a new application version", Run: func(cmd *cobra.Command, aras []string) { // Usage // $ ./beetle deploy -u "http://localhost:8080" -k "" -c "production" -n "default" -a "toad" -s "recreate" -v "0.2.3" -w client := sdk.Client{} client.SetHTTPClient(module.NewHTTPClient(20)) client.SetAPIURL(apiURL) client.SetAPIKey(apiKey) spin := spinner.New(spinner.CharSets[26], 100*time.Millisecond) spin.Color("green") spin.Start() job, err := client.CreateDeployment(context.TODO(), model.DeploymentRequest{ Cluster: cluster, Namespace: namespace, Application: application, Version: version, Strategy: strategy, MaxSurge: maxSurge, MaxUnavailable: maxUnavailable, }) if err != nil { fmt.Println(aurora.Red(fmt.Sprintf("Error: %s", err.Error()))) spin.Stop() return } if watch { for { job, err := client.GetJob(context.TODO(), job.UUID) if err != nil { fmt.Println(aurora.Red(fmt.Sprintf("Error: %s", err.Error()))) spin.Stop() return } if job.Status == model.JobFailed { fmt.Println(aurora.Red(fmt.Sprintf( "Deployment Request %s Failed!", job.UUID, ))) spin.Stop() return } if job.Status == model.JobSuccess { fmt.Println(aurora.Green(fmt.Sprintf( "Deployment Request %s Succeeded!", job.UUID, ))) spin.Stop() return } time.Sleep(2 * time.Second) } } else { fmt.Println(aurora.Green(fmt.Sprintf( "Deployment Request %s Submitted Successfully!", job.UUID, ))) } spin.Stop() }, } func init() { deployCmd.Flags().StringVarP(&application, "application", "a", "", "The Application ID") deployCmd.MarkFlagRequired("application") deployCmd.Flags().StringVarP(&version, "version", "v", "", "The Application Version") deployCmd.MarkFlagRequired("version") deployCmd.Flags().StringVarP(&strategy, "strategy", "s", "recreate", "The Deployment Strategy (recreate, ramped, canary or blue_green)") deployCmd.MarkFlagRequired("strategy") deployCmd.Flags().StringVarP(&maxSurge, "max_surge", "g", "50%", "Deployment Strategy MaxSurge") deployCmd.Flags().StringVarP(&maxUnavailable, "max_unavailable", "b", "50%", "Deployment Strategy MaxUnavailable") deployCmd.Flags().StringVarP(&namespace, "namespace", "n", "default", "The Kubernetes Cluster Namespace (eg. default)") deployCmd.MarkFlagRequired("namespace") deployCmd.Flags().StringVarP(&cluster, "cluster", "c", "", "The Kubernetes Cluster (eg. production)") deployCmd.MarkFlagRequired("cluster") deployCmd.Flags().StringVarP(&apiKey, "api_key", "k", "", "API Key of the Beetle API Server") deployCmd.MarkFlagRequired("api_key") deployCmd.Flags().StringVarP(&apiURL, "api_url", "u", "", "Beetle API Server URL (eg. https://example.com/)") deployCmd.MarkFlagRequired("api_url") deployCmd.Flags().BoolVarP(&watch, "watch", "w", false, "Watch the deployment") rootCmd.AddCommand(deployCmd) } ================================================ FILE: core/cmd/license.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package cmd import ( "fmt" "github.com/spf13/cobra" ) var licenseCmd = &cobra.Command{ Use: "license", Short: "Get License", Run: func(cmd *cobra.Command, args []string) { fmt.Println(`MIT License Copyright (c) 2020 Clivern Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`) }, } func init() { rootCmd.AddCommand(licenseCmd) } ================================================ FILE: core/cmd/root.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package cmd import ( "fmt" "os" "github.com/spf13/cobra" ) var rootCmd = &cobra.Command{ Use: "beetle", Short: `🔥 Kubernetes multi-cluster deployment automation service Beetle is in early stages of development, and we'd love to hear your feedback at `, } // Execute runs cmd tool func Execute() { if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) } } ================================================ FILE: core/cmd/serve.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package cmd import ( "bytes" "fmt" "io" "io/ioutil" "net/http" "os" "path/filepath" "strconv" "strings" "github.com/clivern/beetle/core/controller" "github.com/clivern/beetle/core/middleware" "github.com/clivern/beetle/core/module" "github.com/drone/envsubst" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" ) var config string var serveCmd = &cobra.Command{ Use: "serve", Short: "Start beetle server", Run: func(cmd *cobra.Command, args []string) { configUnparsed, err := ioutil.ReadFile(config) if err != nil { panic(fmt.Sprintf( "Error while reading config file [%s]: %s", config, err.Error(), )) } configParsed, err := envsubst.EvalEnv(string(configUnparsed)) if err != nil { panic(fmt.Sprintf( "Error while parsing config file [%s]: %s", config, err.Error(), )) } viper.SetConfigType("yaml") err = viper.ReadConfig(bytes.NewBufferString(configParsed)) if err != nil { panic(fmt.Sprintf( "Error while loading configs [%s]: %s", config, err.Error(), )) } if viper.GetString("log.output") != "stdout" { fs := module.FileSystem{} dir, _ := filepath.Split(viper.GetString("log.output")) if !fs.DirExists(dir) { if _, err := fs.EnsureDir(dir, 0775); err != nil { panic(fmt.Sprintf( "Directory [%s] creation failed with error: %s", dir, err.Error(), )) } } if !fs.FileExists(viper.GetString("log.output")) { f, err := os.Create(viper.GetString("log.output")) if err != nil { panic(fmt.Sprintf( "Error while creating log file [%s]: %s", viper.GetString("log.output"), err.Error(), )) } defer f.Close() } } if viper.GetString("log.output") == "stdout" { gin.DefaultWriter = os.Stdout log.SetOutput(os.Stdout) } else { f, _ := os.OpenFile( viper.GetString("log.output"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0775, ) gin.DefaultWriter = io.MultiWriter(f) log.SetOutput(f) } lvl := strings.ToLower(viper.GetString("log.level")) level, err := log.ParseLevel(lvl) if err != nil { level = log.InfoLevel } log.SetLevel(level) if viper.GetString("app.mode") == "prod" { gin.SetMode(gin.ReleaseMode) gin.DefaultWriter = ioutil.Discard gin.DisableConsoleColor() } if viper.GetString("log.format") == "json" { log.SetFormatter(&log.JSONFormatter{}) } else { log.SetFormatter(&log.TextFormatter{}) } // Init DB Connection db := module.Database{} err = db.AutoConnect() if err != nil { panic(err.Error()) } // Migrate Database success := db.Migrate() if !success { panic("Error! Unable to migrate database tables.") } defer db.Close() messages := make(chan string, viper.GetInt("app.broker.native.capacity")) r := gin.Default() r.Use(middleware.Correlation()) r.Use(middleware.Auth()) r.Use(middleware.Logger()) r.Use(middleware.Metric()) r.GET("/favicon.ico", func(c *gin.Context) { c.String(http.StatusNoContent, "") }) r.GET("/", controller.HealthCheck) r.GET("/_health", controller.HealthCheck) r.GET("/_ready", controller.ReadyCheck) r.GET(viper.GetString("app.metrics.prometheus.endpoint"), gin.WrapH(controller.Metrics())) r.GET("/api/v1/cluster", controller.Clusters) r.GET("/api/v1/cluster/:cn", controller.Cluster) r.GET("/api/v1/cluster/:cn/namespace", controller.Namespaces) r.GET("/api/v1/cluster/:cn/namespace/:ns", controller.Namespace) r.GET("/api/v1/cluster/:cn/namespace/:ns/app", controller.Applications) r.GET("/api/v1/cluster/:cn/namespace/:ns/app/:id", controller.Application) r.POST("/api/v1/cluster/:cn/namespace/:ns/app/:id/deployment", func(c *gin.Context) { controller.CreateDeployment(c, messages) }) r.GET("/api/v1/job", controller.Jobs) r.GET("/api/v1/job/:uuid", controller.GetJob) r.DELETE("/api/v1/job/:uuid", controller.DeleteJob) for i := 0; i < viper.GetInt("app.broker.native.workers"); i++ { go controller.Worker(i+1, messages) } go controller.Daemon() var runerr error if viper.GetBool("app.tls.status") { runerr = r.RunTLS( fmt.Sprintf(":%s", strconv.Itoa(viper.GetInt("app.port"))), viper.GetString("app.tls.pemPath"), viper.GetString("app.tls.keyPath"), ) } else { runerr = r.Run( fmt.Sprintf(":%s", strconv.Itoa(viper.GetInt("app.port"))), ) } if runerr != nil { panic(runerr.Error()) } }, } func init() { serveCmd.Flags().StringVarP(&config, "config", "c", "config.prod.yml", "Absolute path to config file (required)") serveCmd.MarkFlagRequired("config") rootCmd.AddCommand(serveCmd) } ================================================ FILE: core/cmd/version.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package cmd import ( "fmt" "github.com/clivern/beetle/core/module" "github.com/spf13/cobra" ) var ( // Version buildinfo item Version = "dev" // Commit buildinfo item Commit = "none" // Date buildinfo item Date = "unknown" // BuiltBy buildinfo item BuiltBy = "unknown" ) var versionCmd = &cobra.Command{ Use: "version", Short: "Get current and latest version", Run: func(cmd *cobra.Command, args []string) { fmt.Println( fmt.Sprintf( `Current Beetle Version %v Commit %v, Built @%v By %v.`, Version, Commit, Date, BuiltBy, ), ) latest, err := module.GetLatestRelease() if err != nil { fmt.Printf("Error: %s \n", err.Error()) return } fmt.Printf( "Latest release %s, Latest tag %s \n", latest.Name, latest.TagName, ) }, } func init() { rootCmd.AddCommand(versionCmd) } ================================================ FILE: core/controller/application.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package controller import ( "context" "net/http" "github.com/clivern/beetle/core/kubernetes" "github.com/clivern/beetle/core/model" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" ) // Application controller func Application(c *gin.Context) { cn := c.Param("cn") ns := c.Param("ns") id := c.Param("id") config := model.Configs{} cluster, err := kubernetes.GetCluster(cn) if err != nil { log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "cluster_name": cn, "error": err.Error(), }).Info(`Cluster not found`) c.Status(http.StatusNotFound) return } config, err = cluster.GetConfig(context.TODO(), ns) if err != nil { log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "cluster_name": cn, "namespace_name": ns, "error": err.Error(), }).Warn(`Error while fetching beetle configMap`) } for _, app := range config.Applications { if app.ID == id { application, err := cluster.GetApplication( context.TODO(), ns, app.ID, app.Name, app.ImageFormat, ) if err != nil { log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "application_id": id, "cluster_name": cn, "namespace_name": ns, "error": err.Error(), }).Warn(`Error while fetching application current version`) } c.JSON(http.StatusOK, gin.H{ "id": application.ID, "name": application.Name, "format": application.Format, "containers": application.Containers, }) return } } log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "application_id": id, "cluster_name": cn, "namespace_name": ns, }).Info(`Application not found`) c.Status(http.StatusNotFound) } ================================================ FILE: core/controller/applications.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package controller import ( "context" "net/http" "github.com/clivern/beetle/core/kubernetes" "github.com/clivern/beetle/core/model" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" ) // Applications controller func Applications(c *gin.Context) { cn := c.Param("cn") ns := c.Param("ns") config := model.Configs{} cluster, err := kubernetes.GetCluster(cn) if err != nil { log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "cluster_name": cn, "error": err.Error(), }).Info(`Cluster not found`) c.Status(http.StatusNotFound) return } config, err = cluster.GetConfig(context.TODO(), ns) if err != nil { log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "cluster_name": cn, "namespace_name": ns, "error": err.Error(), }).Warn(`Error while fetching beetle configMap`) } applications := []model.Application{} for _, app := range config.Applications { application, err := cluster.GetApplication( context.TODO(), ns, app.ID, app.Name, app.ImageFormat, ) if err != nil { log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "application_id": app.ID, "cluster_name": cn, "namespace_name": ns, "error": err.Error(), }).Warn(`Error while fetching application current version`) continue } applications = append(applications, application) } c.JSON(http.StatusOK, gin.H{ "applications": applications, }) } ================================================ FILE: core/controller/cluster.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package controller import ( "context" "net/http" "github.com/clivern/beetle/core/kubernetes" "github.com/clivern/beetle/core/model" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" ) // Cluster controller func Cluster(c *gin.Context) { cn := c.Param("cn") result := model.Cluster{} cluster, err := kubernetes.GetCluster(cn) if err != nil { log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "cluster_name": cn, "error": err.Error(), }).Info(`Cluster not found`) c.Status(http.StatusNotFound) return } status, err := cluster.Ping(context.TODO()) if err != nil { log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "cluster_name": cn, "error": err.Error(), }).Error(`Error ping a cluster`) } result.Name = cluster.Name result.Health = status c.JSON(http.StatusOK, gin.H{ "name": result.Name, "health": result.Health, }) } ================================================ FILE: core/controller/clusters.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package controller import ( "context" "net/http" "github.com/clivern/beetle/core/kubernetes" "github.com/clivern/beetle/core/model" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" ) // Clusters controller func Clusters(c *gin.Context) { result := []model.Cluster{} clusters, err := kubernetes.GetClusters() if err != nil { log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "error": err.Error(), }).Error(`Error fetching clusters`) c.Status(http.StatusInternalServerError) return } var status bool for _, cluster := range clusters { status, err = cluster.Ping(context.TODO()) if err != nil { log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "cluster_name": cluster.Name, "error": err.Error(), }).Error(`Error while ping a cluster`) } result = append(result, model.Cluster{ Name: cluster.Name, Health: status, }) } c.JSON(http.StatusOK, gin.H{ "clusters": result, }) } ================================================ FILE: core/controller/daemon.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package controller import ( "context" "fmt" "net/http" "strconv" "time" "github.com/clivern/beetle/core/model" "github.com/clivern/beetle/core/module" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" "github.com/spf13/viper" ) var ( pendingJobs = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: "beetle", Name: "workers_queue_pending_jobs", Help: "The pending jobs in the queue", }) failedJobs = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: "beetle", Name: "workers_queue_failed_jobs", Help: "The failed jobs in the queue", }) successJobs = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: "beetle", Name: "workers_queue_success_jobs", Help: "The successful jobs in the queue", }) onHoldJobs = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: "beetle", Name: "workers_queue_on_hold_jobs", Help: "The on hold jobs in the queue", }) ) func init() { prometheus.MustRegister(pendingJobs) prometheus.MustRegister(failedJobs) prometheus.MustRegister(successJobs) prometheus.MustRegister(onHoldJobs) } // Daemon function func Daemon() { var err error var pendingJobsCount int var failedJobsCount int var successfulJobsCount int var onHoldJobsCount int var job model.Job var parentJob model.Job var deploymentRequest model.DeploymentRequest var payload string httpClient := module.NewHTTPClient(20) db := module.Database{} retry, err := strconv.Atoi(viper.GetString("app.webhook.retry")) if err != nil { panic(err.Error()) } for { err = db.AutoConnect() if err != nil { log.WithFields(log.Fields{ "correlation_id": "", "error": err.Error(), }).Error(`Failure while connecting database`) time.Sleep(2 * time.Second) continue } // Update Metrics pendingJobsCount = db.CountJobs(model.JobPending) failedJobsCount = db.CountJobs(model.JobFailed) successfulJobsCount = db.CountJobs(model.JobSuccess) onHoldJobsCount = db.CountJobs(model.JobOnHold) log.WithFields(log.Fields{ "correlation_id": "", "pending_jobs_count": pendingJobsCount, "failed_jobs_count": failedJobsCount, "successful_jobs_count": successfulJobsCount, "on_hold_jobs_count": onHoldJobsCount, }).Debug(`Update metrics`) pendingJobs.Set(float64(pendingJobsCount)) failedJobs.Set(float64(failedJobsCount)) successJobs.Set(float64(successfulJobsCount)) onHoldJobs.Set(float64(onHoldJobsCount)) // Run Pending Jobs (HTTP Notification) job = db.GetPendingJobByType(model.JobDeploymentNotify) if job.ID > 0 { if job.Retry > retry { now := time.Now() job.Status = model.JobFailed job.RunAt = &now job.Result = fmt.Sprintf("Failed to deliver the notification") db.UpdateJobByID(&job) } else { deploymentRequest.LoadFromJSON([]byte(job.Payload)) if job.Parent > 0 { parentJob = db.GetJobByID(job.Parent) if parentJob.ID > 0 { deploymentRequest.Status = parentJob.Status } } payload, _ = deploymentRequest.ConvertToJSON() response, err := httpClient.Post( context.TODO(), viper.GetString("app.webhook.url"), payload, map[string]string{}, map[string]string{ "Content-Type": "application/json", "X-API-KEY": viper.GetString("app.webhook.apiKey"), "X-NOTIFICATION-ID": job.UUID, "X-ACTION-NAME": job.Type, "X-DEPLOYMENT-ID": parentJob.UUID, }, ) if httpClient.GetStatusCode(response) != http.StatusOK || err != nil { job.Status = model.JobFailed job.Result = fmt.Sprintf("Failed to deliver the notification") } else { job.Status = model.JobSuccess job.Result = fmt.Sprintf("Notification delivered successfully") } if job.Status == model.JobFailed && job.Retry <= retry { job.Status = model.JobPending } now := time.Now() job.Retry++ job.RunAt = &now db.UpdateJobByID(&job) } } time.Sleep(2 * time.Second) } } ================================================ FILE: core/controller/deployment.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package controller import ( "net/http" "github.com/clivern/beetle/core/model" "github.com/clivern/beetle/core/module" "github.com/clivern/beetle/core/util" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" ) // CreateDeployment controller func CreateDeployment(c *gin.Context, messages chan<- string) { rawBody, _ := c.GetRawData() deploymentRequest := model.DeploymentRequest{} _, err := deploymentRequest.LoadFromJSON(rawBody) deploymentRequest.Cluster = c.Param("cn") deploymentRequest.Namespace = c.Param("ns") deploymentRequest.Application = c.Param("id") if err != nil { log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "error": err.Error(), }).Info(`Invalid request`) c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid request!", }) return } err = deploymentRequest.Validate([]string{ model.RecreateStrategy, model.RampedStrategy, model.CanaryStrategy, model.BlueGreenStrategy, }) if err != nil { log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "error": err.Error(), }).Info(`Invalid request`) c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error(), }) return } if deploymentRequest.MaxSurge == "" { deploymentRequest.MaxSurge = "25%" } if deploymentRequest.MaxUnavailable == "" { deploymentRequest.MaxUnavailable = "25%" } result, err := deploymentRequest.ConvertToJSON() if err != nil { log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "error": err.Error(), }).Info(`Invalid request`) c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error(), }) return } // Then create async job db := module.Database{} err = db.AutoConnect() if err != nil { log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "error": err.Error(), }).Error(`Failure while connecting database`) c.Status(http.StatusInternalServerError) return } defer db.Close() uuid := util.GenerateUUID4() for db.JobExistByUUID(uuid) { uuid = util.GenerateUUID4() } job := db.CreateJob(&model.Job{ UUID: uuid, Payload: result, Status: model.JobPending, Parent: 0, Type: model.JobDeploymentUpdate, }) messageObj := model.Message{ UUID: c.Request.Header.Get("X-Correlation-ID"), Job: job.ID, } message, _ := messageObj.ConvertToJSON() // Send the job to workers messages <- message c.JSON(http.StatusAccepted, gin.H{ "id": job.ID, "uuid": job.UUID, "type": job.Type, "status": job.Status, "createdAt": job.CreatedAt, }) } ================================================ FILE: core/controller/health_check.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package controller import ( "net/http" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" ) // HealthCheck controller func HealthCheck(c *gin.Context) { status := "ok" log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "status": status, }).Info(`Health check`) c.JSON(http.StatusOK, gin.H{ "status": status, }) } ================================================ FILE: core/controller/health_check_test.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package controller import ( "bytes" "fmt" "io/ioutil" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "github.com/clivern/beetle/core/module" "github.com/clivern/beetle/pkg" "github.com/drone/envsubst" "github.com/gin-gonic/gin" "github.com/spf13/viper" ) // TestHealthCheck test cases func TestHealthCheck(t *testing.T) { testingConfig := "config.testing.yml" // LoadConfigFile t.Run("LoadConfigFile", func(t *testing.T) { fs := module.FileSystem{} dir, _ := os.Getwd() configFile := fmt.Sprintf("%s/%s", dir, testingConfig) for { if fs.FileExists(configFile) { break } dir = filepath.Dir(dir) configFile = fmt.Sprintf("%s/%s", dir, testingConfig) } t.Logf("Load Config File %s", configFile) configUnparsed, _ := ioutil.ReadFile(configFile) configParsed, _ := envsubst.EvalEnv(string(configUnparsed)) viper.SetConfigType("yaml") viper.ReadConfig(bytes.NewBuffer([]byte(configParsed))) }) // TestHealthCheckController t.Run("TestHealthCheckController", func(t *testing.T) { gin.SetMode(gin.ReleaseMode) gin.DefaultWriter = ioutil.Discard gin.DisableConsoleColor() router := gin.Default() router.GET("/_health", HealthCheck) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/_health", nil) router.ServeHTTP(w, req) pkg.Expect(t, viper.GetString("app.mode"), "test") pkg.Expect(t, w.Code, 200) pkg.Expect(t, strings.TrimSpace(w.Body.String()), `{"status":"ok"}`) }) } ================================================ FILE: core/controller/job.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package controller import ( "fmt" "net/http" "github.com/clivern/beetle/core/module" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" ) // GetJob controller func GetJob(c *gin.Context) { uuid := c.Param("uuid") db := module.Database{} err := db.AutoConnect() if err != nil { log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "error": err.Error(), }).Error(`Failure while connecting database`) c.Status(http.StatusInternalServerError) return } defer db.Close() job := db.GetJobByUUID(uuid) if job.ID < 1 { log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "job_uuid": uuid, }).Info(fmt.Sprintf(`Job not found`)) c.Status(http.StatusNotFound) return } log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "job_uuid": uuid, }).Info(`Retrieve a job`) c.JSON(http.StatusOK, gin.H{ "id": job.ID, "uuid": job.UUID, "status": job.Status, "type": job.Type, "runAt": job.RunAt, "createdAt": job.CreatedAt, "updatedAt": job.UpdatedAt, }) } // DeleteJob controller func DeleteJob(c *gin.Context) { uuid := c.Param("uuid") db := module.Database{} err := db.AutoConnect() if err != nil { log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "error": err.Error(), }).Error(`Failure while connecting database`) c.Status(http.StatusInternalServerError) return } defer db.Close() job := db.GetJobByUUID(uuid) if job.ID < 1 { log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "job_uuid": uuid, }).Info(`Job not found`) c.Status(http.StatusNotFound) return } log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "job_uuid": uuid, }).Info(`Deleting a job`) db.DeleteJobByID(job.ID) c.Status(http.StatusNoContent) return } ================================================ FILE: core/controller/jobs.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package controller import ( "net/http" "github.com/clivern/beetle/core/module" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" ) // Jobs controller func Jobs(c *gin.Context) { db := module.Database{} err := db.AutoConnect() if err != nil { log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "error": err.Error(), }).Error(`Failure while connecting database`) c.Status(http.StatusInternalServerError) return } defer db.Close() c.JSON(http.StatusOK, gin.H{ "jobs": db.GetJobs(), }) } ================================================ FILE: core/controller/metrics.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package controller import ( "net/http" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/spf13/viper" ) var ( workersCount = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: "beetle", Name: "workers_count", Help: "Number of Async Workers", }) queueCapacity = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: "beetle", Name: "workers_queue_capacity", Help: "The maximum number of messages queue can process", }) ) func init() { prometheus.MustRegister(workersCount) prometheus.MustRegister(queueCapacity) } // Metrics controller func Metrics() http.Handler { workersCount.Set(float64(viper.GetInt("app.broker.native.workers"))) queueCapacity.Set(float64(viper.GetInt("app.broker.native.capacity"))) return promhttp.Handler() } ================================================ FILE: core/controller/namespace.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package controller import ( "context" "net/http" "github.com/clivern/beetle/core/kubernetes" "github.com/clivern/beetle/core/model" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" ) // Namespace controller func Namespace(c *gin.Context) { cn := c.Param("cn") ns := c.Param("ns") result := model.Namespace{} clusters, err := kubernetes.GetClusters() if err != nil { log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "error": err.Error(), }).Error(`Failure to get clusters`) c.Status(http.StatusInternalServerError) return } for _, cluster := range clusters { if cn != cluster.Name { continue } result, err = cluster.GetNamespace(context.TODO(), ns) if err != nil { log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "namespace_name": ns, "cluster_name": cn, "error": err.Error(), }).Error(`Failure to get cluster namespace`) } } if result.Name == "" { c.Status(http.StatusNotFound) return } c.JSON(http.StatusOK, gin.H{ "name": result.Name, "uid": result.UID, "status": result.Status, }) } ================================================ FILE: core/controller/namespaces.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package controller import ( "context" "net/http" "github.com/clivern/beetle/core/kubernetes" "github.com/clivern/beetle/core/model" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" ) // Namespaces controller func Namespaces(c *gin.Context) { cn := c.Param("cn") result := []model.Namespace{} clusters, err := kubernetes.GetClusters() if err != nil { log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "error": err.Error(), }).Error(`Failure to get clusters`) c.Status(http.StatusInternalServerError) return } for _, cluster := range clusters { if cn != cluster.Name { continue } result, err = cluster.GetNamespaces(context.TODO()) if err != nil { log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "error": err.Error(), "cluster_name": cn, }).Error(`Failure to get cluster namespaces`) } } c.JSON(http.StatusOK, gin.H{ "namespaces": result, }) } ================================================ FILE: core/controller/ready_check.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package controller import ( "net/http" "github.com/clivern/beetle/core/module" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" ) // ReadyCheck controller func ReadyCheck(c *gin.Context) { status := "ok" db := module.Database{} err := db.AutoConnect() if err != nil { status = "down" log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "status": status, "error": err.Error(), }).Error(`Failed ready check`) c.Status(http.StatusInternalServerError) return } err = db.Ping() if err != nil { status = "down" log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "status": status, "error": err.Error(), }).Error(`Failed ready check`) c.Status(http.StatusInternalServerError) return } defer db.Close() log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "status": status, }).Info(`Passed ready check`) c.JSON(http.StatusOK, gin.H{ "status": status, }) } ================================================ FILE: core/controller/ready_check_test.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package controller import ( "bytes" "fmt" "io/ioutil" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "github.com/clivern/beetle/core/module" "github.com/clivern/beetle/pkg" "github.com/drone/envsubst" "github.com/gin-gonic/gin" "github.com/spf13/viper" ) // TestReadyCheck test cases func TestReadyCheck(t *testing.T) { testingConfig := "config.testing.yml" // LoadConfigFile t.Run("LoadConfigFile", func(t *testing.T) { fs := module.FileSystem{} dir, _ := os.Getwd() configFile := fmt.Sprintf("%s/%s", dir, testingConfig) for { if fs.FileExists(configFile) { break } dir = filepath.Dir(dir) configFile = fmt.Sprintf("%s/%s", dir, testingConfig) } t.Logf("Load Config File %s", configFile) configUnparsed, _ := ioutil.ReadFile(configFile) configParsed, _ := envsubst.EvalEnv(string(configUnparsed)) viper.SetConfigType("yaml") viper.ReadConfig(bytes.NewBuffer([]byte(configParsed))) }) // TestReadyCheckController t.Run("TestReadyCheckController", func(t *testing.T) { gin.SetMode(gin.ReleaseMode) gin.DefaultWriter = ioutil.Discard gin.DisableConsoleColor() router := gin.Default() router.GET("/_ready", ReadyCheck) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/_ready", nil) router.ServeHTTP(w, req) pkg.Expect(t, viper.GetString("app.mode"), "test") pkg.Expect(t, w.Code, 200) pkg.Expect(t, strings.TrimSpace(w.Body.String()), `{"status":"ok"}`) }) } ================================================ FILE: core/controller/worker.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package controller import ( "context" "fmt" "strings" "time" "github.com/clivern/beetle/core/kubernetes" "github.com/clivern/beetle/core/model" "github.com/clivern/beetle/core/module" "github.com/clivern/beetle/core/util" log "github.com/sirupsen/logrus" "github.com/spf13/viper" ) // Worker controller func Worker(workerID int, messages <-chan string) { var ok bool var err error var job model.Job var cluster *kubernetes.Cluster var uuid string messageObj := model.Message{} deploymentRequest := model.DeploymentRequest{} log.WithFields(log.Fields{ "correlation_id": util.GenerateUUID4(), "worker_id": workerID, }).Info(`Worker started`) db := module.Database{} for message := range messages { ok, err = messageObj.LoadFromJSON([]byte(message)) if !ok || err != nil { log.WithFields(log.Fields{ "correlation_id": messageObj.UUID, "worker_id": workerID, "message": message, }).Warn(`Worker received invalid message`) continue } log.WithFields(log.Fields{ "correlation_id": messageObj.UUID, "worker_id": workerID, "job_id": messageObj.Job, }).Info(`Worker received a new job`) err = db.AutoConnect() if err != nil { log.WithFields(log.Fields{ "correlation_id": messageObj.UUID, "worker_id": workerID, "error": err.Error(), }).Error(`Worker unable to connect to database`) continue } job = db.GetJobByID(messageObj.Job) ok, err = deploymentRequest.LoadFromJSON([]byte(job.Payload)) if !ok || err != nil { log.WithFields(log.Fields{ "correlation_id": messageObj.UUID, "worker_id": workerID, "job_id": messageObj.Job, "job_uuid": job.UUID, "error": err.Error(), }).Error(`Invalid job payload`) // Job Failed now := time.Now() job.Status = model.JobFailed job.RunAt = &now job.Result = fmt.Sprintf("Invalid job payload, UUID %s", messageObj.UUID) db.UpdateJobByID(&job) db.ReleaseChildJobs(job.ID) continue } log.WithFields(log.Fields{ "correlation_id": messageObj.UUID, "worker_id": workerID, "job_id": messageObj.Job, "job_uuid": job.UUID, "deployment_request": deploymentRequest, }).Info(`Worker accepted deployment request`) // Notify if there is a webhook if strings.TrimSpace(viper.GetString("app.webhook.url")) != "" { uuid = util.GenerateUUID4() for db.JobExistByUUID(uuid) { uuid = util.GenerateUUID4() } db.CreateJob(&model.Job{ UUID: uuid, Payload: job.Payload, Status: model.JobOnHold, Parent: messageObj.Job, Type: model.JobDeploymentNotify, }) log.WithFields(log.Fields{ "correlation_id": messageObj.UUID, "worker_id": workerID, "job_id": messageObj.Job, "job_uuid": job.UUID, "deployment_request": deploymentRequest, "webhook_url": viper.GetString("app.webhook.url"), }).Info(`HTTP webhook enabled`) } else { log.WithFields(log.Fields{ "correlation_id": messageObj.UUID, "worker_id": workerID, "job_id": messageObj.Job, "job_uuid": job.UUID, "deployment_request": deploymentRequest, }).Info(`HTTP webhook disabled`) } cluster, err = kubernetes.GetCluster(deploymentRequest.Cluster) if err != nil { log.WithFields(log.Fields{ "correlation_id": messageObj.UUID, "worker_id": workerID, "error": err.Error(), "deployment_request": deploymentRequest, }).Error(`Worker can not find the cluster`) // Job Failed now := time.Now() job.Status = model.JobFailed job.RunAt = &now job.Result = fmt.Sprintf("Worker can not find the cluster, UUID %s", messageObj.UUID) db.UpdateJobByID(&job) db.ReleaseChildJobs(job.ID) continue } ok, err = cluster.Ping(context.TODO()) if !ok || err != nil { log.WithFields(log.Fields{ "correlation_id": messageObj.UUID, "worker_id": workerID, "error": err.Error(), "deployment_request": deploymentRequest, }).Error(`Worker unable to ping cluster`) // Job Failed now := time.Now() job.Status = model.JobFailed job.RunAt = &now job.Result = fmt.Sprintf("Worker unable to ping cluster, UUID %s", messageObj.UUID) db.UpdateJobByID(&job) db.ReleaseChildJobs(job.ID) continue } ok, err = cluster.Deploy(deploymentRequest) if !ok || err != nil { log.WithFields(log.Fields{ "correlation_id": messageObj.UUID, "worker_id": workerID, "error": err.Error(), "deployment_request": deploymentRequest, }).Error(`Worker unable deploy`) // Job Failed now := time.Now() job.Status = model.JobFailed job.RunAt = &now job.Result = fmt.Sprintf("Failure during deployment, UUID %s", messageObj.UUID) db.UpdateJobByID(&job) db.ReleaseChildJobs(job.ID) continue } log.WithFields(log.Fields{ "correlation_id": messageObj.UUID, "worker_id": workerID, "deployment_request": deploymentRequest, }).Info(`Deployment finished successfully`) // Job Succeeded now := time.Now() job.Status = model.JobSuccess job.RunAt = &now job.Result = "Deployment finished successfully" db.UpdateJobByID(&job) db.ReleaseChildJobs(job.ID) } } ================================================ FILE: core/kubernetes/application.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package kubernetes import ( "context" "fmt" "strings" "github.com/clivern/beetle/core/model" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // GetApplication gets current application version func (c *Cluster) GetApplication(ctx context.Context, namespace, id, name, format string) (model.Application, error) { result := model.Application{} err := c.Config() if err != nil { return result, err } data, err := c.ClientSet.AppsV1().Deployments(namespace).List(ctx, metav1.ListOptions{ LabelSelector: fmt.Sprintf( "%s=%s,%s=%s", "beetle.clivern.com/status", "enabled", "beetle.clivern.com/application-id", id, ), }) if err != nil { return result, err } result.ID = id result.Name = name result.Format = format result.Containers = []model.Container{} for _, deployment := range data.Items { for _, container := range deployment.Spec.Template.Spec.Containers { result.Containers = append(result.Containers, model.Container{ Name: container.Name, Image: container.Image, Version: strings.Replace( container.Image, strings.Replace(format, "[.Release]", "", -1), "", -1, ), Deployment: model.Deployment{ Name: deployment.ObjectMeta.Name, UID: string(deployment.ObjectMeta.UID), }, }) } } return result, nil } ================================================ FILE: core/kubernetes/cluster.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package kubernetes import ( "context" "fmt" "github.com/clivern/beetle/core/module" "github.com/spf13/viper" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" ) // Clusters struct type Clusters struct { Clusters []*Cluster `mapstructure:",clusters"` } // Cluster struct type Cluster struct { Name string `mapstructure:",name"` Kubeconfig string `mapstructure:",kubeconfig"` InCluster bool `mapstructure:",inCluster"` ClientSet kubernetes.Interface Fake bool } // GetClusters get a list of clusters func GetClusters() ([]*Cluster, error) { var clusters Clusters err := viper.UnmarshalKey("app", &clusters) if err != nil { return nil, err } return clusters.Clusters, nil } // GetCluster get a list of clusters func GetCluster(name string) (*Cluster, error) { var clusters Clusters err := viper.UnmarshalKey("app", &clusters) if err != nil { return nil, err } for _, cluster := range clusters.Clusters { if name == cluster.Name { return cluster, nil } } return &Cluster{}, fmt.Errorf("Unable to find cluster %s", name) } // Override overrides the client set for testing func (c *Cluster) Override(objects ...runtime.Object) { c.Fake = true c.ClientSet = fake.NewSimpleClientset(objects...) } // Config configs the client set for testing func (c *Cluster) Config() error { if c.Fake { return nil } var config *rest.Config var err error if !c.InCluster { fs := module.FileSystem{} if !fs.FileExists(c.Kubeconfig) { return fmt.Errorf( "cluster [%s] config file [%s] not exist", c.Name, c.Kubeconfig, ) } config, err = clientcmd.BuildConfigFromFlags("", c.Kubeconfig) if err != nil { return err } } else { config, err = rest.InClusterConfig() if err != nil { return err } } clientset, err := kubernetes.NewForConfig(config) if err != nil { return err } c.ClientSet = clientset return nil } // Ping check the cluster func (c *Cluster) Ping(ctx context.Context) (bool, error) { err := c.Config() if err != nil { return false, err } data, err := c.ClientSet.CoreV1().RESTClient().Get().AbsPath("/api/v1").DoRaw(ctx) if err != nil { return false, err } return (string(data) != ""), nil } ================================================ FILE: core/kubernetes/cluster_test.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package kubernetes import ( "bytes" "fmt" "io/ioutil" "os" "path/filepath" "testing" "github.com/clivern/beetle/core/module" "github.com/clivern/beetle/pkg" "github.com/drone/envsubst" "github.com/spf13/viper" ) // TestCluster test cases func TestCluster(t *testing.T) { testingConfig := "config.testing.yml" // LoadConfigFile t.Run("LoadConfigFile", func(t *testing.T) { fs := module.FileSystem{} dir, _ := os.Getwd() configFile := fmt.Sprintf("%s/%s", dir, testingConfig) for { if fs.FileExists(configFile) { break } dir = filepath.Dir(dir) configFile = fmt.Sprintf("%s/%s", dir, testingConfig) } t.Logf("Load Config File %s", configFile) configUnparsed, _ := ioutil.ReadFile(configFile) configParsed, _ := envsubst.EvalEnv(string(configUnparsed)) viper.SetConfigType("yaml") viper.ReadConfig(bytes.NewBuffer([]byte(configParsed))) }) // TestGetClusters t.Run("TestGetClusters", func(t *testing.T) { clusters, err := GetClusters() pkg.Expect(t, nil, err) pkg.Expect(t, clusters[0].Name, "production") pkg.Expect(t, clusters[0].Kubeconfig, "/app/configs/production-cluster-kubeconfig.yaml") pkg.Expect(t, clusters[1].Name, "staging") pkg.Expect(t, clusters[1].Kubeconfig, "/app/configs/staging-cluster-kubeconfig.yaml") }) // TestGetCluster t.Run("TestGetCluster", func(t *testing.T) { cluster, err := GetCluster("production") pkg.Expect(t, nil, err) pkg.Expect(t, cluster.Name, "production") pkg.Expect(t, cluster.Kubeconfig, "/app/configs/production-cluster-kubeconfig.yaml") cluster, err = GetCluster("not-found") pkg.Expect(t, fmt.Errorf("Unable to find cluster not-found"), err) pkg.Expect(t, cluster.Name, "") pkg.Expect(t, cluster.Kubeconfig, "") }) } ================================================ FILE: core/kubernetes/config.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package kubernetes import ( "context" "fmt" "github.com/clivern/beetle/core/model" log "github.com/sirupsen/logrus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // GetConfig gets a beetle configs for a specific namespace func (c *Cluster) GetConfig(ctx context.Context, namespace string) (model.Configs, error) { result := model.Configs{} err := c.Config() if err != nil { return result, err } data, err := c.ClientSet.AppsV1().Deployments(namespace).List(ctx, metav1.ListOptions{ LabelSelector: fmt.Sprintf( "%s=%s", "beetle.clivern.com/status", "enabled", ), }) if err != nil { return result, err } for _, deployment := range data.Items { applicationName := "" imageFormat := "" applicationID := "" status := "disabled" for key, value := range deployment.ObjectMeta.Annotations { if key == "beetle.clivern.com/application-name" { applicationName = value } if key == "beetle.clivern.com/image-format" { imageFormat = value } } for key, value := range deployment.ObjectMeta.Labels { if key == "beetle.clivern.com/status" { status = value } if key == "beetle.clivern.com/application-id" { applicationID = value } } if status == "enabled" && applicationID != "" && imageFormat != "" { result.Applications = append(result.Applications, model.App{ ID: applicationID, Name: applicationName, ImageFormat: imageFormat, }) } else { log.WithFields(log.Fields{ "application_id": applicationID, }).Debug(`Application status disabled`) } } result.Exists = true return result, nil } ================================================ FILE: core/kubernetes/configmap.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package kubernetes import ( "context" "github.com/clivern/beetle/core/model" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // GetConfigMap gets a configmap data func (c *Cluster) GetConfigMap(ctx context.Context, namespace, name string) (model.ConfigMap, error) { result := model.ConfigMap{} err := c.Config() if err != nil { return result, err } configmap, err := c.ClientSet.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { return result, err } result.Name = configmap.ObjectMeta.Name result.Namespace = configmap.ObjectMeta.Namespace result.UID = string(configmap.ObjectMeta.UID) result.CreationTimestamp = configmap.ObjectMeta.CreationTimestamp.String() result.Data = configmap.Data result.Labels = configmap.ObjectMeta.Labels return result, nil } ================================================ FILE: core/kubernetes/deployment.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package kubernetes import ( "context" "fmt" "time" "github.com/clivern/beetle/core/model" log "github.com/sirupsen/logrus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" ) // GetDeployments gets a list of deployments func (c *Cluster) GetDeployments(ctx context.Context, namespace, label string) ([]model.Deployment, error) { result := []model.Deployment{} err := c.Config() if err != nil { return result, err } data, err := c.ClientSet.AppsV1().Deployments(namespace).List(ctx, metav1.ListOptions{ LabelSelector: label, }) if err != nil { return result, err } for _, deployment := range data.Items { result = append(result, model.Deployment{ Name: deployment.ObjectMeta.Name, UID: string(deployment.ObjectMeta.UID), }) } return result, nil } // GetDeployment gets a deployment by name func (c *Cluster) GetDeployment(ctx context.Context, namespace, name string) (model.Deployment, error) { result := model.Deployment{} err := c.Config() if err != nil { return result, err } deployment, err := c.ClientSet.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { return result, err } result.Name = deployment.ObjectMeta.Name result.UID = string(deployment.ObjectMeta.UID) return result, nil } // PatchDeployment updates the deployment func (c *Cluster) PatchDeployment(ctx context.Context, namespace, name, data string) (bool, error) { err := c.Config() if err != nil { return false, err } _, err = c.ClientSet.AppsV1().Deployments(namespace).Patch( ctx, name, types.JSONPatchType, []byte(data), metav1.PatchOptions{}, ) if err != nil { return false, err } return true, nil } // FetchDeploymentStatus get deployment status func (c *Cluster) FetchDeploymentStatus(ctx context.Context, namespace, name string, limit int) (bool, error) { err := c.Config() if err != nil { return false, err } // Wait till k8s pick the deployment time.Sleep(10 * time.Second) deployment, err := c.ClientSet.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { return false, err } status := true for i := 0; i < limit; i++ { status = true deployment, err = c.ClientSet.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { return false, err } if int(deployment.Generation) != int(deployment.Status.ObservedGeneration) { status = false } if int(deployment.Status.UnavailableReplicas) > 0 { status = false } if int(int32(*deployment.Spec.Replicas)) != int(deployment.Status.AvailableReplicas) { status = false } if !status { log.WithFields(log.Fields{ "deployment.Generation": int(deployment.Generation), "deployment.Status.ObservedGeneration": int(deployment.Status.ObservedGeneration), "deployment.Spec.Replicas": int(int32(*deployment.Spec.Replicas)), "deployment.Status.AvailableReplicas": int(deployment.Status.AvailableReplicas), "deployment.Status.UnavailableReplicas": int(deployment.Status.UnavailableReplicas), }).Debug(`Deployment Success`) time.Sleep(2 * time.Second) } else { log.WithFields(log.Fields{ "deployment.Generation": int(deployment.Generation), "deployment.Status.ObservedGeneration": int(deployment.Status.ObservedGeneration), "deployment.Spec.Replicas": int(int32(*deployment.Spec.Replicas)), "deployment.Status.AvailableReplicas": int(deployment.Status.AvailableReplicas), "deployment.Status.UnavailableReplicas": int(deployment.Status.UnavailableReplicas), }).Debug(`Deployment Success`) return true, nil } } log.WithFields(log.Fields{ "deployment.Generation": int(deployment.Generation), "deployment.Status.ObservedGeneration": int(deployment.Status.ObservedGeneration), "deployment.Spec.Replicas": int(int32(*deployment.Spec.Replicas)), "deployment.Status.AvailableReplicas": int(deployment.Status.AvailableReplicas), "deployment.Status.UnavailableReplicas": int(deployment.Status.UnavailableReplicas), }).Debug(`Deployment failure`) return false, fmt.Errorf(fmt.Sprintf( "Deployment %s failed: namespace %s, Generation %d, ObservedGeneration %d,"+ " UnavailableReplicas %d, Replicas %d, AvailableReplicas %d", name, namespace, int(deployment.Generation), int(deployment.Status.ObservedGeneration), int(deployment.Status.UnavailableReplicas), int(int32(*deployment.Spec.Replicas)), int(deployment.Status.AvailableReplicas), )) } ================================================ FILE: core/kubernetes/deployment_strategy.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package kubernetes import ( "context" "fmt" "strconv" "strings" "github.com/clivern/beetle/core/model" "github.com/clivern/beetle/core/util" ) // Deploy deploys an application func (c *Cluster) Deploy(deploymentRequest model.DeploymentRequest) (bool, error) { switch strategy := deploymentRequest.Strategy; strategy { case model.RecreateStrategy: return c.RecreateStrategy(deploymentRequest) case model.RampedStrategy: return c.RampedStrategy(deploymentRequest) case model.CanaryStrategy: return c.CanaryStrategy(deploymentRequest) case model.BlueGreenStrategy: return c.BlueGreenStrategy(deploymentRequest) default: return false, fmt.Errorf("Invalid deployment strategy %s", strategy) } } // RecreateStrategy terminates the old version and release the new one. // // # This method is like running this command // // $ kubectl patch deployment toad-deployment --type=json -p '[ // // {"op":"replace", "path":"/spec/strategy", "value":{"type":"Recreate"}}, // {"op":"replace","path":"/spec/template/spec/containers/0/image","value":"clivern/toad:release-0.2.4"} // // ]' func (c *Cluster) RecreateStrategy(deploymentRequest model.DeploymentRequest) (bool, error) { result := model.Application{} patch := make(map[string][]model.PatchStringValue) config, err := c.GetConfig(context.TODO(), deploymentRequest.Namespace) if err != nil { return false, err } for _, app := range config.Applications { if app.ID == deploymentRequest.Application { result, err = c.GetApplication( context.TODO(), deploymentRequest.Namespace, app.ID, app.Name, app.ImageFormat, ) if err != nil { return false, err } break } } i := 0 for _, container := range result.Containers { if _, ok := patch[container.Deployment.Name]; !ok { patch[container.Deployment.Name] = []model.PatchStringValue{} } patch[container.Deployment.Name] = append(patch[container.Deployment.Name], model.PatchStringValue{ Op: "replace", Path: fmt.Sprintf("/spec/template/spec/containers/%d/image", i), Value: strings.Replace(container.Image, container.Version, deploymentRequest.Version, -1), }) i++ } data := "" status := true for deploymentName, deploymentPatch := range patch { data, err = util.ConvertToJSON(deploymentPatch) if err != nil { return false, err } // Enforce Recreate strategy data = strings.Replace( data, `[`, `[{"op":"replace","path":"/spec/strategy","value":{"type":"Recreate"}},`, -1, ) status, err = c.PatchDeployment( context.TODO(), deploymentRequest.Namespace, deploymentName, data, ) if !status || err != nil { return false, err } } for deploymentName := range patch { status, err = c.FetchDeploymentStatus(context.TODO(), deploymentRequest.Namespace, deploymentName, 600) if !status || err != nil { return false, err } } return true, nil } // RampedStrategy releases a new version on a rolling update fashion, one after the other. // // it will set maxSurge as 25% and maxUnavailable as 25% // // # This method is like running this command // // $ kubectl patch deployment toad-deployment --type=json -p '[ // // {"op":"replace", "path":"/spec/strategy", "value":{"type":"RollingUpdate"}}, // {"op":"replace", "path":"/spec/strategy/rollingUpdate", "value":{"maxSurge":""}}, // {"op":"replace", "path":"/spec/strategy/rollingUpdate", "value":{"maxUnavailable":""}}, // {"op":"replace","path":"/spec/template/spec/containers/0/image","value":"clivern/toad:release-0.2.4"} // // ]' func (c *Cluster) RampedStrategy(deploymentRequest model.DeploymentRequest) (bool, error) { result := model.Application{} patch := make(map[string][]model.PatchStringValue) config, err := c.GetConfig(context.TODO(), deploymentRequest.Namespace) if err != nil { return false, err } for _, app := range config.Applications { if app.ID == deploymentRequest.Application { result, err = c.GetApplication( context.TODO(), deploymentRequest.Namespace, app.ID, app.Name, app.ImageFormat, ) if err != nil { return false, err } break } } i := 0 for _, container := range result.Containers { if _, ok := patch[container.Deployment.Name]; !ok { patch[container.Deployment.Name] = []model.PatchStringValue{} } patch[container.Deployment.Name] = append(patch[container.Deployment.Name], model.PatchStringValue{ Op: "replace", Path: fmt.Sprintf("/spec/template/spec/containers/%d/image", i), Value: strings.Replace(container.Image, container.Version, deploymentRequest.Version, -1), }) i++ } data := "" status := true for deploymentName, deploymentPatch := range patch { data, err = util.ConvertToJSON(deploymentPatch) if err != nil { return false, err } diff := "" if strings.Contains(deploymentRequest.MaxSurge, "%") && strings.Contains(deploymentRequest.MaxUnavailable, "%") { diff = fmt.Sprintf( `[{"op":"replace","path":"/spec/strategy","value":{"type":"RollingUpdate"}},`+ `{"op":"replace", "path":"/spec/strategy/rollingUpdate", "value":{"maxSurge":"%s"}},`+ `{"op":"replace", "path":"/spec/strategy/rollingUpdate", "value":{"maxUnavailable":"%s"}},`, deploymentRequest.MaxSurge, deploymentRequest.MaxUnavailable, ) } else { maxSurge, err := strconv.Atoi(deploymentRequest.MaxSurge) if err != nil { return false, err } maxUnavailable, err := strconv.Atoi(deploymentRequest.MaxUnavailable) if err != nil { return false, err } diff = fmt.Sprintf( `[{"op":"replace","path":"/spec/strategy","value":{"type":"RollingUpdate"}},`+ `{"op":"replace", "path":"/spec/strategy/rollingUpdate", "value":{"maxSurge":%d}},`+ `{"op":"replace", "path":"/spec/strategy/rollingUpdate", "value":{"maxUnavailable":%d}},`, maxSurge, maxUnavailable, ) } // Enforce RollingUpdate strategy data = strings.Replace( data, `[`, diff, -1, ) status, err = c.PatchDeployment( context.TODO(), deploymentRequest.Namespace, deploymentName, data, ) if !status || err != nil { return false, err } } for deploymentName := range patch { status, err = c.FetchDeploymentStatus(context.TODO(), deploymentRequest.Namespace, deploymentName, 1000) if !status || err != nil { return false, err } } return true, nil } // BlueGreenStrategy releases a new version alongside the old version then switch traffic. func (c *Cluster) BlueGreenStrategy(_ model.DeploymentRequest) (bool, error) { return true, nil } // CanaryStrategy releases a new version to a subset of users, then proceed to a full rollout. func (c *Cluster) CanaryStrategy(_ model.DeploymentRequest) (bool, error) { return true, nil } ================================================ FILE: core/kubernetes/namespace.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package kubernetes import ( "context" "strings" "github.com/clivern/beetle/core/model" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // GetNamespaces gets a list of cluster namespaces func (c *Cluster) GetNamespaces(ctx context.Context) ([]model.Namespace, error) { result := []model.Namespace{} err := c.Config() if err != nil { return result, err } data, err := c.ClientSet.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) if err != nil { return result, err } for _, namespace := range data.Items { result = append(result, model.Namespace{ Name: namespace.ObjectMeta.Name, UID: string(namespace.ObjectMeta.UID), Status: strings.ToLower(string(namespace.Status.Phase)), }) } return result, nil } // GetNamespace gets a namespace by name func (c *Cluster) GetNamespace(ctx context.Context, name string) (model.Namespace, error) { result := model.Namespace{} err := c.Config() if err != nil { return result, err } namespace, err := c.ClientSet.CoreV1().Namespaces().Get(ctx, name, metav1.GetOptions{}) if err != nil { return result, err } result.Name = namespace.ObjectMeta.Name result.UID = string(namespace.ObjectMeta.UID) result.Status = strings.ToLower(string(namespace.Status.Phase)) return result, nil } ================================================ FILE: core/kubernetes/namespace_test.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package kubernetes import ( "bytes" "context" "fmt" "io/ioutil" "os" "path/filepath" "testing" "github.com/clivern/beetle/core/module" "github.com/clivern/beetle/pkg" "github.com/drone/envsubst" "github.com/spf13/viper" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // TestNamespace test cases func TestNamespace(t *testing.T) { testingConfig := "config.testing.yml" // LoadConfigFile t.Run("LoadConfigFile", func(t *testing.T) { fs := module.FileSystem{} dir, _ := os.Getwd() configFile := fmt.Sprintf("%s/%s", dir, testingConfig) for { if fs.FileExists(configFile) { break } dir = filepath.Dir(dir) configFile = fmt.Sprintf("%s/%s", dir, testingConfig) } t.Logf("Load Config File %s", configFile) configUnparsed, _ := ioutil.ReadFile(configFile) configParsed, _ := envsubst.EvalEnv(string(configUnparsed)) viper.SetConfigType("yaml") viper.ReadConfig(bytes.NewBuffer([]byte(configParsed))) }) // TestGetNamespaces t.Run("TestGetNamespaces", func(t *testing.T) { cluster, err := GetCluster("production") pkg.Expect(t, nil, err) pkg.Expect(t, cluster.Name, "production") pkg.Expect(t, cluster.Kubeconfig, "/app/configs/production-cluster-kubeconfig.yaml") cluster.Override( &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: "default", UID: "9d0cdf8a-dedc-11e9-bf91-42010a800167", }, }, &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: "beetle", UID: "9d0cdf8a-dedc-11e9-bf91-42010a800168", }, }, ) namespaces, err := cluster.GetNamespaces(context.TODO()) pkg.Expect(t, nil, err) pkg.Expect(t, namespaces[1].Name, "default") pkg.Expect(t, namespaces[1].UID, "9d0cdf8a-dedc-11e9-bf91-42010a800167") pkg.Expect(t, namespaces[1].Status, "") pkg.Expect(t, namespaces[0].Name, "beetle") pkg.Expect(t, namespaces[0].UID, "9d0cdf8a-dedc-11e9-bf91-42010a800168") pkg.Expect(t, namespaces[0].Status, "") }) // TestGetNamespaceBeetle t.Run("TestGetNamespaceBeetle", func(t *testing.T) { cluster, err := GetCluster("production") pkg.Expect(t, nil, err) pkg.Expect(t, cluster.Name, "production") pkg.Expect(t, cluster.Kubeconfig, "/app/configs/production-cluster-kubeconfig.yaml") cluster.Override( &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: "default", UID: "9d0cdf8a-dedc-11e9-bf91-42010a800167", }, }, &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: "beetle", UID: "9d0cdf8a-dedc-11e9-bf91-42010a800168", }, }, ) namespace, err := cluster.GetNamespace(context.TODO(), "beetle") pkg.Expect(t, nil, err) pkg.Expect(t, namespace.Name, "beetle") pkg.Expect(t, namespace.UID, "9d0cdf8a-dedc-11e9-bf91-42010a800168") pkg.Expect(t, namespace.Status, "") }) // TestGetNamespaceDefault t.Run("TestGetNamespaceDefault", func(t *testing.T) { cluster, err := GetCluster("production") pkg.Expect(t, nil, err) pkg.Expect(t, cluster.Name, "production") pkg.Expect(t, cluster.Kubeconfig, "/app/configs/production-cluster-kubeconfig.yaml") cluster.Override( &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: "default", UID: "9d0cdf8a-dedc-11e9-bf91-42010a800167", }, }, &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: "beetle", UID: "9d0cdf8a-dedc-11e9-bf91-42010a800168", }, }, ) namespace, err := cluster.GetNamespace(context.TODO(), "default") pkg.Expect(t, nil, err) pkg.Expect(t, namespace.Name, "default") pkg.Expect(t, namespace.UID, "9d0cdf8a-dedc-11e9-bf91-42010a800167") pkg.Expect(t, namespace.Status, "") }) } ================================================ FILE: core/kubernetes/pod.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package kubernetes ================================================ FILE: core/middleware/auth.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package middleware import ( "net/http" "strings" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" "github.com/spf13/viper" ) // Auth middleware func Auth() gin.HandlerFunc { return func(c *gin.Context) { path := c.Request.URL.Path method := c.Request.Method if strings.Contains(path, "/api/") { apiKey := c.GetHeader("X-API-KEY") if viper.GetString("app.api.key") != "" && apiKey != viper.GetString("app.api.key") { log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "http_method": method, "http_path": path, "api_key": apiKey, }).Info(`Unauthorized access`) c.AbortWithStatus(http.StatusUnauthorized) } } } } ================================================ FILE: core/middleware/correlation.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package middleware import ( "strings" "github.com/clivern/beetle/core/util" "github.com/gin-gonic/gin" ) // Correlation middleware func Correlation() gin.HandlerFunc { return func(c *gin.Context) { corralationID := c.Request.Header.Get("X-Correlation-ID") if strings.TrimSpace(corralationID) == "" { c.Request.Header.Add("X-Correlation-ID", util.GenerateUUID4()) } c.Next() } } ================================================ FILE: core/middleware/log.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package middleware import ( "bytes" "io/ioutil" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" ) // Logger middleware func Logger() gin.HandlerFunc { return func(c *gin.Context) { // before request var bodyBytes []byte // Workaround for issue https://github.com/gin-gonic/gin/issues/1651 if c.Request.Body != nil { bodyBytes, _ = ioutil.ReadAll(c.Request.Body) } c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "http_method": c.Request.Method, "http_path": c.Request.URL.Path, "request_body": string(bodyBytes), }).Info("Request started") c.Next() // after request status := c.Writer.Status() size := c.Writer.Size() log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), "http_status": status, "response_size": size, }).Info(`Request finished`) } } ================================================ FILE: core/middleware/metric.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package middleware import ( "strconv" "time" "github.com/gin-gonic/gin" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" ) var ( httpRequests = prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: "beetle", Name: "total_http_requests", Help: "How many HTTP requests processed, partitioned by status code and HTTP method.", }, []string{"code", "method", "handler", "host", "url"}) requestDuration = prometheus.NewHistogramVec( prometheus.HistogramOpts{ Subsystem: "beetle", Name: "request_duration_seconds", Help: "The HTTP request latencies in seconds.", }, []string{"code", "method", "url"}, ) responseSize = prometheus.NewSummary( prometheus.SummaryOpts{ Namespace: "beetle", Name: "response_size_bytes", Help: "The HTTP response sizes in bytes.", }, ) ) func init() { prometheus.MustRegister(httpRequests) prometheus.MustRegister(requestDuration) prometheus.MustRegister(responseSize) } // Metric middleware func Metric() gin.HandlerFunc { return func(c *gin.Context) { // before request start := time.Now() c.Next() // after request elapsed := float64(time.Since(start)) / float64(time.Second) log.WithFields(log.Fields{ "correlation_id": c.Request.Header.Get("X-Correlation-ID"), }).Info(`Collecting metrics`) // Collect Metrics httpRequests.WithLabelValues( strconv.Itoa(c.Writer.Status()), c.Request.Method, c.HandlerName(), c.Request.Host, c.Request.URL.Path, ).Inc() requestDuration.WithLabelValues( strconv.Itoa(c.Writer.Status()), c.Request.Method, c.Request.URL.Path, ).Observe(elapsed) responseSize.Observe(float64(c.Writer.Size())) } } ================================================ FILE: core/migration/schema.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package migration import ( "encoding/json" "time" "github.com/jinzhu/gorm" ) // Job struct type Job struct { gorm.Model UUID string `json:"uuid"` Payload string `json:"payload"` Status string `json:"status"` Type string `json:"type"` Result string `json:"result"` Retry int `json:"retry"` Parent int `json:"parent"` RunAt time.Time `json:"run_at"` } // LoadFromJSON update object from json func (j *Job) LoadFromJSON(data []byte) (bool, error) { err := json.Unmarshal(data, &j) if err != nil { return false, err } return true, nil } // ConvertToJSON convert object to json func (j *Job) ConvertToJSON() (string, error) { data, err := json.Marshal(&j) if err != nil { return "", err } return string(data), nil } ================================================ FILE: core/model/application.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package model import ( "encoding/json" ) // Container struct type Container struct { Name string `json:"name"` Image string `json:"image"` Version string `json:"version"` Deployment Deployment `json:"deployment"` } // Application struct type Application struct { ID string `json:"id"` Name string `json:"name"` Format string `json:"format"` Containers []Container `json:"containers"` } // Applications struct type Applications struct { Applications []Application `json:"applications"` } // Deployment struct type Deployment struct { Name string `json:"name"` UID string `json:"uid"` } // LoadFromJSON update object from json func (c *Application) LoadFromJSON(data []byte) (bool, error) { err := json.Unmarshal(data, &c) if err != nil { return false, err } return true, nil } // ConvertToJSON convert object to json func (c *Application) ConvertToJSON() (string, error) { data, err := json.Marshal(&c) if err != nil { return "", err } return string(data), nil } // LoadFromJSON update object from json func (c *Applications) LoadFromJSON(data []byte) (bool, error) { err := json.Unmarshal(data, &c) if err != nil { return false, err } return true, nil } // ConvertToJSON convert object to json func (c *Applications) ConvertToJSON() (string, error) { data, err := json.Marshal(&c) if err != nil { return "", err } return string(data), nil } // LoadFromJSON update object from json func (d *Deployment) LoadFromJSON(data []byte) (bool, error) { err := json.Unmarshal(data, &d) if err != nil { return false, err } return true, nil } // ConvertToJSON convert object to json func (d *Deployment) ConvertToJSON() (string, error) { data, err := json.Marshal(&d) if err != nil { return "", err } return string(data), nil } ================================================ FILE: core/model/cluster.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package model import ( "encoding/json" ) // Cluster struct type Cluster struct { Name string `json:"name"` Health bool `json:"health"` } // Clusters struct type Clusters struct { Clusters []Cluster `json:"clusters"` } // LoadFromJSON update object from json func (c *Cluster) LoadFromJSON(data []byte) (bool, error) { err := json.Unmarshal(data, &c) if err != nil { return false, err } return true, nil } // ConvertToJSON convert object to json func (c *Cluster) ConvertToJSON() (string, error) { data, err := json.Marshal(&c) if err != nil { return "", err } return string(data), nil } // LoadFromJSON update object from json func (c *Clusters) LoadFromJSON(data []byte) (bool, error) { err := json.Unmarshal(data, &c) if err != nil { return false, err } return true, nil } // ConvertToJSON convert object to json func (c *Clusters) ConvertToJSON() (string, error) { data, err := json.Marshal(&c) if err != nil { return "", err } return string(data), nil } ================================================ FILE: core/model/configmap.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package model import ( "encoding/json" ) // ConfigMap struct type ConfigMap struct { Name string `json:"name"` Namespace string `json:"namespace"` UID string `json:"uid"` CreationTimestamp string `json:"creation_timestamp"` Data map[string]string `json:"data"` Labels map[string]string `json:"labels"` } // LoadFromJSON update object from json func (d *ConfigMap) LoadFromJSON(data []byte) (bool, error) { err := json.Unmarshal(data, &d) if err != nil { return false, err } return true, nil } // ConvertToJSON convert object to json func (d *ConfigMap) ConvertToJSON() (string, error) { data, err := json.Marshal(&d) if err != nil { return "", err } return string(data), nil } ================================================ FILE: core/model/configs.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package model // App struct type App struct { ID string Name string ImageFormat string } // Configs struct type Configs struct { Exists bool Version string Applications []App } ================================================ FILE: core/model/dsn.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package model import ( "encoding/json" "fmt" ) // DSN struct type DSN struct { Driver string `json:"driver"` Username string `json:"username"` Password string `json:"password"` Hostname string `json:"hostname"` Port int `json:"port"` Name string `json:"name"` } // ToString gets the dsn string func (d *DSN) ToString() string { if d.Driver == "mysql" { return fmt.Sprintf( "%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True", d.Username, d.Password, d.Hostname, d.Port, d.Name, ) } // sqlite3 by default return d.Name } // LoadFromJSON update object from json func (d *DSN) LoadFromJSON(data []byte) (bool, error) { err := json.Unmarshal(data, &d) if err != nil { return false, err } return true, nil } // ConvertToJSON convert object to json func (d *DSN) ConvertToJSON() (string, error) { data, err := json.Marshal(&d) if err != nil { return "", err } return string(data), nil } ================================================ FILE: core/model/dsn_test.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package model import ( "testing" "github.com/clivern/beetle/pkg" ) // TestDsnToString test cases func TestDsnToString(t *testing.T) { t.Run("TestDsnToStringForMySQL", func(t *testing.T) { dsn := DSN{ Driver: "mysql", Username: "root", Password: "root", Hostname: "127.0.0.1", Port: 3306, Name: "beetle", } pkg.Expect(t, "root:root@tcp(127.0.0.1:3306)/beetle?charset=utf8&parseTime=True", dsn.ToString()) }) t.Run("TestDsnToStringForSQLLite", func(t *testing.T) { dsn := DSN{ Driver: "sqlite3", Name: "/path/to/beetle.db", } pkg.Expect(t, "/path/to/beetle.db", dsn.ToString()) }) } ================================================ FILE: core/model/job.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package model import ( "encoding/json" "time" ) var ( // JobPending pending job type JobPending = "pending" // JobFailed failed job type JobFailed = "failed" // JobSuccess success job type JobSuccess = "success" // JobOnHold on hold job type JobOnHold = "on_hold" // JobDeploymentUpdate deployment update JobDeploymentUpdate = "deployment.update" // JobDeploymentNotify deployment notify JobDeploymentNotify = "deployment.notify" ) // Job struct type Job struct { ID int `json:"id"` UUID string `json:"uuid"` Payload string `json:"payload"` Status string `json:"status"` Type string `json:"type"` Result string `json:"result"` Retry int `json:"retry"` Parent int `json:"parent"` RunAt *time.Time `json:"run_at"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // Jobs struct type Jobs struct { Jobs []Job `json:"jobs"` } // LoadFromJSON update object from json func (j *Job) LoadFromJSON(data []byte) (bool, error) { err := json.Unmarshal(data, &j) if err != nil { return false, err } return true, nil } // ConvertToJSON convert object to json func (j *Job) ConvertToJSON() (string, error) { data, err := json.Marshal(&j) if err != nil { return "", err } return string(data), nil } // LoadFromJSON update object from json func (j *Jobs) LoadFromJSON(data []byte) (bool, error) { err := json.Unmarshal(data, &j) if err != nil { return false, err } return true, nil } // ConvertToJSON convert object to json func (j *Jobs) ConvertToJSON() (string, error) { data, err := json.Marshal(&j) if err != nil { return "", err } return string(data), nil } ================================================ FILE: core/model/message.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package model import ( "encoding/json" ) // Message struct type Message struct { UUID string `json:"uuid"` Job int `json:"job"` } // LoadFromJSON update object from json func (m *Message) LoadFromJSON(data []byte) (bool, error) { err := json.Unmarshal(data, &m) if err != nil { return false, err } return true, nil } // ConvertToJSON convert object to json func (m *Message) ConvertToJSON() (string, error) { data, err := json.Marshal(&m) if err != nil { return "", err } return string(data), nil } ================================================ FILE: core/model/metric.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package model import ( "encoding/json" "strconv" "github.com/prometheus/client_golang/prometheus" ) const ( // COUNTER is a Prometheus COUNTER metric COUNTER string = "counter" // GAUGE is a Prometheus GAUGE metric GAUGE string = "gauge" // HISTOGRAM is a Prometheus HISTOGRAM metric HISTOGRAM string = "histogram" // SUMMARY is a Prometheus SUMMARY metric SUMMARY string = "summary" ) // Metric struct type Metric struct { Type string `json:"type"` Name string `json:"name"` Help string `json:"help"` Method string `json:"method"` Value string `json:"value"` Labels prometheus.Labels `json:"labels"` Buckets []float64 `json:"buckets"` } // LoadFromJSON update object from json func (m *Metric) LoadFromJSON(data []byte) (bool, error) { err := json.Unmarshal(data, &m) if err != nil { return false, err } return true, nil } // ConvertToJSON convert object to json func (m *Metric) ConvertToJSON() (string, error) { data, err := json.Marshal(&m) if err != nil { return "", err } return string(data), nil } // LabelKeys gets a list of label keys func (m *Metric) LabelKeys() []string { keys := []string{} for k := range m.Labels { keys = append(keys, k) } return keys } // LabelValues gets a list of label values func (m *Metric) LabelValues() []string { values := []string{} for _, v := range m.Labels { values = append(values, v) } return values } // GetValueAsFloat gets a list of label values func (m *Metric) GetValueAsFloat() (float64, error) { value, err := strconv.ParseFloat(m.Value, 64) if err != nil { return 0, nil } return value, nil } ================================================ FILE: core/model/migration.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package model import ( "encoding/json" "time" ) // Migration struct type Migration struct { ID int `json:"id"` Flag string `json:"file"` RunAt time.Time `json:"run_at"` } // LoadFromJSON update object from json func (m *Migration) LoadFromJSON(data []byte) (bool, error) { err := json.Unmarshal(data, &m) if err != nil { return false, err } return true, nil } // ConvertToJSON convert object to json func (m *Migration) ConvertToJSON() (string, error) { data, err := json.Marshal(&m) if err != nil { return "", err } return string(data), nil } ================================================ FILE: core/model/namespace.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package model import ( "encoding/json" ) // Namespace struct type Namespace struct { Name string `json:"name"` UID string `json:"uid"` Status string `json:"status"` } // Namespaces struct type Namespaces struct { Namespaces []Namespace `json:"namespaces"` } // LoadFromJSON update object from json func (d *Namespace) LoadFromJSON(data []byte) (bool, error) { err := json.Unmarshal(data, &d) if err != nil { return false, err } return true, nil } // ConvertToJSON convert object to json func (d *Namespace) ConvertToJSON() (string, error) { data, err := json.Marshal(&d) if err != nil { return "", err } return string(data), nil } // LoadFromJSON update object from json func (d *Namespaces) LoadFromJSON(data []byte) (bool, error) { err := json.Unmarshal(data, &d) if err != nil { return false, err } return true, nil } // ConvertToJSON convert object to json func (d *Namespaces) ConvertToJSON() (string, error) { data, err := json.Marshal(&d) if err != nil { return "", err } return string(data), nil } ================================================ FILE: core/model/patch.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package model // PatchStringValue specifies a patch operation for a string. type PatchStringValue struct { Op string `json:"op"` Path string `json:"path"` Value string `json:"value"` } // PatchUInt32Value specifies a patch operation for a uint32. type PatchUInt32Value struct { Op string `json:"op"` Path string `json:"path"` Value uint32 `json:"value"` } ================================================ FILE: core/model/request.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package model import ( "encoding/json" "fmt" "reflect" ) var ( // RecreateStrategy var RecreateStrategy = "recreate" // RampedStrategy var RampedStrategy = "ramped" // CanaryStrategy var CanaryStrategy = "canary" // BlueGreenStrategy var BlueGreenStrategy = "blue_green" ) // DeploymentRequest struct type DeploymentRequest struct { Cluster string `json:"cluster"` Namespace string `json:"namespace"` Application string `json:"application"` Version string `json:"version"` Strategy string `json:"strategy"` Status string `json:"status"` // Ramped Strategy MaxSurge string `json:"maxSurge"` MaxUnavailable string `json:"maxUnavailable"` } // LoadFromJSON update object from json func (d *DeploymentRequest) LoadFromJSON(data []byte) (bool, error) { err := json.Unmarshal(data, &d) if err != nil { return false, err } return true, nil } // ConvertToJSON convert object to json func (d *DeploymentRequest) ConvertToJSON() (string, error) { data, err := json.Marshal(&d) if err != nil { return "", err } return string(data), nil } // Validate validates the request func (d *DeploymentRequest) Validate(strategies []string) error { if d.Version == "" { return fmt.Errorf( "Error! version is required", ) } if !In(d.Strategy, strategies) { return fmt.Errorf( "Error! strategy %s is invalid", d.Strategy, ) } return nil } // In check if value is on array func In(val interface{}, array interface{}) bool { switch reflect.TypeOf(array).Kind() { case reflect.Slice: s := reflect.ValueOf(array) for i := 0; i < s.Len(); i++ { if reflect.DeepEqual(val, s.Index(i).Interface()) { return true } } } return false } ================================================ FILE: core/module/database.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package module import ( "fmt" "time" "github.com/clivern/beetle/core/migration" "github.com/clivern/beetle/core/model" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" _ "github.com/jinzhu/gorm/dialects/sqlite" log "github.com/sirupsen/logrus" "github.com/spf13/viper" ) // Database struct type Database struct { Connection *gorm.DB } // Connect connects to a MySQL database func (db *Database) Connect(dsn model.DSN) error { var err error // Reuse db connections http://go-database-sql.org/surprises.html if db.Ping() == nil { return nil } db.Connection, err = gorm.Open(dsn.Driver, dsn.ToString()) if err != nil { return err } return nil } // Ping check the db connection func (db *Database) Ping() error { if db.Connection == nil { return fmt.Errorf("No DB Connections Found") } err := db.Connection.DB().Ping() if err != nil { return err } // Cleanup stale connections http://go-database-sql.org/surprises.html db.Connection.DB().SetMaxOpenConns(5) db.Connection.DB().SetConnMaxLifetime(time.Duration(10) * time.Second) dbStats := db.Connection.DB().Stats() log.WithFields(log.Fields{ "dbStats.maxOpenConnections": int(dbStats.MaxOpenConnections), "dbStats.openConnections": int(dbStats.OpenConnections), "dbStats.inUse": int(dbStats.InUse), "dbStats.idle": int(dbStats.Idle), }).Debug(`Open DB Connection`) return nil } // AutoConnect connects to a MySQL database using loaded configs func (db *Database) AutoConnect() error { var err error // Reuse db connections http://go-database-sql.org/surprises.html if db.Ping() == nil { return nil } dsn := model.DSN{ Driver: viper.GetString("app.database.driver"), Username: viper.GetString("app.database.username"), Password: viper.GetString("app.database.password"), Hostname: viper.GetString("app.database.host"), Port: viper.GetInt("app.database.port"), Name: viper.GetString("app.database.name"), } db.Connection, err = gorm.Open(dsn.Driver, dsn.ToString()) if err != nil { return err } return nil } // Migrate migrates the database func (db *Database) Migrate() bool { status := true db.Connection.AutoMigrate(&migration.Job{}) status = status && db.Connection.HasTable(&migration.Job{}) return status } // Rollback drop tables func (db *Database) Rollback() bool { status := true db.Connection.DropTableIfExists(&migration.Job{}) status = status && !db.Connection.HasTable(&migration.Job{}) return status } // HasTable checks if table exists func (db *Database) HasTable(table string) bool { return db.Connection.HasTable(table) } // CreateJob creates a new job func (db *Database) CreateJob(job *model.Job) *model.Job { db.Connection.Create(job) return job } // JobExistByID check if job exists func (db *Database) JobExistByID(id int) bool { job := model.Job{} db.Connection.Where("id = ?", id).First(&job) return job.ID > 0 } // GetJobByID gets a job by id func (db *Database) GetJobByID(id int) model.Job { job := model.Job{} db.Connection.Where("id = ?", id).First(&job) return job } // GetJobs gets jobs func (db *Database) GetJobs() []model.Job { jobs := []model.Job{} db.Connection.Select("*").Find(&jobs) return jobs } // JobExistByUUID check if job exists func (db *Database) JobExistByUUID(uuid string) bool { job := model.Job{} db.Connection.Where("uuid = ?", uuid).First(&job) return job.ID > 0 } // GetJobByUUID gets a job by uuid func (db *Database) GetJobByUUID(uuid string) model.Job { job := model.Job{} db.Connection.Where("uuid = ?", uuid).First(&job) return job } // GetPendingJobByType gets a job by uuid func (db *Database) GetPendingJobByType(jobType string) model.Job { job := model.Job{} db.Connection.Where("status = ? AND type = ?", model.JobPending, jobType).First(&job) return job } // CountJobs count jobs by status func (db *Database) CountJobs(status string) int { count := 0 db.Connection.Model(&model.Job{}).Where("status = ?", status).Count(&count) return count } // DeleteJobByID deletes a job by id func (db *Database) DeleteJobByID(id int) { db.Connection.Unscoped().Where("id=?", id).Delete(&migration.Job{}) } // DeleteJobByUUID deletes a job by uuid func (db *Database) DeleteJobByUUID(uuid string) { db.Connection.Unscoped().Where("uuid=?", uuid).Delete(&migration.Job{}) } // UpdateJobByID updates a job by ID func (db *Database) UpdateJobByID(job *model.Job) { db.Connection.Save(&job) } // Close closes MySQL database connection func (db *Database) Close() error { return db.Connection.Close() } // ReleaseChildJobs count jobs by status func (db *Database) ReleaseChildJobs(parentID int) { db.Connection.Model(&model.Job{}).Where( "parent = ? AND status = ?", parentID, model.JobOnHold, ).Update("status", model.JobPending) } ================================================ FILE: core/module/database_test.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package module import ( "bytes" "fmt" "io/ioutil" "os" "path/filepath" "testing" "github.com/clivern/beetle/core/model" "github.com/clivern/beetle/pkg" "github.com/drone/envsubst" "github.com/spf13/viper" ) var testingConfig = "config.testing.yml" // TestDatabase test cases func TestDatabase(t *testing.T) { // LoadConfigFile t.Run("LoadConfigFile", func(t *testing.T) { fs := FileSystem{} dir, _ := os.Getwd() configFile := fmt.Sprintf("%s/%s", dir, testingConfig) for { if fs.FileExists(configFile) { break } dir = filepath.Dir(dir) configFile = fmt.Sprintf("%s/%s", dir, testingConfig) } t.Logf("Load Config File %s", configFile) configUnparsed, _ := ioutil.ReadFile(configFile) configParsed, _ := envsubst.EvalEnv(string(configUnparsed)) viper.SetConfigType("yaml") viper.ReadConfig(bytes.NewBuffer([]byte(configParsed))) }) // TestDatabaseConnection t.Run("TestDatabaseConnection", func(t *testing.T) { db := Database{} err := db.Connect(model.DSN{ Driver: viper.GetString("app.database.driver"), Username: viper.GetString("app.database.username"), Password: viper.GetString("app.database.password"), Hostname: viper.GetString("app.database.host"), Port: viper.GetInt("app.database.port"), Name: viper.GetString("app.database.name"), }) pkg.Expect(t, nil, err) defer db.Close() pkg.Expect(t, true, db.Rollback()) pkg.Expect(t, true, db.Migrate()) pkg.Expect(t, true, db.HasTable("jobs")) }) // TestJobCRUD t.Run("TestJobCRUD", func(t *testing.T) { db := Database{} err := db.Connect(model.DSN{ Driver: viper.GetString("app.database.driver"), Username: viper.GetString("app.database.username"), Password: viper.GetString("app.database.password"), Hostname: viper.GetString("app.database.host"), Port: viper.GetInt("app.database.port"), Name: viper.GetString("app.database.name"), }) pkg.Expect(t, nil, err) defer db.Close() pkg.Expect(t, true, db.Rollback()) pkg.Expect(t, true, db.Migrate()) pkg.Expect(t, true, db.HasTable("jobs")) // Delete the job if it exists db.DeleteJobByID(1) db.DeleteJobByUUID("dddde755-5f99-4e51-a517-77878986a07e") // Create the job job := db.CreateJob(&model.Job{ UUID: "dddde755-5f99-4e51-a517-77878986a07e", Parent: 0, }) pkg.Expect(t, 1, job.ID) pkg.Expect(t, "dddde755-5f99-4e51-a517-77878986a07e", job.UUID) job1 := db.GetJobByID(1) job2 := db.GetJobByUUID("dddde755-5f99-4e51-a517-77878986a07e") pkg.Expect(t, job1.ID, job2.ID) pkg.Expect(t, job1.UUID, job2.UUID) job1.UUID = "dddde755-5f99-4e51-a517-77878986a07n" db.UpdateJobByID(&job1) job3 := db.GetJobByID(1) pkg.Expect(t, "dddde755-5f99-4e51-a517-77878986a07n", job3.UUID) pkg.Expect(t, job1.UUID, job3.UUID) pkg.Expect(t, 0, db.GetJobByUUID("dddde755-5f99-4e51-a517-77878986a07ek").ID) pkg.Expect(t, false, db.JobExistByUUID("dddde755-5f99-4e51-a517-77878986a07eo")) pkg.Expect(t, false, db.JobExistByID(20)) }) } ================================================ FILE: core/module/file_system.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package module import ( "os" ) // FileSystem struct type FileSystem struct{} // PathExists reports whether the path exists func (fs *FileSystem) PathExists(path string) bool { if _, err := os.Stat(path); os.IsNotExist(err) { return false } return true } // FileExists reports whether the named file exists func (fs *FileSystem) FileExists(path string) bool { if fi, err := os.Stat(path); err == nil { if fi.Mode().IsRegular() { return true } } return false } // DirExists reports whether the dir exists func (fs *FileSystem) DirExists(path string) bool { if fi, err := os.Stat(path); err == nil { if fi.Mode().IsDir() { return true } } return false } // EnsureDir ensures that directory exists func (fs *FileSystem) EnsureDir(dirName string, mode int) (bool, error) { err := os.MkdirAll(dirName, os.FileMode(mode)) if err == nil || os.IsExist(err) { return true, nil } return false, err } ================================================ FILE: core/module/http.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package module import ( "bytes" "context" "fmt" "io/ioutil" "net/http" "net/url" "strings" "time" ) // HTTPClient struct type HTTPClient struct { Timeout time.Duration } // NewHTTPClient creates an instance of http client func NewHTTPClient(timeout int) *HTTPClient { return &HTTPClient{ Timeout: time.Duration(timeout), } } // Get http call func (h *HTTPClient) Get(ctx context.Context, endpoint string, parameters, headers map[string]string) (*http.Response, error) { endpoint, err := h.buildParameters(endpoint, parameters) if err != nil { return nil, err } req, _ := http.NewRequest("GET", endpoint, nil) req = req.WithContext(ctx) for k, v := range headers { req.Header.Add(k, v) } client := http.Client{ Timeout: time.Second * h.Timeout, } resp, err := client.Do(req) if err != nil { return resp, err } return resp, err } // Post http call func (h *HTTPClient) Post(ctx context.Context, endpoint string, data string, parameters, headers map[string]string) (*http.Response, error) { endpoint, err := h.buildParameters(endpoint, parameters) if err != nil { return nil, err } req, _ := http.NewRequest("POST", endpoint, bytes.NewBufferString(data)) req = req.WithContext(ctx) for k, v := range headers { req.Header.Add(k, v) } client := http.Client{ Timeout: time.Second * h.Timeout, } resp, err := client.Do(req) if err != nil { return resp, err } return resp, err } // Put http call func (h *HTTPClient) Put(ctx context.Context, endpoint string, data string, parameters, headers map[string]string) (*http.Response, error) { endpoint, err := h.buildParameters(endpoint, parameters) if err != nil { return nil, err } req, _ := http.NewRequest("PUT", endpoint, bytes.NewBufferString(data)) req = req.WithContext(ctx) for k, v := range headers { req.Header.Add(k, v) } client := http.Client{ Timeout: time.Second * h.Timeout, } resp, err := client.Do(req) if err != nil { return resp, err } return resp, err } // Patch http call func (h *HTTPClient) Patch(ctx context.Context, endpoint string, data string, parameters, headers map[string]string) (*http.Response, error) { endpoint, err := h.buildParameters(endpoint, parameters) if err != nil { return nil, err } req, _ := http.NewRequest("PATCH", endpoint, bytes.NewBufferString(data)) req = req.WithContext(ctx) for k, v := range headers { req.Header.Add(k, v) } client := http.Client{ Timeout: time.Second * h.Timeout, } resp, err := client.Do(req) if err != nil { return resp, err } return resp, err } // Delete http call func (h *HTTPClient) Delete(ctx context.Context, endpoint string, parameters, headers map[string]string) (*http.Response, error) { endpoint, err := h.buildParameters(endpoint, parameters) if err != nil { return nil, err } req, _ := http.NewRequest("DELETE", endpoint, nil) req = req.WithContext(ctx) for k, v := range headers { req.Header.Add(k, v) } client := http.Client{ Timeout: time.Second * h.Timeout, } resp, err := client.Do(req) if err != nil { return resp, err } return resp, err } // buildParameters add parameters to URL func (h *HTTPClient) buildParameters(endpoint string, parameters map[string]string) (string, error) { u, err := url.Parse(endpoint) if err != nil { return "", err } q := u.Query() for k, v := range parameters { q.Set(k, v) } u.RawQuery = q.Encode() return u.String(), nil } // BuildData build body data func (h *HTTPClient) BuildData(parameters map[string]string) string { var items []string for k, v := range parameters { items = append(items, fmt.Sprintf("%s=%s", k, v)) } return strings.Join(items, "&") } // ToString response body to string func (h *HTTPClient) ToString(response *http.Response) (string, error) { defer response.Body.Close() body, err := ioutil.ReadAll(response.Body) if err != nil { return "", err } return string(body), nil } // GetStatusCode response status code func (h *HTTPClient) GetStatusCode(response *http.Response) int { return response.StatusCode } // GetHeaderValue get response header value func (h *HTTPClient) GetHeaderValue(response *http.Response, key string) string { return response.Header.Get(key) } ================================================ FILE: core/module/http_test.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package module import ( "context" "net/http" "strings" "testing" "github.com/clivern/beetle/pkg" ) // TestHttpGet test cases func TestHttpGet(t *testing.T) { t.Run("TestHttpGet", func(t *testing.T) { httpClient := NewHTTPClient(20) response, error := httpClient.Get( context.TODO(), "https://httpbin.org/get", map[string]string{"arg1": "value1"}, map[string]string{"X-Api-Key": "hipp-123"}, ) pkg.Expect(t, http.StatusOK, httpClient.GetStatusCode(response)) pkg.Expect(t, nil, error) body, error := httpClient.ToString(response) pkg.Expect(t, true, strings.Contains(body, "value1")) pkg.Expect(t, true, strings.Contains(body, "arg1")) pkg.Expect(t, true, strings.Contains(body, "arg1=value1")) pkg.Expect(t, true, strings.Contains(body, "X-Api-Key")) pkg.Expect(t, true, strings.Contains(body, "hipp-123")) pkg.Expect(t, nil, error) }) } // TestHttpDelete test cases func TestHttpDelete(t *testing.T) { t.Run("TestHttpDelete", func(t *testing.T) { httpClient := NewHTTPClient(20) response, error := httpClient.Delete( context.TODO(), "https://httpbin.org/delete", map[string]string{"arg1": "value1"}, map[string]string{"X-Api-Key": "hipp-123"}, ) pkg.Expect(t, http.StatusOK, httpClient.GetStatusCode(response)) pkg.Expect(t, nil, error) body, error := httpClient.ToString(response) pkg.Expect(t, true, strings.Contains(body, "value1")) pkg.Expect(t, true, strings.Contains(body, "arg1")) pkg.Expect(t, true, strings.Contains(body, "arg1=value1")) pkg.Expect(t, true, strings.Contains(body, "X-Api-Key")) pkg.Expect(t, true, strings.Contains(body, "hipp-123")) pkg.Expect(t, nil, error) }) } // TestHttpPost test cases func TestHttpPost(t *testing.T) { t.Run("TestHttpPost", func(t *testing.T) { httpClient := NewHTTPClient(20) response, error := httpClient.Post( context.TODO(), "https://httpbin.org/post", `{"Username":"admin", "Password":"12345"}`, map[string]string{"arg1": "value1"}, map[string]string{"X-Api-Key": "hipp-123"}, ) pkg.Expect(t, http.StatusOK, httpClient.GetStatusCode(response)) pkg.Expect(t, nil, error) body, error := httpClient.ToString(response) pkg.Expect(t, true, strings.Contains(body, `"12345"`)) pkg.Expect(t, true, strings.Contains(body, `"Username"`)) pkg.Expect(t, true, strings.Contains(body, `"admin"`)) pkg.Expect(t, true, strings.Contains(body, `"Password"`)) pkg.Expect(t, true, strings.Contains(body, "value1")) pkg.Expect(t, true, strings.Contains(body, "arg1")) pkg.Expect(t, true, strings.Contains(body, "arg1=value1")) pkg.Expect(t, true, strings.Contains(body, "X-Api-Key")) pkg.Expect(t, true, strings.Contains(body, "hipp-123")) pkg.Expect(t, nil, error) }) } // TestHttpPut test cases func TestHttpPut(t *testing.T) { t.Run("TestHttpPut", func(t *testing.T) { httpClient := NewHTTPClient(20) response, error := httpClient.Put( context.TODO(), "https://httpbin.org/put", `{"Username":"admin", "Password":"12345"}`, map[string]string{"arg1": "value1"}, map[string]string{"X-Api-Key": "hipp-123"}, ) pkg.Expect(t, http.StatusOK, httpClient.GetStatusCode(response)) pkg.Expect(t, nil, error) body, error := httpClient.ToString(response) pkg.Expect(t, true, strings.Contains(body, `"12345"`)) pkg.Expect(t, true, strings.Contains(body, `"Username"`)) pkg.Expect(t, true, strings.Contains(body, `"admin"`)) pkg.Expect(t, true, strings.Contains(body, `"Password"`)) pkg.Expect(t, true, strings.Contains(body, "value1")) pkg.Expect(t, true, strings.Contains(body, "arg1")) pkg.Expect(t, true, strings.Contains(body, "arg1=value1")) pkg.Expect(t, true, strings.Contains(body, "X-Api-Key")) pkg.Expect(t, true, strings.Contains(body, "hipp-123")) pkg.Expect(t, nil, error) }) } // TestHttpGetStatusCode1 test cases func TestHttpGetStatusCode1(t *testing.T) { t.Run("TestHttpGetStatusCode1", func(t *testing.T) { httpClient := NewHTTPClient(20) response, error := httpClient.Get( context.TODO(), "https://httpbin.org/status/200", map[string]string{"arg1": "value1"}, map[string]string{"X-Api-Key": "hipp-123"}, ) pkg.Expect(t, http.StatusOK, httpClient.GetStatusCode(response)) pkg.Expect(t, nil, error) body, error := httpClient.ToString(response) pkg.Expect(t, "", body) pkg.Expect(t, nil, error) }) } // TestHttpGetStatusCode2 test cases func TestHttpGetStatusCode2(t *testing.T) { t.Run("TestHttpGetStatusCode2", func(t *testing.T) { httpClient := NewHTTPClient(20) response, error := httpClient.Get( context.TODO(), "https://httpbin.org/status/500", map[string]string{"arg1": "value1"}, map[string]string{"X-Api-Key": "hipp-123"}, ) pkg.Expect(t, http.StatusInternalServerError, httpClient.GetStatusCode(response)) pkg.Expect(t, nil, error) body, error := httpClient.ToString(response) pkg.Expect(t, "", body) pkg.Expect(t, nil, error) }) } // TestHttpGetStatusCode3 test cases func TestHttpGetStatusCode3(t *testing.T) { t.Run("TestHttpGetStatusCode3", func(t *testing.T) { httpClient := NewHTTPClient(20) response, error := httpClient.Get( context.TODO(), "https://httpbin.org/status/404", map[string]string{"arg1": "value1"}, map[string]string{"X-Api-Key": "hipp-123"}, ) pkg.Expect(t, http.StatusNotFound, httpClient.GetStatusCode(response)) pkg.Expect(t, nil, error) body, error := httpClient.ToString(response) pkg.Expect(t, "", body) pkg.Expect(t, nil, error) }) } // TestHttpGetStatusCode4 test cases func TestHttpGetStatusCode4(t *testing.T) { t.Run("TestHttpGetStatusCode4", func(t *testing.T) { httpClient := NewHTTPClient(20) response, error := httpClient.Get( context.TODO(), "https://httpbin.org/status/201", map[string]string{"arg1": "value1"}, map[string]string{"X-Api-Key": "hipp-123"}, ) pkg.Expect(t, http.StatusCreated, httpClient.GetStatusCode(response)) pkg.Expect(t, nil, error) body, error := httpClient.ToString(response) pkg.Expect(t, "", body) pkg.Expect(t, nil, error) }) } // TestBuildParameters test cases func TestBuildParameters(t *testing.T) { t.Run("TestBuildParameters", func(t *testing.T) { httpClient := NewHTTPClient(20) url, error := httpClient.buildParameters("http://127.0.0.1", map[string]string{"arg1": "value1"}) pkg.Expect(t, "http://127.0.0.1?arg1=value1", url) pkg.Expect(t, nil, error) }) } // TestBuildData test cases func TestBuildData(t *testing.T) { t.Run("TestBuildData", func(t *testing.T) { httpClient := NewHTTPClient(20) pkg.Expect(t, httpClient.BuildData(map[string]string{}), "") pkg.Expect(t, httpClient.BuildData(map[string]string{"arg1": "value1"}), "arg1=value1") }) } ================================================ FILE: core/module/prometheus.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package module import ( "fmt" "github.com/clivern/beetle/core/model" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" ) // Prometheus struct type Prometheus struct{} // NewPrometheus create a new instance of prometheus backend func NewPrometheus() *Prometheus { return &Prometheus{} } // Send sends metrics to prometheus func (p *Prometheus) Send(metrics []model.Metric) error { log.Info(fmt.Sprintf( "Send %d metrics to prometheus backend", len(metrics), )) for _, metric := range metrics { switch metric.Type { case model.COUNTER: p.Counter(metric) case model.GAUGE: p.Gauge(metric) case model.HISTOGRAM: p.Histogram(metric) case model.SUMMARY: p.Summary(metric) default: return fmt.Errorf("metric with type %s not implemented yet", metric.Type) } } return nil } // Summary updates or creates a summary func (p *Prometheus) Summary(item model.Metric) error { var metric prometheus.Summary value, _ := item.GetValueAsFloat() opts := prometheus.SummaryOpts{ Name: item.Name, Help: item.Help, } if len(item.Labels) > 0 { vec := prometheus.NewSummaryVec(opts, item.LabelKeys()) err := prometheus.Register(vec) if err != nil { if are, ok := err.(prometheus.AlreadyRegisteredError); ok { vec = are.ExistingCollector.(*prometheus.SummaryVec) } else { return err } } metric = vec.With(item.Labels).(prometheus.Summary) } else { metric = prometheus.NewSummary(opts) err := prometheus.Register(metric) if err != nil { if are, ok := err.(prometheus.AlreadyRegisteredError); ok { metric = are.ExistingCollector.(prometheus.Summary) } else { return err } } } if item.Method == "observe" { metric.Observe(value) } else { return fmt.Errorf("method %s is not implemented yet", item.Method) } return nil } // Counter updates or creates a counter func (p *Prometheus) Counter(item model.Metric) error { var metric prometheus.Counter value, _ := item.GetValueAsFloat() opts := prometheus.CounterOpts{ Name: item.Name, Help: item.Help, } if len(item.Labels) > 0 { vec := prometheus.NewCounterVec(opts, item.LabelKeys()) err := prometheus.Register(vec) if err != nil { if are, ok := err.(prometheus.AlreadyRegisteredError); ok { vec = are.ExistingCollector.(*prometheus.CounterVec) } else { return err } } metric = vec.With(item.Labels) } else { metric = prometheus.NewCounter(opts) err := prometheus.Register(metric) if err != nil { if are, ok := err.(prometheus.AlreadyRegisteredError); ok { metric = are.ExistingCollector.(prometheus.Counter) } else { return err } } } switch item.Method { case "inc": metric.Inc() case "add": metric.Add(value) default: return fmt.Errorf("method %s is not implemented yet", item.Method) } return nil } // Histogram updates or creates a histogram func (p *Prometheus) Histogram(item model.Metric) error { var metric prometheus.Histogram value, _ := item.GetValueAsFloat() opts := prometheus.HistogramOpts{ Name: item.Name, Help: item.Help, Buckets: item.Buckets, } if len(item.Labels) > 0 { vec := prometheus.NewHistogramVec(opts, item.LabelKeys()) err := prometheus.Register(vec) if err != nil { if are, ok := err.(prometheus.AlreadyRegisteredError); ok { vec = are.ExistingCollector.(*prometheus.HistogramVec) } else { return err } } metric = vec.With(item.Labels).(prometheus.Histogram) } else { metric = prometheus.NewHistogram(opts) err := prometheus.Register(metric) if err != nil { if are, ok := err.(prometheus.AlreadyRegisteredError); ok { metric = are.ExistingCollector.(prometheus.Histogram) } else { return err } } } if item.Method == "observe" { metric.Observe(value) } else { return fmt.Errorf("method %s is not implemented yet", item.Method) } return nil } // Gauge updates or creates a gauge func (p *Prometheus) Gauge(item model.Metric) error { var metric prometheus.Gauge value, _ := item.GetValueAsFloat() opts := prometheus.GaugeOpts{ Name: item.Name, Help: item.Help, } if len(item.Labels) > 0 { vec := prometheus.NewGaugeVec(opts, item.LabelKeys()) err := prometheus.Register(vec) if err != nil { if are, ok := err.(prometheus.AlreadyRegisteredError); ok { vec = are.ExistingCollector.(*prometheus.GaugeVec) } else { return err } } metric = vec.With(item.Labels) } else { metric = prometheus.NewGauge(opts) err := prometheus.Register(metric) if err != nil { if are, ok := err.(prometheus.AlreadyRegisteredError); ok { metric = are.ExistingCollector.(prometheus.Gauge) } else { return err } } } switch item.Method { case "set": metric.Set(value) case "inc": metric.Inc() case "dec": metric.Dec() case "add": metric.Add(value) case "sub": metric.Sub(value) default: return fmt.Errorf("method %s is not implemented yet", item.Method) } return nil } ================================================ FILE: core/module/remote.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package module import ( "context" "encoding/json" "fmt" "net/http" ) // ReleaseURL remote release URL const ReleaseURL = "https://api.github.com/repos/Clivern/Beetle/releases/latest" // LatestRelease struct type LatestRelease struct { Name string `json:"name"` TagName string `json:"tag_name"` } // LoadFromJSON update object from json func (lr *LatestRelease) LoadFromJSON(data []byte) (bool, error) { err := json.Unmarshal(data, &lr) if err != nil { return false, err } return true, nil } // ConvertToJSON convert object to json func (lr *LatestRelease) ConvertToJSON() (string, error) { data, err := json.Marshal(&lr) if err != nil { return "", err } return string(data), nil } // GetLatestRelease gets the latest beetle release func GetLatestRelease() (LatestRelease, error) { result := LatestRelease{} httpClient := NewHTTPClient(20) response, err := httpClient.Get( context.TODO(), ReleaseURL, map[string]string{}, map[string]string{}, ) if http.StatusOK != httpClient.GetStatusCode(response) || err != nil { return result, fmt.Errorf("Error: Unable to fetch latest release") } body, err := httpClient.ToString(response) if err != nil { return result, fmt.Errorf("Error: Unable to fetch latest release") } ok, err := result.LoadFromJSON([]byte(body)) if !ok || err != nil { return result, fmt.Errorf("Error: Invalid remote response") } return result, nil } ================================================ FILE: core/module/remote_test.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package module import ( "strings" "testing" "github.com/clivern/beetle/pkg" ) // TestRemote test cases func TestRemote(t *testing.T) { t.Run("TestRemote", func(t *testing.T) { result, err := GetLatestRelease() pkg.Expect(t, true, strings.Contains(result.Name, ".")) pkg.Expect(t, true, strings.Contains(result.TagName, ".")) pkg.Expect(t, nil, err) }) } ================================================ FILE: core/util/helpers.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package util import ( "encoding/json" "io/ioutil" "os" "path/filepath" "reflect" "strings" "github.com/satori/go.uuid" ) // InArray check if value is on array func InArray(val interface{}, array interface{}) bool { switch reflect.TypeOf(array).Kind() { case reflect.Slice: s := reflect.ValueOf(array) for i := 0; i < s.Len(); i++ { if reflect.DeepEqual(val, s.Index(i).Interface()) { return true } } } return false } // GenerateUUID4 create a UUID func GenerateUUID4() string { u := uuid.Must(uuid.NewV4(), nil) return u.String() } // ListFiles lists all files inside a dir func ListFiles(basePath string) []string { var files []string err := filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error { if basePath != path && !info.IsDir() { files = append(files, path) } return nil }) if err != nil { return files } return files } // ReadFile get the file content func ReadFile(path string) string { data, err := ioutil.ReadFile(path) if err != nil { return err.Error() } return string(data) } // FilterFiles filters files list based on specific sub-strings func FilterFiles(files, filters []string) []string { var filteredFiles []string for _, file := range files { ok := true for _, filter := range filters { ok = ok && strings.Contains(file, filter) } if ok { filteredFiles = append(filteredFiles, file) } } return filteredFiles } // Unset remove element at position i func Unset(a []string, i int) []string { a[i] = a[len(a)-1] a[len(a)-1] = "" return a[:len(a)-1] } // ConvertToJSON convert object to json func ConvertToJSON(val interface{}) (string, error) { data, err := json.Marshal(val) if err != nil { return "", err } return string(data), nil } ================================================ FILE: core/util/helpers_test.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package util import ( "testing" "github.com/clivern/beetle/core/model" "github.com/clivern/beetle/pkg" ) // TestInArray test cases func TestInArray(t *testing.T) { // TestInArray t.Run("TestInArray", func(t *testing.T) { pkg.Expect(t, InArray("A", []string{"A", "B", "C", "D"}), true) pkg.Expect(t, InArray("B", []string{"A", "B", "C", "D"}), true) pkg.Expect(t, InArray("H", []string{"A", "B", "C", "D"}), false) pkg.Expect(t, InArray(1, []int{2, 3, 1}), true) pkg.Expect(t, InArray(9, []int{2, 3, 1}), false) payload := []model.PatchStringValue{ model.PatchStringValue{ Op: "replace", Path: "/spec/template/spec/containers/0/image", Value: "clivern/toad:release-0.2.4", }, } data, err := ConvertToJSON(payload) pkg.Expect(t, data, `[{"op":"replace","path":"/spec/template/spec/containers/0/image","value":"clivern/toad:release-0.2.4"}]`) pkg.Expect(t, err, nil) }) } ================================================ FILE: deployment/docker/README.md ================================================ ⚠️ This only to test beetle with prometheus and grafana. ================================================ FILE: deployment/docker/docker-compose.yml ================================================ version: '3' services: # Redis Service redis: image: 'redis:7.2-alpine' volumes: - 'redis_data:/data' ports: - '6379:6379' restart: always # Prometheus Service prometheus: image: 'prom/prometheus:v2.53.0' volumes: - './:/etc/prometheus' command: '--config.file=/etc/prometheus/prometheus.yml' ports: - '9090:9090' restart: always # Grafana Service grafana: image: 'grafana/grafana:9.5.20' environment: - GF_SECURITY_ADMIN_USER=${ADMIN_USER:-admin} - GF_SECURITY_ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin} - GF_USERS_ALLOW_SIGN_UP=false ports: - '3000:3000' depends_on: - prometheus restart: always volumes: redis_data: null ================================================ FILE: deployment/docker/prometheus.yml ================================================ # my global config global: evaluation_interval: 15s scrape_interval: 15s rule_files: ~ scrape_configs: - job_name: prometheus scrape_interval: 5s static_configs: - targets: - "localhost:9090" - job_name: beetle metrics_path: /metrics scrape_interval: 5s static_configs: - targets: - "xx.ngrok.io" ================================================ FILE: deployment/k8s/incluster/README.md ================================================ ## Running Beetle inside Kubernetes Cluster ```bash $ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.41.2/deploy/static/provider/cloud/deploy.yaml $ kubectl get pods -n ingress-nginx \ -l app.kubernetes.io/name=ingress-nginx --watch $ kubectl get svc --namespace=ingress-nginx ``` Update `beetle.yaml` with the database credentials. The following part inside the file ```bash .... # Application Database database: # Database driver (sqlite3, mysql) driver: ${BEETLE_DATABASE_DRIVER:-mysql} # Hostname host: ${BEETLE_DATABASE_MYSQL_HOST:-REPLACE_WITH_MYSQL_HOSTNAME} # Port port: ${BEETLE_DATABASE_MYSQL_PORT:-3306} # Database name: ${BEETLE_DATABASE_MYSQL_DATABASE:-REPLACE_WITH_MYSQL_DATABASE} # Username username: ${BEETLE_DATABASE_MYSQL_USERNAME:-REPLACE_WITH_MYSQL_USERNAME} # Password password: ${BEETLE_DATABASE_MYSQL_PASSWORD:-REPLACE_WITH_MYSQL_PASSWORD} .... ``` Deploy a sample application and Beetle API server ```bash $ kubectl apply -f sample_app.yaml --record $ kubectl apply -f beetle.yaml --record $ kubectl get ingress # Update /etc/hosts with the ingress IP # 167.x.x.x example.com $ kubectl describe ingress toad-ing $ kubectl describe ingress beetle-ing $ curl http://example.com/toad/_ready $ curl http://example.com/beetle/_ready ``` Interact with Beetle API server ```bash # Get clusters $ curl http://example.com/beetle/api/v1/cluster -H "X-API-KEY: 1234" -s | jq . # Get cluster $ curl http://example.com/beetle/api/v1/cluster/production -H "X-API-KEY: 1234" -s | jq . # Get cluster namespaces $ curl http://example.com/beetle/api/v1/cluster/production/namespace -H "X-API-KEY: 1234" -s | jq . # Get namespace $ curl http://example.com/beetle/api/v1/cluster/production/namespace/default -H "X-API-KEY: 1234" -s | jq . # Get namespace applications $ curl http://example.com/beetle/api/v1/cluster/production/namespace/default/app -H "X-API-KEY: 1234" -s | jq . # Get application `toad` $ curl http://example.com/beetle/api/v1/cluster/production/namespace/default/app/toad -H "X-API-KEY: 1234" -s | jq . # Get Async Jobs $ curl -X GET http://example.com/beetle/api/v1/job -H "X-API-KEY: 1234" -s | jq . # Deploy a new version with recreate strategy $ curl -X POST \ -H "X-API-KEY: 1234" \ -d '{"version":"0.2.4","strategy":"recreate"}' \ http://example.com/beetle/api/v1/cluster/production/namespace/default/app/toad/deployment # Get application `toad` version $ curl http://example.com/beetle/api/v1/cluster/production/namespace/default/app/toad -H "X-API-KEY: 1234" -s | jq . # Another deployment with ramped strategy $ curl -X POST \ -H "X-API-KEY: 1234" \ -d '{"version":"0.2.3","strategy":"ramped", "maxSurge": "1", "maxUnavailable": "0"}' \ http://example.com/beetle/api/v1/cluster/production/namespace/default/app/toad/deployment ``` ================================================ FILE: deployment/k8s/incluster/beetle.yaml ================================================ --- apiVersion: v1 kind: ServiceAccount metadata: name: beetle-service-account namespace: default --- apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: name: beetle-service-account namespace: default rules: - apiGroups: [""] resources: ["namespaces"] verbs: ["get", "list"] - apiGroups: [""] resources: ["nodes"] verbs: ["get", "list", "watch"] - apiGroups: [""] resources: ["pods"] verbs: ["get", "list", "watch"] - apiGroups: [""] resources: ["configmaps"] verbs: ["get", "list"] - apiGroups: ["extensions", "apps"] resources: ["deployments"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] --- kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1beta1 metadata: name: beetle-service-account roleRef: kind: ClusterRole name: beetle-service-account apiGroup: rbac.authorization.k8s.io subjects: - kind: ServiceAccount name: beetle-service-account namespace: default --- apiVersion: v1 kind: ConfigMap metadata: name: incluster-beetle-configs namespace: default data: config.dist.yml: |- --- # App configs app: # Env mode (dev or prod) mode: ${BEETLE_APP_MODE:-prod} # HTTP port port: ${BEETLE_API_PORT:-8080} # App URL domain: ${BEETLE_APP_DOMAIN:-http://127.0.0.1:8080} # TLS configs tls: status: ${BEETLE_API_TLS_STATUS:-off} pemPath: ${BEETLE_API_TLS_PEMPATH:-cert/server.pem} keyPath: ${BEETLE_API_TLS_KEYPATH:-cert/server.key} # Message Broker Configs broker: # Broker driver (native) driver: ${BEETLE_BROKER_DRIVER:-native} # Native driver configs native: # Queue max capacity capacity: ${BEETLE_BROKER_NATIVE_CAPACITY:-5000} # Number of concurrent workers workers: ${BEETLE_BROKER_NATIVE_WORKERS:-4} # API Configs api: key: ${BEETLE_API_KEY:-1234} # Runtime, Requests/Response and Beetle Metrics metrics: prometheus: # Route for the metrics endpoint endpoint: ${BEETLE_METRICS_PROM_ENDPOINT:-/metrics} # Application Database database: # Database driver (sqlite3, mysql) driver: ${BEETLE_DATABASE_DRIVER:-mysql} # Hostname host: ${BEETLE_DATABASE_MYSQL_HOST:-REPLACE_WITH_MYSQL_HOSTNAME} # Port port: ${BEETLE_DATABASE_MYSQL_PORT:-3306} # Database name: ${BEETLE_DATABASE_MYSQL_DATABASE:-REPLACE_WITH_MYSQL_DATABASE} # Username username: ${BEETLE_DATABASE_MYSQL_USERNAME:-REPLACE_WITH_MYSQL_USERNAME} # Password password: ${BEETLE_DATABASE_MYSQL_PASSWORD:-REPLACE_WITH_MYSQL_PASSWORD} # Kubernetes Clusters clusters: - name: ${BEETLE_KUBE_CLUSTER_01_NAME:-production} inCluster: ${BEETLE_KUBE_CLUSTER_01_IN_CLUSTER:-true} kubeconfig: ${BEETLE_KUBE_CLUSTER_01_CONFIG_FILE:- } # HTTP Webhook webhook: url: ${BEETLE_WEBHOOK_URL:-https://httpbin.org/anything} retry: ${BEETLE_WEBHOOK_RETRY:-3} apiKey: ${BEETLE_WEBHOOK_API_KEY:-12345} # Log configs log: # Log level, it can be debug, info, warn, error, panic, fatal level: ${BEETLE_LOG_LEVEL:-info} # output can be stdout or abs path to log file /var/logs/beetle.log output: ${BEETLE_LOG_OUTPUT:-stdout} # Format can be json format: ${BEETLE_LOG_FORMAT:-json} --- apiVersion: apps/v1 kind: Deployment metadata: name: beetle-deployment spec: replicas: 2 selector: matchLabels: app: beetle template: metadata: labels: app: beetle name: beetle spec: serviceAccount: beetle-service-account serviceAccountName: beetle-service-account containers: - image: "clivern/beetle:1.0.2" name: beetle-app volumeMounts: - mountPath: /app/configs name: incluster-beetle-configs-volume volumes: - configMap: name: incluster-beetle-configs name: incluster-beetle-configs-volume --- apiVersion: v1 kind: Service metadata: name: beetle-svc labels: app: beetle spec: ports: - port: 80 targetPort: 8080 selector: app: beetle type: LoadBalancer --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: # example.com/beetle rewrites to example.com/ # example.com/beetle/ rewrites to example.com/ # example.com/beetle/_ready rewrites to example.com/_ready nginx.ingress.kubernetes.io/rewrite-target: /$2 name: beetle-ing spec: rules: - host: example.com http: paths: - path: /beetle(/|$)(.*) pathType: Prefix backend: service: name: beetle-svc port: number: 80 ================================================ FILE: deployment/k8s/incluster/sample_app.yaml ================================================ --- apiVersion: apps/v1 kind: Deployment metadata: labels: beetle.clivern.com/status: enabled beetle.clivern.com/application-id: toad annotations: beetle.clivern.com/application-name: Toad beetle.clivern.com/image-format: "clivern/toad:release-[.Release]" name: toad-deployment spec: replicas: 2 selector: matchLabels: app: toad template: metadata: labels: app: toad name: toad spec: containers: - image: "clivern/toad:release-0.2.3" name: toad-app --- apiVersion: v1 kind: Service metadata: name: toad-svc labels: app: toad spec: ports: - port: 80 targetPort: 8080 selector: app: toad type: LoadBalancer --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: # example.com/toad rewrites to example.com/ # example.com/toad/ rewrites to example.com/ # example.com/toad/_ready rewrites to example.com/_ready nginx.ingress.kubernetes.io/rewrite-target: /$2 name: toad-ing spec: rules: - host: example.com http: paths: - path: /toad(/|$)(.*) pathType: Prefix backend: service: name: toad-svc port: number: 80 ================================================ FILE: go.mod ================================================ module github.com/clivern/beetle go 1.20 require ( github.com/briandowns/spinner v1.23.0 github.com/drone/envsubst v1.0.3 github.com/gin-gonic/gin v1.10.0 github.com/jinzhu/gorm v1.9.16 github.com/logrusorgru/aurora/v3 v3.0.0 github.com/olekukonko/tablewriter v0.0.5 github.com/prometheus/client_golang v1.18.0 github.com/satori/go.uuid v1.2.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.18.2 k8s.io/api v0.27.4 k8s.io/apimachinery v0.27.4 k8s.io/client-go v0.27.4 ) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/fatih/color v1.14.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.1 // indirect github.com/go-openapi/swag v0.22.3 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/go-sql-driver/mysql v1.5.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // 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/google/uuid v1.4.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/imdario/mergo v0.3.6 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mattn/go-sqlite3 v1.14.0 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // 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/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.45.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.23.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/oauth2 v0.15.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/term v0.20.0 // indirect golang.org/x/text v0.15.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.34.1 // 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 gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.90.1 // indirect k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) ================================================ FILE: go.sum ================================================ 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.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= 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.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= 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/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= 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/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A= github.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 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/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 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/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g= github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g= 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/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 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/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 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.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8= github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 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/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.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/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= 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.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= 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-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= 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/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/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/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= 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/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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 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/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 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.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 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/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/logrusorgru/aurora/v3 v3.0.0 h1:R6zcoZZbvVcGMvDCKo45A9U/lzYyzl5NfYIvznmDfE4= github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc= 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.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 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/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA= github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= 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/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/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo/v2 v2.9.1 h1:zie5Ly042PD3bsCvsSOPvRnFwyo3rKe64TJlD6nu0mk= github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 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= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM= github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 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.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= github.com/spf13/viper v1.18.1 h1:rmuU42rScKWlhhJDyXZRKJQHXFX02chSVW1IvkPGiVM= github.com/spf13/viper v1.18.1/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= 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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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.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/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 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= 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.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 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-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 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/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 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/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/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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-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-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.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 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.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= 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/sys v0.0.0-20180830151530-49385e6e1522/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-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-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-20210225134936-a50acf3fe073/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-20210615035016-665e8c7367d1/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-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 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.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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-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-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.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.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= 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/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 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= 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-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-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= 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-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 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.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/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= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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= 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= k8s.io/api v0.27.4 h1:0pCo/AN9hONazBKlNUdhQymmnfLRbSZjd5H5H3f0bSs= k8s.io/api v0.27.4/go.mod h1:O3smaaX15NfxjzILfiln1D8Z3+gEYpjEpiNA/1EVK1Y= k8s.io/apimachinery v0.27.4 h1:CdxflD4AF61yewuid0fLl6bM4a3q04jWel0IlP+aYjs= k8s.io/apimachinery v0.27.4/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E= k8s.io/client-go v0.27.4 h1:vj2YTtSJ6J4KxaC88P4pMPEQECWMY8gqPqsTgUKzvjk= k8s.io/client-go v0.27.4/go.mod h1:ragcly7lUlN0SRPk5/ZkGnDjPknzb37TICq07WhI6Xc= k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg= k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg= k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY= k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 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-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/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= ================================================ FILE: pkg/expect.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package pkg import ( "reflect" "testing" ) // Expect compare two values for testing func Expect(t *testing.T, got, want interface{}) { t.Logf(`Comparing values %v, %v`, got, want) if !reflect.DeepEqual(got, want) { t.Errorf(`got %v, want %v`, got, want) } } ================================================ FILE: pkg/server_mock.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package pkg import ( "net/http" "net/http/httptest" ) // ServerMock mocks http server func ServerMock(uri, response string, statusCode int) *httptest.Server { handler := http.NewServeMux() handler.HandleFunc(uri, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(statusCode) w.Write([]byte(response)) }) srv := httptest.NewServer(handler) return srv } ================================================ FILE: renovate.json ================================================ { "extends": [ "config:base" ] } ================================================ FILE: sdk/application.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package sdk import ( "context" "fmt" "net/http" "github.com/clivern/beetle/core/model" ) // GetApplications Get Applications List func (c *Client) GetApplications(ctx context.Context, cluster, namespace string) (model.Applications, error) { var result model.Applications response, err := c.HTTPClient.Get( ctx, fmt.Sprintf("%s/api/v1/cluster/%s/namespace/%s/app", c.APIURL, cluster, namespace), map[string]string{}, map[string]string{"X-API-KEY": c.APIKey}, ) if err != nil { return result, err } statusCode := c.HTTPClient.GetStatusCode(response) if statusCode != http.StatusOK { return result, fmt.Errorf(fmt.Sprintf("Invalid status code %d", statusCode)) } body, err := c.HTTPClient.ToString(response) if err != nil { return result, fmt.Errorf(fmt.Sprintf("Invalid response: %s", err.Error())) } ok, err := result.LoadFromJSON([]byte(body)) if err != nil { return result, fmt.Errorf(fmt.Sprintf("Invalid response: %s", err.Error())) } if !ok { return result, fmt.Errorf("Invalid response") } return result, nil } // GetApplication Get Application func (c *Client) GetApplication(ctx context.Context, cluster, namespace, application string) (model.Application, error) { var result model.Application response, err := c.HTTPClient.Get( ctx, fmt.Sprintf("%s/api/v1/cluster/%s/namespace/%s/app/%s", c.APIURL, cluster, namespace, application), map[string]string{}, map[string]string{"X-API-KEY": c.APIKey}, ) if err != nil { return result, err } statusCode := c.HTTPClient.GetStatusCode(response) if statusCode != http.StatusOK { return result, fmt.Errorf(fmt.Sprintf("Invalid status code %d", statusCode)) } body, err := c.HTTPClient.ToString(response) if err != nil { return result, fmt.Errorf(fmt.Sprintf("Invalid response: %s", err.Error())) } ok, err := result.LoadFromJSON([]byte(body)) if err != nil { return result, fmt.Errorf(fmt.Sprintf("Invalid response: %s", err.Error())) } if !ok { return result, fmt.Errorf("Invalid response") } return result, nil } ================================================ FILE: sdk/application_test.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package sdk import ( "bytes" "context" "fmt" "io/ioutil" "net/http" "os" "path/filepath" "testing" "github.com/clivern/beetle/core/model" "github.com/clivern/beetle/core/module" "github.com/clivern/beetle/pkg" "github.com/drone/envsubst" "github.com/spf13/viper" ) // TestApplicationCRUD test cases func TestApplicationCRUD(t *testing.T) { testingConfig := "config.testing.yml" httpClient := Client{} httpClient.SetHTTPClient(module.NewHTTPClient(20)) httpClient.SetAPIKey("") // LoadConfigFile t.Run("LoadConfigFile", func(t *testing.T) { fs := module.FileSystem{} dir, _ := os.Getwd() configFile := fmt.Sprintf("%s/%s", dir, testingConfig) for { if fs.FileExists(configFile) { break } dir = filepath.Dir(dir) configFile = fmt.Sprintf("%s/%s", dir, testingConfig) } t.Logf("Load Config File %s", configFile) configUnparsed, _ := ioutil.ReadFile(configFile) configParsed, _ := envsubst.EvalEnv(string(configUnparsed)) viper.SetConfigType("yaml") viper.ReadConfig(bytes.NewBuffer([]byte(configParsed))) }) // TestGetApplications t.Run("TestGetApplications", func(t *testing.T) { srv := pkg.ServerMock( "/api/v1/cluster/production/namespace/default/app", `{"applications":[{"id":"toad","name":"Toad App","format":"clivern/toad:release-[.Release]","containers":[{"name":"toad","image":"clivern/toad:release-0.2.3","version":"0.2.3","deployment":{"name":"toad-deployment","uid":"0f77903a-ce69-4aa5-a025-cad4b4a3209e"}}]}]}`, http.StatusOK, ) defer srv.Close() httpClient.SetAPIURL(srv.URL) result, err := httpClient.GetApplications(context.TODO(), "production", "default") pkg.Expect(t, nil, err) pkg.Expect(t, result, model.Applications{ Applications: []model.Application{ model.Application{ ID: "toad", Name: "Toad App", Format: "clivern/toad:release-[.Release]", Containers: []model.Container{ model.Container{ Name: "toad", Image: "clivern/toad:release-0.2.3", Version: "0.2.3", Deployment: model.Deployment{ Name: "toad-deployment", UID: "0f77903a-ce69-4aa5-a025-cad4b4a3209e", }, }, }, }, }, }) }) // TestGetApplication t.Run("TestGetApplication", func(t *testing.T) { srv := pkg.ServerMock( "/api/v1/cluster/production/namespace/default/app/toad", `{"id":"toad","name":"Toad App","format":"clivern/toad:release-[.Release]","containers":[{"name":"toad","image":"clivern/toad:release-0.2.3","version":"0.2.3","deployment":{"name":"toad-deployment","uid":"0f77903a-ce69-4aa5-a025-cad4b4a3209e"}}]}`, http.StatusOK, ) defer srv.Close() httpClient.SetAPIURL(srv.URL) result, err := httpClient.GetApplication(context.TODO(), "production", "default", "toad") pkg.Expect(t, nil, err) pkg.Expect(t, result, model.Application{ ID: "toad", Name: "Toad App", Format: "clivern/toad:release-[.Release]", Containers: []model.Container{ model.Container{ Name: "toad", Image: "clivern/toad:release-0.2.3", Version: "0.2.3", Deployment: model.Deployment{ Name: "toad-deployment", UID: "0f77903a-ce69-4aa5-a025-cad4b4a3209e", }, }, }, }) }) } ================================================ FILE: sdk/client.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package sdk import ( "github.com/clivern/beetle/core/module" ) // Client struct type Client struct { APIKey string APIURL string HTTPClient *module.HTTPClient } // SetHTTPClient sets http client func (c *Client) SetHTTPClient(httpClient *module.HTTPClient) { c.HTTPClient = httpClient } // SetAPIURL sets api url func (c *Client) SetAPIURL(APIURL string) { c.APIURL = APIURL } // SetAPIKey sets api key func (c *Client) SetAPIKey(APIKey string) { c.APIKey = APIKey } ================================================ FILE: sdk/cluster.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package sdk import ( "context" "fmt" "net/http" "github.com/clivern/beetle/core/model" ) // GetClusters Get Clusters List func (c *Client) GetClusters(ctx context.Context) (model.Clusters, error) { var result model.Clusters response, err := c.HTTPClient.Get( ctx, fmt.Sprintf("%s/api/v1/cluster", c.APIURL), map[string]string{}, map[string]string{"X-API-KEY": c.APIKey}, ) if err != nil { return result, err } statusCode := c.HTTPClient.GetStatusCode(response) if statusCode != http.StatusOK { return result, fmt.Errorf(fmt.Sprintf("Invalid status code %d", statusCode)) } body, err := c.HTTPClient.ToString(response) if err != nil { return result, fmt.Errorf(fmt.Sprintf("Invalid response: %s", err.Error())) } ok, err := result.LoadFromJSON([]byte(body)) if err != nil { return result, fmt.Errorf(fmt.Sprintf("Invalid response: %s", err.Error())) } if !ok { return result, fmt.Errorf("Invalid response") } return result, nil } // GetCluster Get Cluster func (c *Client) GetCluster(ctx context.Context, cluster string) (model.Cluster, error) { var result model.Cluster response, err := c.HTTPClient.Get( ctx, fmt.Sprintf("%s/api/v1/cluster/%s", c.APIURL, cluster), map[string]string{}, map[string]string{"X-API-KEY": c.APIKey}, ) if err != nil { return result, err } statusCode := c.HTTPClient.GetStatusCode(response) if statusCode != http.StatusOK { return result, fmt.Errorf(fmt.Sprintf("Invalid status code %d", statusCode)) } body, err := c.HTTPClient.ToString(response) if err != nil { return result, fmt.Errorf(fmt.Sprintf("Invalid response: %s", err.Error())) } ok, err := result.LoadFromJSON([]byte(body)) if err != nil { return result, fmt.Errorf(fmt.Sprintf("Invalid response: %s", err.Error())) } if !ok { return result, fmt.Errorf("Invalid response") } return result, nil } ================================================ FILE: sdk/cluster_test.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package sdk import ( "bytes" "context" "fmt" "io/ioutil" "net/http" "os" "path/filepath" "testing" "github.com/clivern/beetle/core/model" "github.com/clivern/beetle/core/module" "github.com/clivern/beetle/pkg" "github.com/drone/envsubst" "github.com/spf13/viper" ) // TestClusterCRUD test cases func TestClusterCRUD(t *testing.T) { testingConfig := "config.testing.yml" httpClient := Client{} httpClient.SetHTTPClient(module.NewHTTPClient(20)) httpClient.SetAPIKey("") // LoadConfigFile t.Run("LoadConfigFile", func(t *testing.T) { fs := module.FileSystem{} dir, _ := os.Getwd() configFile := fmt.Sprintf("%s/%s", dir, testingConfig) for { if fs.FileExists(configFile) { break } dir = filepath.Dir(dir) configFile = fmt.Sprintf("%s/%s", dir, testingConfig) } t.Logf("Load Config File %s", configFile) configUnparsed, _ := ioutil.ReadFile(configFile) configParsed, _ := envsubst.EvalEnv(string(configUnparsed)) viper.SetConfigType("yaml") viper.ReadConfig(bytes.NewBuffer([]byte(configParsed))) }) // TestGetClusters t.Run("TestGetClusters", func(t *testing.T) { srv := pkg.ServerMock( "/api/v1/cluster", `{"clusters": [{"name": "staging", "health": false},{"name": "production", "health": true}]}`, http.StatusOK, ) defer srv.Close() httpClient.SetAPIURL(srv.URL) result, err := httpClient.GetClusters(context.TODO()) pkg.Expect(t, nil, err) pkg.Expect(t, result, model.Clusters{ Clusters: []model.Cluster{ model.Cluster{Name: "staging", Health: false}, model.Cluster{Name: "production", Health: true}, }, }) }) // TestGetCluster t.Run("TestGetCluster", func(t *testing.T) { srv := pkg.ServerMock( "/api/v1/cluster/staging", `{"name": "staging", "health": false}`, http.StatusOK, ) defer srv.Close() httpClient.SetAPIURL(srv.URL) result, err := httpClient.GetCluster(context.TODO(), "staging") pkg.Expect(t, nil, err) pkg.Expect(t, result, model.Cluster{Name: "staging", Health: false}) }) } ================================================ FILE: sdk/deployment.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package sdk import ( "context" "fmt" "net/http" "github.com/clivern/beetle/core/model" ) // CreateDeployment Get Application func (c *Client) CreateDeployment(ctx context.Context, request model.DeploymentRequest) (model.Job, error) { var result model.Job requestBody, err := request.ConvertToJSON() if err != nil { return result, err } response, err := c.HTTPClient.Post( ctx, fmt.Sprintf("%s/api/v1/cluster/%s/namespace/%s/app/%s/deployment", c.APIURL, request.Cluster, request.Namespace, request.Application), requestBody, map[string]string{}, map[string]string{"X-API-KEY": c.APIKey}, ) if err != nil { return result, err } statusCode := c.HTTPClient.GetStatusCode(response) if statusCode != http.StatusAccepted { return result, fmt.Errorf(fmt.Sprintf("Invalid status code %d", statusCode)) } body, err := c.HTTPClient.ToString(response) if err != nil { return result, fmt.Errorf(fmt.Sprintf("Invalid response: %s", err.Error())) } ok, err := result.LoadFromJSON([]byte(body)) if err != nil { return result, fmt.Errorf(fmt.Sprintf("Invalid response: %s", err.Error())) } if !ok { return result, fmt.Errorf("Invalid response") } return result, nil } ================================================ FILE: sdk/deployment_test.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package sdk import ( "bytes" "context" "fmt" "io/ioutil" "net/http" "os" "path/filepath" "testing" "github.com/clivern/beetle/core/model" "github.com/clivern/beetle/core/module" "github.com/clivern/beetle/pkg" "github.com/drone/envsubst" "github.com/spf13/viper" ) // TestDeploymentCRUD test cases func TestDeploymentCRUD(t *testing.T) { testingConfig := "config.testing.yml" httpClient := Client{} httpClient.SetHTTPClient(module.NewHTTPClient(20)) httpClient.SetAPIKey("") // LoadConfigFile t.Run("LoadConfigFile", func(t *testing.T) { fs := module.FileSystem{} dir, _ := os.Getwd() configFile := fmt.Sprintf("%s/%s", dir, testingConfig) for { if fs.FileExists(configFile) { break } dir = filepath.Dir(dir) configFile = fmt.Sprintf("%s/%s", dir, testingConfig) } t.Logf("Load Config File %s", configFile) configUnparsed, _ := ioutil.ReadFile(configFile) configParsed, _ := envsubst.EvalEnv(string(configUnparsed)) viper.SetConfigType("yaml") viper.ReadConfig(bytes.NewBuffer([]byte(configParsed))) }) // TestCreateDeployment t.Run("TestCreateDeployment", func(t *testing.T) { srv := pkg.ServerMock( "/api/v1/cluster/production/namespace/default/app/toad/deployment", `{"id":1,"uuid":"4f540ab1-2c29-47e6-b900-675312b784d8","status":"pending","type":"deployment.update","created_at":"2020-06-16T18:20:35Z"}`, http.StatusAccepted, ) defer srv.Close() deploymentRequest := model.DeploymentRequest{ Cluster: "production", Namespace: "default", Application: "toad", Version: "1.0.0", Strategy: "recreate", } httpClient.SetAPIURL(srv.URL) result, err := httpClient.CreateDeployment(context.TODO(), deploymentRequest) pkg.Expect(t, err, nil) pkg.Expect(t, 1, result.ID) pkg.Expect(t, "4f540ab1-2c29-47e6-b900-675312b784d8", result.UUID) pkg.Expect(t, "pending", result.Status) pkg.Expect(t, "deployment.update", result.Type) }) } ================================================ FILE: sdk/job.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package sdk import ( "context" "fmt" "net/http" "github.com/clivern/beetle/core/model" ) // GetJobs Get Jobs List func (c *Client) GetJobs(ctx context.Context) (model.Jobs, error) { var result model.Jobs response, err := c.HTTPClient.Get( ctx, fmt.Sprintf("%s/api/v1/job", c.APIURL), map[string]string{}, map[string]string{"X-API-KEY": c.APIKey}, ) if err != nil { return result, err } statusCode := c.HTTPClient.GetStatusCode(response) if statusCode != http.StatusOK { return result, fmt.Errorf(fmt.Sprintf("Invalid status code %d", statusCode)) } body, err := c.HTTPClient.ToString(response) if err != nil { return result, fmt.Errorf(fmt.Sprintf("Invalid response: %s", err.Error())) } ok, err := result.LoadFromJSON([]byte(body)) if err != nil { return result, fmt.Errorf(fmt.Sprintf("Invalid response: %s", err.Error())) } if !ok { return result, fmt.Errorf("Invalid response") } return result, nil } // GetJob Get Job func (c *Client) GetJob(ctx context.Context, uuid string) (model.Job, error) { var result model.Job response, err := c.HTTPClient.Get( ctx, fmt.Sprintf("%s/api/v1/job/%s", c.APIURL, uuid), map[string]string{}, map[string]string{"X-API-KEY": c.APIKey}, ) if err != nil { return result, err } statusCode := c.HTTPClient.GetStatusCode(response) if statusCode != http.StatusOK { return result, fmt.Errorf(fmt.Sprintf("Invalid status code %d", statusCode)) } body, err := c.HTTPClient.ToString(response) if err != nil { return result, fmt.Errorf(fmt.Sprintf("Invalid response: %s", err.Error())) } ok, err := result.LoadFromJSON([]byte(body)) if err != nil { return result, fmt.Errorf(fmt.Sprintf("Invalid response: %s", err.Error())) } if !ok { return result, fmt.Errorf("Invalid response") } return result, nil } // DeleteJob Delete Job func (c *Client) DeleteJob(ctx context.Context, uuid string) (bool, error) { response, err := c.HTTPClient.Delete( ctx, fmt.Sprintf("%s/api/v1/job/%s", c.APIURL, uuid), map[string]string{}, map[string]string{"X-API-KEY": c.APIKey}, ) if err != nil { return false, err } statusCode := c.HTTPClient.GetStatusCode(response) if statusCode != http.StatusNoContent { return false, fmt.Errorf(fmt.Sprintf("Invalid status code %d", statusCode)) } return true, nil } ================================================ FILE: sdk/job_test.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package sdk import ( "bytes" "context" "fmt" "io/ioutil" "net/http" "os" "path/filepath" "testing" "github.com/clivern/beetle/core/module" "github.com/clivern/beetle/pkg" "github.com/drone/envsubst" "github.com/spf13/viper" ) // TestJobCRUD test cases func TestJobCRUD(t *testing.T) { testingConfig := "config.testing.yml" httpClient := Client{} httpClient.SetHTTPClient(module.NewHTTPClient(20)) httpClient.SetAPIKey("") // LoadConfigFile t.Run("LoadConfigFile", func(t *testing.T) { fs := module.FileSystem{} dir, _ := os.Getwd() configFile := fmt.Sprintf("%s/%s", dir, testingConfig) for { if fs.FileExists(configFile) { break } dir = filepath.Dir(dir) configFile = fmt.Sprintf("%s/%s", dir, testingConfig) } t.Logf("Load Config File %s", configFile) configUnparsed, _ := ioutil.ReadFile(configFile) configParsed, _ := envsubst.EvalEnv(string(configUnparsed)) viper.SetConfigType("yaml") viper.ReadConfig(bytes.NewBuffer([]byte(configParsed))) }) // TestGetJobs t.Run("TestGetJobs", func(t *testing.T) { srv := pkg.ServerMock( "/api/v1/job", `{"jobs": [{"id":1,"uuid":"4f540ab1-2c29-47e6-b900-675312b784d8","payload":"{}","status":"pending","type":"deployment.update","result":"","retry":0,"parent":0,"run_at":null,"created_at":"2020-06-16T18:20:35Z","updated_at":"2020-06-16T18:20:35Z"}]}`, http.StatusOK, ) defer srv.Close() httpClient.SetAPIURL(srv.URL) result, err := httpClient.GetJobs(context.TODO()) pkg.Expect(t, err, nil) pkg.Expect(t, result.Jobs[0].ID, 1) pkg.Expect(t, result.Jobs[0].UUID, "4f540ab1-2c29-47e6-b900-675312b784d8") pkg.Expect(t, result.Jobs[0].Status, "pending") pkg.Expect(t, result.Jobs[0].Type, "deployment.update") }) // TestGetJob t.Run("TestGetJob", func(t *testing.T) { srv := pkg.ServerMock( "/api/v1/job/4f540ab1-2c29-47e6-b900-675312b784d8", `{"id":1,"uuid":"4f540ab1-2c29-47e6-b900-675312b784d8","payload":"{}","status":"pending","type":"deployment.update","result":"","retry":0,"parent":0,"run_at":null,"created_at":"2020-06-16T18:20:35Z","updated_at":"2020-06-16T18:20:35Z"}`, http.StatusOK, ) defer srv.Close() httpClient.SetAPIURL(srv.URL) result, err := httpClient.GetJob(context.TODO(), "4f540ab1-2c29-47e6-b900-675312b784d8") pkg.Expect(t, err, nil) pkg.Expect(t, result.ID, 1) pkg.Expect(t, result.UUID, "4f540ab1-2c29-47e6-b900-675312b784d8") pkg.Expect(t, result.Status, "pending") pkg.Expect(t, result.Type, "deployment.update") }) // TestDeleteJob t.Run("TestDeleteJob", func(t *testing.T) { srv := pkg.ServerMock( "/api/v1/job/4f540ab1-2c29-47e6-b900-675312b784d8", ``, http.StatusNoContent, ) defer srv.Close() httpClient.SetAPIURL(srv.URL) result, err := httpClient.DeleteJob(context.TODO(), "4f540ab1-2c29-47e6-b900-675312b784d8") pkg.Expect(t, err, nil) pkg.Expect(t, result, true) }) } ================================================ FILE: sdk/namespace.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package sdk import ( "context" "fmt" "net/http" "github.com/clivern/beetle/core/model" ) // GetNamespaces Get Namespaces List func (c *Client) GetNamespaces(ctx context.Context, cluster string) (model.Namespaces, error) { var result model.Namespaces response, err := c.HTTPClient.Get( ctx, fmt.Sprintf("%s/api/v1/cluster/%s/namespace", c.APIURL, cluster), map[string]string{}, map[string]string{"X-API-KEY": c.APIKey}, ) if err != nil { return result, err } statusCode := c.HTTPClient.GetStatusCode(response) if statusCode != http.StatusOK { return result, fmt.Errorf(fmt.Sprintf("Invalid status code %d", statusCode)) } body, err := c.HTTPClient.ToString(response) if err != nil { return result, fmt.Errorf(fmt.Sprintf("Invalid response: %s", err.Error())) } ok, err := result.LoadFromJSON([]byte(body)) if err != nil { return result, fmt.Errorf(fmt.Sprintf("Invalid response: %s", err.Error())) } if !ok { return result, fmt.Errorf("Invalid response") } return result, nil } // GetNamespace Get Namespace func (c *Client) GetNamespace(ctx context.Context, cluster, namespace string) (model.Namespace, error) { var result model.Namespace response, err := c.HTTPClient.Get( ctx, fmt.Sprintf("%s/api/v1/cluster/%s/namespace/%s", c.APIURL, cluster, namespace), map[string]string{}, map[string]string{"X-API-KEY": c.APIKey}, ) if err != nil { return result, err } statusCode := c.HTTPClient.GetStatusCode(response) if statusCode != http.StatusOK { return result, fmt.Errorf(fmt.Sprintf("Invalid status code %d", statusCode)) } body, err := c.HTTPClient.ToString(response) if err != nil { return result, fmt.Errorf(fmt.Sprintf("Invalid response: %s", err.Error())) } ok, err := result.LoadFromJSON([]byte(body)) if err != nil { return result, fmt.Errorf(fmt.Sprintf("Invalid response: %s", err.Error())) } if !ok { return result, fmt.Errorf("Invalid response") } return result, nil } ================================================ FILE: sdk/namespace_test.go ================================================ // Copyright 2020 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. package sdk import ( "bytes" "context" "fmt" "io/ioutil" "net/http" "os" "path/filepath" "testing" "github.com/clivern/beetle/core/model" "github.com/clivern/beetle/core/module" "github.com/clivern/beetle/pkg" "github.com/drone/envsubst" "github.com/spf13/viper" ) // TestNamespaceCRUD test cases func TestNamespaceCRUD(t *testing.T) { testingConfig := "config.testing.yml" httpClient := Client{} httpClient.SetHTTPClient(module.NewHTTPClient(20)) httpClient.SetAPIKey("") // LoadConfigFile t.Run("LoadConfigFile", func(t *testing.T) { fs := module.FileSystem{} dir, _ := os.Getwd() configFile := fmt.Sprintf("%s/%s", dir, testingConfig) for { if fs.FileExists(configFile) { break } dir = filepath.Dir(dir) configFile = fmt.Sprintf("%s/%s", dir, testingConfig) } t.Logf("Load Config File %s", configFile) configUnparsed, _ := ioutil.ReadFile(configFile) configParsed, _ := envsubst.EvalEnv(string(configUnparsed)) viper.SetConfigType("yaml") viper.ReadConfig(bytes.NewBuffer([]byte(configParsed))) }) // TestGetNamespaces t.Run("TestGetNamespaces", func(t *testing.T) { srv := pkg.ServerMock( "/api/v1/cluster/production/namespace", `{"namespaces": [{"name": "default","uid": "f03ea2f1-bc1c-4563-b9c7-4413dffc18db","status": "active"},{"name": "kube-node-lease","uid": "398c907f-d888-455d-871d-145752f9ca73","status": "active"}]}`, http.StatusOK, ) defer srv.Close() httpClient.SetAPIURL(srv.URL) result, err := httpClient.GetNamespaces(context.TODO(), "production") pkg.Expect(t, nil, err) pkg.Expect(t, result, model.Namespaces{ Namespaces: []model.Namespace{ model.Namespace{Name: "default", UID: "f03ea2f1-bc1c-4563-b9c7-4413dffc18db", Status: "active"}, model.Namespace{Name: "kube-node-lease", UID: "398c907f-d888-455d-871d-145752f9ca73", Status: "active"}, }, }) }) // TestGetNamespace t.Run("TestGetNamespace", func(t *testing.T) { srv := pkg.ServerMock( "/api/v1/cluster/production/namespace/default", `{"name":"default","status":"active","uid":"f03ea2f1-bc1c-4563-b9c7-4413dffc18db"}`, http.StatusOK, ) defer srv.Close() httpClient.SetAPIURL(srv.URL) result, err := httpClient.GetNamespace(context.TODO(), "production", "default") pkg.Expect(t, nil, err) pkg.Expect(t, result, model.Namespace{Name: "default", UID: "f03ea2f1-bc1c-4563-b9c7-4413dffc18db", Status: "active"}) }) } ================================================ FILE: swagger.yaml ================================================ swagger: '2.0' info: description: | Application deployment and management should be automated, auditable, and easy to understand and that\'s what beetle tries to achieve in a simple manner. Beetle automates the deployment and rollback of your applications in a multi-cluster, multi-namespaces kubernetes environments. Easy to integrate with through API endpoints & webhooks to fit a variety of workflows. version: 1.0.3 title: Beetle contact: email: hello@clivern.com license: name: MIT url: 'https://github.com/Clivern/Beetle/blob/main/LICENSE' host: beetle.yourcompany.com basePath: / schemes: - https - http paths: /_health: get: tags: - Healthcheck summary: Get system health status produces: - application/json parameters: [] responses: '200': description: system healthy schema: $ref: '#/definitions/healthResponse' '500': description: system is down schema: $ref: '#/definitions/healthResponse' /_ready: get: tags: - Readiness summary: Get system readiness produces: - application/json parameters: [] responses: '200': description: system ready to accept traffic schema: $ref: '#/definitions/healthResponse' '500': description: system not ready to accept traffic schema: $ref: '#/definitions/healthResponse' /metrics: get: tags: - Metrics summary: Get metrics for prometheus produces: - text/plain parameters: [] responses: '200': description: system metrics '500': description: Internal server error /api/v1/cluster: get: tags: - Cluster summary: Get clusters list description: '' operationId: getClusters produces: - application/json responses: '200': description: successful operation schema: $ref: '#/definitions/Clusters' '400': description: Invalid request '404': description: Resource not found '500': description: Internal server error security: - api_key: [] '/api/v1/cluster/{cn}': get: tags: - Cluster summary: Get cluster by name description: '' operationId: getClusterByName produces: - application/json parameters: - in: path name: cn description: The name of the cluster required: true type: string responses: '200': description: successful operation schema: $ref: '#/definitions/Cluster' '400': description: Invalid request '404': description: Resource not found '500': description: Internal server error security: - api_key: [] '/api/v1/cluster/{cn}/namespace': get: tags: - Namespace summary: Get namespaces list description: '' operationId: getNamespaces produces: - application/json parameters: - in: path name: cn description: The name of the cluster required: true type: string responses: '200': description: successful operation schema: $ref: '#/definitions/Namespaces' '400': description: Invalid request '404': description: Resource not found '500': description: Internal server error security: - api_key: [] '/api/v1/cluster/{cn}/namespace/{ns}': get: tags: - Namespace summary: Get cluster namespace by name description: '' operationId: getClusterNamespaceByName produces: - application/json parameters: - in: path name: cn description: The name of the cluster required: true type: string - in: path name: ns description: The name of the cluster namespace required: true type: string responses: '200': description: successful operation schema: $ref: '#/definitions/Namespace' '400': description: Invalid request '404': description: Resource not found '500': description: Internal server error security: - api_key: [] '/api/v1/cluster/{cn}/namespace/{ns}/app': get: tags: - Application summary: Get applications list description: '' operationId: getApplications produces: - application/json parameters: - in: path name: cn description: The name of the cluster required: true type: string - in: path name: ns description: The name of the cluster namespace required: true type: string responses: '200': description: successful operation schema: $ref: '#/definitions/Applications' '400': description: Invalid request '404': description: Resource not found '500': description: Internal server error security: - api_key: [] '/api/v1/cluster/{cn}/namespace/{ns}/app/{id}': get: tags: - Application summary: Get application by id description: '' operationId: getApplicationById produces: - application/json parameters: - in: path name: cn description: The name of the cluster required: true type: string - in: path name: ns description: The name of the cluster namespace required: true type: string - in: path name: id description: The application id required: true type: string responses: '200': description: successful operation schema: $ref: '#/definitions/Application' '400': description: Invalid request '404': description: Resource not found '500': description: Internal server error security: - api_key: [] post: tags: - Application summary: Create a deployment request description: '' operationId: createDeploymentRequest produces: - application/json parameters: - in: path name: cn description: The name of the cluster required: true type: string - in: path name: ns description: The name of the cluster namespace required: true type: string - in: path name: id description: The application id required: true type: string - in: body name: body description: The deployment request required: true schema: $ref: '#/definitions/DeploymentRequest' responses: '202': description: successful operation schema: $ref: '#/definitions/Job' '400': description: Invalid request '404': description: Resource not found '500': description: Internal server error security: - api_key: [] /api/v1/job: get: tags: - Job summary: Get jobs list description: '' operationId: getJobs produces: - application/json responses: '200': description: successful operation schema: $ref: '#/definitions/Jobs' '400': description: Invalid request '500': description: Internal server error security: - api_key: [] '/api/v1/job/{uuid}': get: tags: - Job summary: Get a job by UUID description: '' operationId: getJobByUUID produces: - application/json parameters: - in: path name: uuid description: The UUID of the job required: true type: string responses: '200': description: successful operation schema: $ref: '#/definitions/Job' '400': description: Invalid request '404': description: Job not found '500': description: Internal server error security: - api_key: [] delete: tags: - Job summary: Delete a job by UUID description: '' operationId: deleteJobByUUID produces: - application/json parameters: - in: path name: uuid description: The UUID of the job required: true type: string responses: '204': description: successful operation '400': description: Invalid request '404': description: Job not found '500': description: Internal server error security: - api_key: [] securityDefinitions: api_key: type: apiKey name: X-API-KEY in: header definitions: healthResponse: type: object properties: status: type: string Cluster: type: object properties: name: type: string health: type: boolean Clusters: type: object properties: clusters: type: array items: $ref: '#/definitions/Cluster' Namespace: type: object properties: name: type: string uid: type: string status: type: string Namespaces: type: object properties: namespaces: type: array items: $ref: '#/definitions/Namespace' Job: type: object properties: id: type: integer format: int64 uuid: type: string payload: type: string status: type: string type: type: string result: type: string retry: type: integer format: int64 parent: type: integer format: int64 run_at: type: string created_at: type: string updated_at: type: string Jobs: type: object properties: namespaces: type: array items: $ref: '#/definitions/Job' Applications: type: object properties: namespaces: type: array items: $ref: '#/definitions/Application' Application: type: object properties: id: type: string name: type: string format: type: string containers: type: array items: $ref: '#/definitions/Container' Container: type: object properties: name: type: string image: type: string version: type: string deployment: $ref: '#/definitions/Deployment' Deployment: type: object properties: name: type: string uid: type: string DeploymentRequest: type: object properties: version: type: string strategy: type: string externalDocs: description: Find out more about beetle url: 'https://github.com/Clivern/Beetle'