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
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'