[
  {
    "path": ".gitattributes",
    "content": "docs/* linguist-vendored\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "# Docs: https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners\n*       @clivern\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: # clivern\ncustom: clivern.com/sponsor/\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**Development or production environment**\n - OS: [e.g. Ubuntu 18.04]\n - Go 1.13\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/auto-merge.yml",
    "content": "# https://github.com/bobvanderlinden/probot-auto-merge\nblockingLabels:\n- blocking\nrules:\n- minApprovals:\n  OWNER: 1\n  MEMBER: 2\n- requiredLabels:\n  - merge\n"
  },
  {
    "path": ".github/boring-cyborg.yml",
    "content": "---\nfirstIssueWelcomeComment: \"Thanks for opening your first issue here! Be sure to follow the issue template!\"\nfirstPRMergeComment: \"Awesome work, congrats on your first merged pull request!\"\nfirstPRWelcomeComment: \"Thanks for opening this pull request! Please check out our contributing guidelines.\"\n\nlabelPRBasedOnFilePath:\n    \"🚧 CI\":\n        - .github/workflows/*\n\n    \"🚧 CSS\":\n        - \"**/*.css\"\n\n    \"🚧 Configuration\":\n        - .github/*\n\n    \"🚧 Dependencies\":\n        - Dockerfile*\n        - composer.*\n        - package.json\n        - package-lock.json\n        - yarn.lock\n        - go.mod\n        - go.sum\n        - build.gradle\n        - Cargo.toml\n        - Cargo.lock\n        - Gemfile.lock\n        - Gemfile\n\n    \"🚧 Docker\":\n        - Dockerfile*\n        - .docker/**/*\n\n    \"🚧 Documentation\":\n        - README.md\n        - CONTRIBUTING.md\n\n    \"🚧 Go\":\n        - \"**/*.go\"\n\n    \"🚧 Rust\":\n        - \"**/*.rs\"\n\n    \"🚧 Java\":\n        - \"**/*.java\"\n\n    \"🚧 Ruby\":\n        - \"**/*.rb\"\n\n    \"🚧 HTML\":\n        - \"**/*.htm\"\n        - \"**/*.html\"\n\n    \"🚧 Image\":\n        - \"**/*.gif\"\n        - \"**/*.jpg\"\n        - \"**/*.jpeg\"\n        - \"**/*.png\"\n        - \"**/*.webp\"\n\n    \"🚧 JSON\":\n        - \"**/*.json\"\n\n    \"🚧 JavaScript\":\n        - \"**/*.js\"\n        - package.json\n        - package-lock.json\n        - yarn.lock\n\n    \"🚧 MarkDown\":\n        - \"**/*.md\"\n\n    \"🚧 PHP\":\n        - \"**/*.php\"\n        - composer.*\n\n    \"🚧 Source\":\n        - src/**/*\n\n    \"🚧 TOML\":\n        - \"**/*.toml\"\n\n    \"🚧 Templates\":\n        - \"**/*.twig\"\n        - \"**/*.tpl\"\n\n    \"🚧 Tests\":\n        - tests/**/*\n\n    \"🚧 YAML\":\n        - \"**/*.yml\"\n        - \"**/*.yaml\"\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\n\non:\n  push:\n  pull_request:\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        go: ['1.19', '1.20.4']\n    name: Go ${{ matrix.go }} run\n    steps:\n      - uses: actions/checkout@v4\n      - name: Setup go\n        uses: actions/setup-go@v4\n        with:\n          go-version: ${{ matrix.go }}\n\n      - name: Get dependencies\n        run: |\n          export PATH=${PATH}:`go env GOPATH`/bin\n          make install_revive\n\n      - name: Run make ci\n        run: |\n          export PATH=${PATH}:`go env GOPATH`/bin\n          go get -t .\n          make ci\n          git status\n          git diff > diff.log\n          cat diff.log\n          git clean -fd\n          git reset --hard\n          make verify\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    tags:\n      - '*'\n\njobs:\n  goreleaser:\n    runs-on: ubuntu-latest\n    steps:\n      -\n        name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      -\n        name: Set up Go\n        uses: actions/setup-go@v4\n        with:\n          go-version: 1.19\n      -\n        name: Run GoReleaser\n        uses: goreleaser/goreleaser-action@v3\n        with:\n          version: latest\n          args: release --rm-dist\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/release_pkg.yml",
    "content": "name: ReleasePkg\n\non:\n  push:\n    tags:\n      - '*'\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n    steps:\n      -\n        name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      -\n        name: Set up Go\n        uses: actions/setup-go@v4\n        with:\n          go-version: 1.19\n\n      - name: Update checksum database\n        run: |\n          ./bin/release.sh\n"
  },
  {
    "path": ".gitignore",
    "content": "# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, build with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\nconfig.prod.yml\n\n# dist dir\ndist\n"
  },
  {
    "path": ".go-version",
    "content": "1.20.4\n"
  },
  {
    "path": ".goreleaser.yml",
    "content": "# This is an example goreleaser.yaml file with some sane defaults.\n# Make sure to check the documentation at http://goreleaser.com\n---\narchives:\n  -\n    replacements:\n      386: i386\n      amd64: x86_64\n      darwin: Darwin\n      linux: Linux\n      windows: Windows\nbefore:\n  hooks:\n    - \"go mod download\"\n    - \"go generate ./...\"\nbuilds:\n  -\n    env:\n      - CGO_ENABLED=0\n    goos:\n      - linux\n      - darwin\n      - windows\n\nchangelog:\n  filters:\n    exclude:\n      - \"^docs:\"\n      - \"^test:\"\n  sort: asc\nchecksum:\n  name_template: checksums.txt\nsnapshot:\n  name_template: \"{{ .Tag }}-next\"\nproject_name: beetle\n"
  },
  {
    "path": ".mergify.yml",
    "content": "---\npull_request_rules:\n  -\n    actions:\n      merge:\n        method: squash\n    conditions:\n      - author!=Clivern\n      - approved-reviews-by=Clivern\n      - label=release\n    name: \"Automatic Merge 🚀\"\n  -\n    actions:\n      merge:\n        method: merge\n    conditions:\n      - author=Clivern\n      - label=release\n    name: \"Automatic Merge 🚀\"\n  -\n    actions:\n      merge:\n        method: squash\n    conditions:\n      - \"author=renovate[bot]\"\n      - label=release\n    name: \"Automatic Merge for Renovate PRs 🚀\"\n  -\n    actions:\n      comment:\n        message: \"Nice! PR merged successfully.\"\n    conditions:\n      - merged\n    name: \"Merge Done 🚀\"\n"
  },
  {
    "path": ".poodle.toml",
    "content": "# API Definition For Beetle\n# --------------------------\n#\n# In order to use this file:\n# 1. Check & Install https://github.com/Clivern/Poodle\n#\n# 2. Now you can use poodle to interact with your local or hosted beetle.\n#   $ poodle call -f .poodle.toml\n#\n\n[Main]\n    id = \"clivern_beetle\"\n    name = \"Clivern - Beetle\"\n    description = \"Beetle API Definitions\"\n    timeout = \"30s\"\n    service_url = \"{$serviceURL:http://127.0.0.1:8080}\"\n    # These headers will be applied to all endpoints http calls\n    headers = []\n\n[Security]\n    # Supported Types are basic, bearer and api_key and none\n    scheme = \"none\"\n\n    [Security.Basic]\n        username = \"{$authUsername:default}\"\n        password = \"{$authPassword:default}\"\n        header = [\"Authorization\", \"Basic base64(username:password)\"]\n\n    [Security.ApiKey]\n        header = [\"X-API-KEY\", \"{$authApiKey:default}\"]\n\n    # In case of bearer authentication, it is recommended to create another\n    # service or endpoint to generate the bearer tokens\n    [Security.Bearer]\n        header = [\"Authorization\", \"Bearer {$authBearerToken:default}\"]\n\n[[Endpoint]]\n    id = \"GetSystemHealth\"\n    name = \"Get System Health\"\n    description = \"\"\n    method = \"get\"\n    headers = [ [\"Content-Type\", \"application/json\"] ]\n    parameters = []\n    public = true\n    uri = \"/_health\"\n    body = \"\"\n\n[[Endpoint]]\n    id = \"GetSystemReadiness\"\n    name = \"Get System Readiness\"\n    description = \"\"\n    method = \"get\"\n    headers = [ [\"Content-Type\", \"application/json\"] ]\n    parameters = []\n    public = true\n    uri = \"/_ready\"\n    body = \"\"\n\n[[Endpoint]]\n    id = \"GetMetrics\"\n    name = \"Get Metrics\"\n    description = \"\"\n    method = \"get\"\n    headers = []\n    parameters = []\n    public = true\n    uri = \"/metrics\"\n    body = \"\"\n\n[[Endpoint]]\n    id = \"GetClusters\"\n    name = \"Get Clusters\"\n    description = \"\"\n    method = \"get\"\n    headers = [ [\"Content-Type\", \"application/json\"] ]\n    parameters = []\n    public = true\n    uri = \"/api/v1/cluster\"\n    body = \"\"\n\n[[Endpoint]]\n    id = \"GetCluster\"\n    name = \"Get Cluster\"\n    description = \"\"\n    method = \"get\"\n    headers = [ [\"Content-Type\", \"application/json\"] ]\n    parameters = []\n    public = true\n    uri = \"/api/v1/cluster/{$clusterName}\"\n    body = \"\"\n\n[[Endpoint]]\n    id = \"GetClusterNamespaces\"\n    name = \"Get Cluster Namespaces\"\n    description = \"\"\n    method = \"get\"\n    headers = [ [\"Content-Type\", \"application/json\"] ]\n    parameters = []\n    public = true\n    uri = \"/api/v1/cluster/{$clusterName}/namespace\"\n    body = \"\"\n\n[[Endpoint]]\n    id = \"GetClusterNamespace\"\n    name = \"Get Cluster Namespace\"\n    description = \"\"\n    method = \"get\"\n    headers = [ [\"Content-Type\", \"application/json\"] ]\n    parameters = []\n    public = true\n    uri = \"/api/v1/cluster/{$clusterName}/namespace/{$namespaceName}\"\n    body = \"\"\n\n[[Endpoint]]\n    id = \"GetNamespaceApplications\"\n    name = \"Get Namespace Applications\"\n    description = \"\"\n    method = \"get\"\n    headers = [ [\"Content-Type\", \"application/json\"] ]\n    parameters = []\n    public = true\n    uri = \"/api/v1/cluster/{$clusterName}/namespace/{$namespaceName}/app\"\n    body = \"\"\n\n[[Endpoint]]\n    id = \"GetNamespaceApplicationByID\"\n    name = \"Get Namespace Application by ID\"\n    description = \"\"\n    method = \"get\"\n    headers = [ [\"Content-Type\", \"application/json\"] ]\n    parameters = []\n    public = true\n    uri = \"/api/v1/cluster/{$clusterName}/namespace/{$namespaceName}/app/{$applicationId}\"\n    body = \"\"\n\n[[Endpoint]]\n    id = \"GetJobs\"\n    name = \"Get Jobs\"\n    description = \"\"\n    method = \"get\"\n    headers = [ [\"Content-Type\", \"application/json\"] ]\n    parameters = []\n    public = true\n    uri = \"/api/v1/job\"\n    body = \"\"\n\n[[Endpoint]]\n    id = \"GetJobByUUID\"\n    name = \"Get Job by UUID\"\n    description = \"\"\n    method = \"get\"\n    headers = [ [\"Content-Type\", \"application/json\"] ]\n    parameters = []\n    uri = \"/api/v1/job/{$jobUUID}\"\n    body = \"\"\n\n[[Endpoint]]\n    id = \"DeleteJobByUUID\"\n    name = \"Delete Job by UUID\"\n    description = \"\"\n    method = \"delete\"\n    headers = [ [\"Content-Type\", \"application/json\"] ]\n    parameters = []\n    uri = \"/api/v1/job/{$jobUUID}\"\n    body = \"\"\n\n[[Endpoint]]\n    id = \"DeployApplicationById\"\n    name = \"Deploy Application By ID\"\n    description = \"\"\n    method = \"post\"\n    headers = [ [\"Content-Type\", \"application/json\"] ]\n    parameters = []\n    uri = \"/api/v1/cluster/{$clusterName}/namespace/{$namespaceName}/app/{$applicationId}/deployment\"\n    body = \"\"\"\n    {\n        \"version\":\"{$version}\",\n        \"strategy\":\"{$strategy:recreate}\"\n    }\n    \"\"\""
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at hello@clivern.com. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "## Contributing\n\n- With issues:\n  - Use the search tool before opening a new issue.\n  - Please provide source code and commit sha if you found a bug.\n  - Review existing issues and provide feedback or react to them.\n\n- With pull requests:\n  - Open your pull request against `main`\n  - Your pull request should have no more than two commits, if not you should squash them.\n  - It should pass all tests in the available continuous integrations systems such as TravisCI.\n  - You should add/modify tests to cover your proposed code changes.\n  - If your pull request contains a new feature, please document it on the README.\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM golang:1.20.2\n\nARG BEETLE_VERSION=1.0.2\n\nENV GO111MODULE=on\n\nRUN mkdir -p /app/configs\nRUN mkdir -p /app/var/logs\nRUN mkdir -p /app/var/storage\nRUN apt-get update\n\nWORKDIR /app\n\nRUN curl -sL https://github.com/Clivern/Beetle/releases/download/v${BEETLE_VERSION}/beetle_${BEETLE_VERSION}_Linux_x86_64.tar.gz | tar xz\nRUN rm LICENSE\nRUN rm README.md\n\nCOPY ./config.dist.yml /app/configs/\n\nEXPOSE 8080\n\nVOLUME /app/configs\nVOLUME /app/var\n\nRUN ./beetle version\n\nCMD [\"./beetle\", \"serve\", \"-c\", \"/app/configs/config.dist.yml\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Clivern\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "GO           ?= go\nGOFMT        ?= $(GO)fmt\npkgs          = ./...\nHUGO ?= hugo\n\n\nhelp: Makefile\n\t@echo\n\t@echo \" Choose a command run in Beetle:\"\n\t@echo\n\t@sed -n 's/^##//p' $< | column -t -s ':' |  sed -e 's/^/ /'\n\t@echo\n\n\n## install_revive: Install revive for linting.\ninstall_revive:\n\t@echo \">> ============= Install Revive ============= <<\"\n\t$(GO) install github.com/mgechev/revive@latest\n\n\n## style: Check code style.\nstyle:\n\t@echo \">> ============= Checking Code Style ============= <<\"\n\t@fmtRes=$$($(GOFMT) -d $$(find . -path ./vendor -prune -o -name '*.go' -print)); \\\n\tif [ -n \"$${fmtRes}\" ]; then \\\n\t\techo \"gofmt checking failed!\"; echo \"$${fmtRes}\"; echo; \\\n\t\techo \"Please ensure you are using $$($(GO) version) for formatting code.\"; \\\n\t\texit 1; \\\n\tfi\n\n\n## check_license: Check if license header on all files.\ncheck_license:\n\t@echo \">> ============= Checking License Header ============= <<\"\n\t@licRes=$$(for file in $$(find . -type f -iname '*.go' ! -path './vendor/*') ; do \\\n               awk 'NR<=3' $$file | grep -Eq \"(Copyright|generated|GENERATED)\" || echo $$file; \\\n       done); \\\n       if [ -n \"$${licRes}\" ]; then \\\n               echo \"license header checking failed:\"; echo \"$${licRes}\"; \\\n               exit 1; \\\n       fi\n\n\n## test_short: Run test cases with short flag.\ntest_short:\n\t@echo \">> ============= Running Short Tests ============= <<\"\n\t$(GO) test -mod=readonly -short $(pkgs)\n\n\n## test: Run test cases.\ntest:\n\t@echo \">> ============= Running All Tests ============= <<\"\n\t$(GO) test -mod=readonly -v -cover $(pkgs)\n\n\n## lint: Lint the code.\nlint:\n\t@echo \">> ============= Lint All Files ============= <<\"\n\trevive -config config.toml -exclude vendor/... -formatter friendly ./...\n\n\n## verify: Verify dependencies\nverify:\n\t@echo \">> ============= List Dependencies ============= <<\"\n\t$(GO) list -m all\n\t@echo \">> ============= Verify Dependencies ============= <<\"\n\t$(GO) mod verify\n\n\n## format: Format the code.\nformat:\n\t@echo \">> ============= Formatting Code ============= <<\"\n\t$(GO) fmt $(pkgs)\n\n\n## vet: Examines source code and reports suspicious constructs.\nvet:\n\t@echo \">> ============= Vetting Code ============= <<\"\n\t$(GO) vet $(pkgs)\n\n\n## coverage: Create HTML coverage report\ncoverage:\n\t@echo \">> ============= Coverage ============= <<\"\n\trm -f coverage.html cover.out\n\t$(GO) test -mod=readonly -coverprofile=cover.out $(pkgs)\n\tgo tool cover -html=cover.out -o coverage.html\n\n\n## ci: Run all CI tests.\nci: style check_license test vet lint\n\t@echo \"\\n==> All quality checks passed\"\n\n\n## run: Run the service\nrun:\n\t-cp -n config.dist.yml config.prod.yml\n\t$(GO) run beetle.go serve -c config.prod.yml\n\n\n.PHONY: help\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n    <img src=\"https://raw.githubusercontent.com/clivern/Beetle/main/assets/img/gopher.png?v=1.0.4\" width=\"180\" />\n    <h3 align=\"center\">Beetle</h3>\n    <p align=\"center\">Kubernetes multi-cluster deployment automation service</p>\n    <p align=\"center\">\n        <a href=\"https://github.com/Clivern/Beetle/actions\"><img src=\"https://github.com/Clivern/Beetle/workflows/Build/badge.svg\"></a>\n        <a href=\"https://github.com/Clivern/Beetle/actions\"><img src=\"https://github.com/Clivern/Beetle/workflows/Release/badge.svg\"></a>\n        <a href=\"https://github.com/Clivern/Beetle/releases\"><img src=\"https://img.shields.io/badge/Version-v1.0.4-red.svg\"></a>\n        <a href=\"https://goreportcard.com/report/github.com/Clivern/Beetle\"><img src=\"https://goreportcard.com/badge/github.com/clivern/Beetle?v=1.0.4\"></a>\n        <a href=\"https://godoc.org/github.com/clivern/beetle\"><img src=\"https://godoc.org/github.com/clivern/beetle?status.svg\"></a>\n        <a href=\"https://hub.docker.com/r/clivern/beetle\"><img src=\"https://img.shields.io/badge/Docker-Latest-orange\"></a>\n        <a href=\"https://github.com/Clivern/Beetle/blob/main/LICENSE\"><img src=\"https://img.shields.io/badge/LICENSE-MIT-orange.svg\"></a>\n    </p>\n</p>\n<br/>\n<p align=\"center\">\n    <img src=\"https://raw.githubusercontent.com/Clivern/Beetle/main/assets/img/chart.png?v=1.0.4\" width=\"100%\" />\n</p>\n\n<h4 align=\"center\">\n    <a href=\"https://www.youtube.com/watch?v=54qQIYTZiAw\" target=\"_blank\">:unicorn: Check out the demo!</a>\n</h4>\n<br/>\n\nApplication 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.\n\n\n## Documentation\n\n## Deployment\n\n### On a Linux Server\n\nDownload [the latest beetle binary.](https://github.com/Clivern/Beetle/releases)\n\n```zsh\n$ curl -sL https://github.com/Clivern/Beetle/releases/download/vx.x.x/beetle_x.x.x_OS.tar.gz | tar xz\n```\n\nCreate your config file as explained on [development part](#development) and run beetle with systemd or anything else you prefer.\n\n```zsh\n$ ./beetle serve -c /custom/path/config.prod.yml\n```\n\n\n## Development\n\nBeetle uses Go Modules to manage dependencies. First Create a prod config file.\n\n```zsh\n$ git clone https://github.com/Clivern/Beetle.git\n$ cp config.dist.yml config.prod.yml\n```\n\nThen add your default configs. You probably wondering how the following configs even work! let's pick one and explain.\n\nThe 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\n\n```yaml\n# App configs\napp:\n    # Env mode (dev or prod)\n    mode: ${BEETLE_APP_MODE:-dev}\n    # HTTP port\n    port: ${BEETLE_API_PORT:-8080}\n    # App URL\n    domain: ${BEETLE_APP_DOMAIN:-http://127.0.0.1:8080}\n    # TLS configs\n    tls:\n        status: ${BEETLE_API_TLS_STATUS:-off}\n        pemPath: ${BEETLE_API_TLS_PEMPATH:-cert/server.pem}\n        keyPath: ${BEETLE_API_TLS_KEYPATH:-cert/server.key}\n\n    # Message Broker Configs\n    broker:\n        # Broker driver (native)\n        driver: ${BEETLE_BROKER_DRIVER:-native}\n        # Native driver configs\n        native:\n            # Queue max capacity\n            capacity: ${BEETLE_BROKER_NATIVE_CAPACITY:-5000}\n            # Number of concurrent workers\n            workers: ${BEETLE_BROKER_NATIVE_WORKERS:-4}\n\n    # API Configs\n    api:\n        key: ${BEETLE_API_KEY:- }\n\n    # Runtime, Requests/Response and Beetle Metrics\n    metrics:\n        prometheus:\n            # Route for the metrics endpoint\n            endpoint: ${BEETLE_METRICS_PROM_ENDPOINT:-/metrics}\n\n    # Application Database\n    database:\n        # Database driver (sqlite3, mysql)\n        driver: ${BEETLE_DATABASE_DRIVER:-sqlite3}\n        # Database Host\n        host: ${BEETLE_DATABASE_MYSQL_HOST:-localhost}\n        # Database Port\n        port: ${BEETLE_DATABASE_MYSQL_PORT:-3306}\n        # Database Name\n        name: ${BEETLE_DATABASE_MYSQL_DATABASE:-beetle.db}\n        # Database Username\n        username: ${BEETLE_DATABASE_MYSQL_USERNAME:-root}\n        # Database Password\n        password: ${BEETLE_DATABASE_MYSQL_PASSWORD:-root}\n\n    # Kubernetes Clusters\n    clusters:\n        -\n            name: ${BEETLE_KUBE_CLUSTER_01_NAME:-production}\n            inCluster: ${BEETLE_KUBE_CLUSTER_01_IN_CLUSTER:-false}\n            kubeconfig: ${BEETLE_KUBE_CLUSTER_01_CONFIG_FILE:-/app/configs/production-cluster-kubeconfig.yaml}\n        -\n            name: ${BEETLE_KUBE_CLUSTER_02_NAME:-staging}\n            inCluster: ${BEETLE_KUBE_CLUSTER_02_IN_CLUSTER:-false}\n            kubeconfig: ${BEETLE_KUBE_CLUSTER_02_CONFIG_FILE:-/app/configs/staging-cluster-kubeconfig.yaml}\n\n    # HTTP Webhook\n    webhook:\n        url: ${BEETLE_WEBHOOK_URL:- }\n        retry: ${BEETLE_WEBHOOK_RETRY:-3}\n        apiKey: ${BEETLE_WEBHOOK_API_KEY:- }\n\n# Log configs\nlog:\n    # Log level, it can be debug, info, warn, error, panic, fatal\n    level: ${BEETLE_LOG_LEVEL:-info}\n    # output can be stdout or abs path to log file /var/logs/beetle.log\n    output: ${BEETLE_LOG_OUTPUT:-stdout}\n    # Format can be json\n    format: ${BEETLE_LOG_FORMAT:-json}\n```\n\nAnd then run the application.\n\n```zsh\n$ go build beetle.go\n$ ./beetle serve -c /custom/path/config.prod.yml\n\n// OR\n\n$ make run\n\n// OR\n\n$ go run beetle.go serve -c /custom/path/config.prod.yml\n```\n\n\n## API Documentation\n\nGo to https://editor.swagger.io/ and import this file https://raw.githubusercontent.com/Clivern/Beetle/main/swagger.yaml.\n\n\n## Versioning\n\nFor 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.\n\nSee 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.\n\n\n## Bug tracker\n\nIf you have any suggestions, bug reports, or annoyances please report them to our issue tracker at https://github.com/clivern/beetle/issues\n\n\n## Security Issues\n\nIf you discover a security vulnerability within Beetle, please send an email to [hello@clivern.com](mailto:hello@clivern.com)\n\n\n## Contributing\n\nWe are an open source, community-driven project so please feel free to join us. see the [contributing guidelines](CONTRIBUTING.md) for more details.\n\n\n## License\n\n© 2020, clivern. Released under [MIT License](https://opensource.org/licenses/mit-license.php).\n\n**Beetle** is authored and maintained by [@clivern](http://github.com/clivern).\n"
  },
  {
    "path": "assets/img/chart.drawio",
    "content": "<mxfile modified=\"2021-04-03T21:36:16.551Z\" host=\"app.diagrams.net\" agent=\"5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36\" etag=\"mCddqXOVAB87riqQZXYU\" version=\"14.5.7\" type=\"device\"><diagram id=\"d1ab7348-05c3-a1e3-ca4d-12c340fd6b49\" name=\"Page-1\">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=</diagram></mxfile>"
  },
  {
    "path": "beetle.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage main\n\nimport (\n\t\"github.com/clivern/beetle/core/cmd\"\n)\n\nvar (\n\tversion = \"dev\"\n\tcommit  = \"none\"\n\tdate    = \"unknown\"\n\tbuiltBy = \"unknown\"\n)\n\nfunc main() {\n\t// Expose build info to cmd subpackage to avoid custom ldflags\n\tcmd.Version = version\n\tcmd.Commit = commit\n\tcmd.Date = date\n\tcmd.BuiltBy = builtBy\n\n\tcmd.Execute()\n}\n"
  },
  {
    "path": "beetle_test.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage main\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/clivern/beetle/core/module\"\n\t\"github.com/clivern/beetle/pkg\"\n\n\t\"github.com/drone/envsubst\"\n\t\"github.com/spf13/viper\"\n)\n\nvar testingConfig = \"config.testing.yml\"\n\n// TestMain test cases\nfunc TestMain(t *testing.T) {\n\t// LoadConfigFile\n\tt.Run(\"LoadConfigFile\", func(t *testing.T) {\n\t\tfs := module.FileSystem{}\n\n\t\tdir, _ := os.Getwd()\n\t\tconfigFile := fmt.Sprintf(\"%s/%s\", dir, testingConfig)\n\n\t\tfor {\n\t\t\tif fs.FileExists(configFile) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tdir = filepath.Dir(dir)\n\t\t\tconfigFile = fmt.Sprintf(\"%s/%s\", dir, testingConfig)\n\t\t}\n\n\t\tt.Logf(\"Load Config File %s\", configFile)\n\n\t\tconfigUnparsed, _ := ioutil.ReadFile(configFile)\n\t\tconfigParsed, _ := envsubst.EvalEnv(string(configUnparsed))\n\t\tviper.SetConfigType(\"yaml\")\n\t\tviper.ReadConfig(bytes.NewBuffer([]byte(configParsed)))\n\n\t\tpkg.Expect(t, viper.GetString(\"app.mode\"), \"test\")\n\t})\n}\n"
  },
  {
    "path": "bin/release.sh",
    "content": "#!/bin/bash\n\n# Fetch latest version\nexport LATEST_VERSION=$(curl --silent \"https://api.github.com/repos/clivern/beetle/releases/latest\" | jq '.tag_name' | sed -E 's/.*\"([^\"]+)\".*/\\1/')\n\n# Update go checksum database (sum.golang.org) immediately after release\ncurl --silent https://sum.golang.org/lookup/github.com/clivern/beetle@{$LATEST_VERSION}\n"
  },
  {
    "path": "config.dist.yml",
    "content": "# App configs\napp:\n    # Env mode (dev or prod)\n    mode: ${BEETLE_APP_MODE:-dev}\n    # HTTP port\n    port: ${BEETLE_API_PORT:-8080}\n    # App URL\n    domain: ${BEETLE_APP_DOMAIN:-http://127.0.0.1:8080}\n    # TLS configs\n    tls:\n        status: ${BEETLE_API_TLS_STATUS:-off}\n        pemPath: ${BEETLE_API_TLS_PEMPATH:-cert/server.pem}\n        keyPath: ${BEETLE_API_TLS_KEYPATH:-cert/server.key}\n\n    # Message Broker Configs\n    broker:\n        # Broker driver (native)\n        driver: ${BEETLE_BROKER_DRIVER:-native}\n        # Native driver configs\n        native:\n            # Queue max capacity\n            capacity: ${BEETLE_BROKER_NATIVE_CAPACITY:-5000}\n            # Number of concurrent workers\n            workers: ${BEETLE_BROKER_NATIVE_WORKERS:-4}\n\n    # API Configs\n    api:\n        key: ${BEETLE_API_KEY:- }\n\n    # Runtime, Requests/Response and Beetle Metrics\n    metrics:\n        prometheus:\n            # Route for the metrics endpoint\n            endpoint: ${BEETLE_METRICS_PROM_ENDPOINT:-/metrics}\n\n    # Application Database\n    database:\n        # Database driver (sqlite3, mysql)\n        driver: ${BEETLE_DATABASE_DRIVER:-sqlite3}\n        # Hostname\n        host: ${BEETLE_DATABASE_MYSQL_HOST:-localhost}\n        # Port\n        port: ${BEETLE_DATABASE_MYSQL_PORT:-3306}\n        # Database\n        name: ${BEETLE_DATABASE_MYSQL_DATABASE:-beetle.db}\n        # Username\n        username: ${BEETLE_DATABASE_MYSQL_USERNAME:-root}\n        # Password\n        password: ${BEETLE_DATABASE_MYSQL_PASSWORD:-root}\n\n    # Kubernetes Clusters\n    clusters:\n        -\n            name: ${BEETLE_KUBE_CLUSTER_01_NAME:-production}\n            inCluster: ${BEETLE_KUBE_CLUSTER_01_IN_CLUSTER:-false}\n            kubeconfig: ${BEETLE_KUBE_CLUSTER_01_CONFIG_FILE:-/app/configs/production-cluster-kubeconfig.yaml}\n        -\n            name: ${BEETLE_KUBE_CLUSTER_02_NAME:-staging}\n            inCluster: ${BEETLE_KUBE_CLUSTER_02_IN_CLUSTER:-false}\n            kubeconfig: ${BEETLE_KUBE_CLUSTER_02_CONFIG_FILE:-/app/configs/staging-cluster-kubeconfig.yaml}\n\n    # HTTP Webhook\n    webhook:\n        url: ${BEETLE_WEBHOOK_URL:- }\n        retry: ${BEETLE_WEBHOOK_RETRY:-3}\n        apiKey: ${BEETLE_WEBHOOK_API_KEY:- }\n\n# Log configs\nlog:\n    # Log level, it can be debug, info, warn, error, panic, fatal\n    level: ${BEETLE_LOG_LEVEL:-info}\n    # output can be stdout or abs path to log file /var/logs/beetle.log\n    output: ${BEETLE_LOG_OUTPUT:-stdout}\n    # Format can be json\n    format: ${BEETLE_LOG_FORMAT:-json}\n"
  },
  {
    "path": "config.testing.yml",
    "content": "# App configs\napp:\n    # Env mode (dev or prod)\n    mode: ${BEETLE_APP_MODE:-test}\n    # HTTP port\n    port: ${BEETLE_API_PORT:-8080}\n    # App URL\n    domain: ${BEETLE_APP_DOMAIN:-http://127.0.0.1:8080}\n    # TLS configs\n    tls:\n        status: ${BEETLE_API_TLS_STATUS:-off}\n        pemPath: ${BEETLE_API_TLS_PEMPATH:-cert/server.pem}\n        keyPath: ${BEETLE_API_TLS_KEYPATH:-cert/server.key}\n\n    # Message Broker Configs\n    broker:\n        # Broker driver (native)\n        driver: ${BEETLE_BROKER_DRIVER:-native}\n        # Native driver configs\n        native:\n            # Queue max capacity\n            capacity: ${BEETLE_BROKER_NATIVE_CAPACITY:-5000}\n            # Number of concurrent workers\n            workers: ${BEETLE_BROKER_NATIVE_WORKERS:-4}\n\n    # API Configs\n    api:\n        key: ${BEETLE_API_KEY:- }\n\n    # Runtime, Requests/Response and Beetle Metrics\n    metrics:\n        prometheus:\n            # Route for the metrics endpoint\n            endpoint: ${BEETLE_METRICS_PROM_ENDPOINT:-/metrics}\n\n    # Application Database\n    database:\n        # Database driver (sqlite3, mysql)\n        driver: ${BEETLE_DATABASE_DRIVER:-sqlite3}\n        # Hostname\n        host: ${BEETLE_DATABASE_MYSQL_HOST:-localhost}\n        # Port\n        port: ${BEETLE_DATABASE_MYSQL_PORT:-3306}\n        # Database\n        name: ${BEETLE_DATABASE_MYSQL_DATABASE:-/tmp/beetle.db}\n        # Username\n        username: ${BEETLE_DATABASE_MYSQL_USERNAME:-root}\n        # Password\n        password: ${BEETLE_DATABASE_MYSQL_PASSWORD:- }\n\n    # Kubernetes Clusters\n    clusters:\n        -\n            name: ${BEETLE_KUBE_CLUSTER_01_NAME:-production}\n            inCluster: ${BEETLE_KUBE_CLUSTER_01_IN_CLUSTER:-false}\n            kubeconfig: ${BEETLE_KUBE_CLUSTER_01_CONFIG_FILE:-/app/configs/production-cluster-kubeconfig.yaml}\n        -\n            name: ${BEETLE_KUBE_CLUSTER_02_NAME:-staging}\n            inCluster: ${BEETLE_KUBE_CLUSTER_02_IN_CLUSTER:-false}\n            kubeconfig: ${BEETLE_KUBE_CLUSTER_02_CONFIG_FILE:-/app/configs/staging-cluster-kubeconfig.yaml}\n\n    # HTTP Webhook\n    webhook:\n        url: ${BEETLE_WEBHOOK_URL:- }\n        retry: ${BEETLE_WEBHOOK_RETRY:-3}\n        apiKey: ${BEETLE_WEBHOOK_API_KEY:- }\n\n# Log configs\nlog:\n    # Log level, it can be debug, info, warn, error, panic, fatal\n    level: ${BEETLE_LOG_LEVEL:-info}\n    # output can be stdout or abs path to log file /var/logs/beetle.log\n    output: ${BEETLE_LOG_OUTPUT:-stdout}\n    # Format can be json\n    format: ${BEETLE_LOG_FORMAT:-json}\n"
  },
  {
    "path": "config.toml",
    "content": "ignoreGeneratedHeader = false\nseverity = \"warning\"\nconfidence = 0.8\nerrorCode = 0\nwarningCode = 0\n\n[rule.blank-imports]\n[rule.context-as-argument]\n[rule.context-keys-type]\n[rule.dot-imports]\n[rule.error-return]\n[rule.error-strings]\n[rule.error-naming]\n[rule.exported]\n[rule.if-return]\n[rule.increment-decrement]\n[rule.var-naming]\n[rule.var-declaration]\n[rule.package-comments]\n[rule.range]\n[rule.receiver-naming]\n[rule.time-naming]\n[rule.unexported-return]\n[rule.indent-error-flow]\n[rule.errorf]\n[rule.empty-block]\n[rule.superfluous-else]\n[rule.unused-parameter]\n[rule.unreachable-code]\n[rule.redefines-builtin-id]"
  },
  {
    "path": "core/cmd/apps.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/clivern/beetle/core/module\"\n\t\"github.com/clivern/beetle/sdk\"\n\n\t\"github.com/olekukonko/tablewriter\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar (\n\t// Beetle API Server URL\n\tapiURL string\n\n\t// Beetle API Server API Key\n\tapiKey string\n\n\t// The Kubernetes Cluster\n\tcluster string\n\n\t// The Kubernetes Cluster Namespace\n\tnamespace string\n)\n\nvar getCmd = &cobra.Command{\n\tUse:   \"get\",\n\tShort: \"Get resources\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tfmt.Println(`You must specify the type of resource to get. Current supported resources are (apps).`)\n\t},\n}\n\nvar appsCmd = &cobra.Command{\n\tUse:   \"apps\",\n\tShort: \"Get a list of applications with cluster id and namespace\",\n\tRun: func(cmd *cobra.Command, aras []string) {\n\t\t// Usage\n\t\t// $ ./beetle get apps -u \"http://localhost:8080\" -k \"\" -c \"production\" -n \"default\"\n\n\t\tdata := [][]string{}\n\n\t\tclient := sdk.Client{}\n\t\tclient.SetHTTPClient(module.NewHTTPClient(20))\n\t\tclient.SetAPIURL(apiURL)\n\t\tclient.SetAPIKey(apiKey)\n\n\t\tapps, err := client.GetApplications(context.TODO(), cluster, namespace)\n\n\t\tif err != nil {\n\t\t\tdata = append(data, []string{\n\t\t\t\tfmt.Sprintf(\"Error: %s\", err.Error()),\n\t\t\t\t\"\",\n\t\t\t\t\"\",\n\t\t\t\t\"\",\n\t\t\t})\n\t\t} else {\n\t\t\tfor _, app := range apps.Applications {\n\t\t\t\tversion := \"N/A\"\n\n\t\t\t\tif len(app.Containers) > 0 {\n\t\t\t\t\tversion = app.Containers[0].Version\n\t\t\t\t}\n\n\t\t\t\tdata = append(data, []string{\n\t\t\t\t\tapp.ID,\n\t\t\t\t\tapp.Name,\n\t\t\t\t\tfmt.Sprintf(\"%d\", len(app.Containers)),\n\t\t\t\t\tversion,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\ttable := tablewriter.NewWriter(os.Stdout)\n\t\ttable.SetHeader([]string{\"ID\", \"Name\", \"Containers\", \"Version\"})\n\t\ttable.SetAutoWrapText(false)\n\t\ttable.SetAutoFormatHeaders(true)\n\t\ttable.SetHeaderAlignment(tablewriter.ALIGN_LEFT)\n\t\ttable.SetAlignment(tablewriter.ALIGN_LEFT)\n\t\ttable.SetCenterSeparator(\"\")\n\t\ttable.SetColumnSeparator(\"\")\n\t\ttable.SetRowSeparator(\"\")\n\t\ttable.SetHeaderLine(false)\n\t\ttable.SetBorder(false)\n\t\ttable.SetTablePadding(\"\\t\")\n\t\ttable.SetNoWhiteSpace(true)\n\t\ttable.AppendBulk(data)\n\t\ttable.Render()\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(getCmd)\n\n\tappsCmd.Flags().StringVarP(&namespace, \"namespace\", \"n\", \"default\", \"The Kubernetes Cluster Namespace (eg. default)\")\n\tappsCmd.MarkFlagRequired(\"namespace\")\n\n\tappsCmd.Flags().StringVarP(&cluster, \"cluster\", \"c\", \"\", \"The Kubernetes Cluster (eg. production)\")\n\tappsCmd.MarkFlagRequired(\"cluster\")\n\n\tappsCmd.Flags().StringVarP(&apiKey, \"api_key\", \"k\", \"\", \"API Key of the Beetle API Server\")\n\tappsCmd.MarkFlagRequired(\"api_key\")\n\n\tappsCmd.Flags().StringVarP(&apiURL, \"api_url\", \"u\", \"\", \"Beetle API Server URL (eg. https://example.com/)\")\n\tappsCmd.MarkFlagRequired(\"api_url\")\n\n\tgetCmd.AddCommand(appsCmd)\n}\n"
  },
  {
    "path": "core/cmd/deploy.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/clivern/beetle/core/model\"\n\t\"github.com/clivern/beetle/core/module\"\n\t\"github.com/clivern/beetle/sdk\"\n\n\t\"github.com/briandowns/spinner\"\n\t\"github.com/logrusorgru/aurora/v3\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar (\n\t// Application ID\n\tapplication string\n\n\t// Application Version\n\tversion string\n\n\t// Deployment Strategy\n\tstrategy string\n\n\t// Ramped Strategy MaxSurge\n\tmaxSurge string\n\n\t// Ramped Strategy MaxUnavailable\n\tmaxUnavailable string\n\n\t// Whether to watch the deployment\n\twatch bool\n)\n\nvar deployCmd = &cobra.Command{\n\tUse:   \"deploy\",\n\tShort: \"Deploy a new application version\",\n\tRun: func(cmd *cobra.Command, aras []string) {\n\t\t// Usage\n\t\t// $ ./beetle deploy -u \"http://localhost:8080\" -k \"\" -c \"production\" -n \"default\" -a \"toad\" -s \"recreate\" -v \"0.2.3\" -w\n\n\t\tclient := sdk.Client{}\n\t\tclient.SetHTTPClient(module.NewHTTPClient(20))\n\t\tclient.SetAPIURL(apiURL)\n\t\tclient.SetAPIKey(apiKey)\n\n\t\tspin := spinner.New(spinner.CharSets[26], 100*time.Millisecond)\n\t\tspin.Color(\"green\")\n\t\tspin.Start()\n\n\t\tjob, err := client.CreateDeployment(context.TODO(), model.DeploymentRequest{\n\t\t\tCluster:        cluster,\n\t\t\tNamespace:      namespace,\n\t\t\tApplication:    application,\n\t\t\tVersion:        version,\n\t\t\tStrategy:       strategy,\n\t\t\tMaxSurge:       maxSurge,\n\t\t\tMaxUnavailable: maxUnavailable,\n\t\t})\n\n\t\tif err != nil {\n\t\t\tfmt.Println(aurora.Red(fmt.Sprintf(\"Error: %s\", err.Error())))\n\t\t\tspin.Stop()\n\t\t\treturn\n\t\t}\n\n\t\tif watch {\n\t\t\tfor {\n\t\t\t\tjob, err := client.GetJob(context.TODO(), job.UUID)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\tfmt.Println(aurora.Red(fmt.Sprintf(\"Error: %s\", err.Error())))\n\t\t\t\t\tspin.Stop()\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif job.Status == model.JobFailed {\n\t\t\t\t\tfmt.Println(aurora.Red(fmt.Sprintf(\n\t\t\t\t\t\t\"Deployment Request %s Failed!\",\n\t\t\t\t\t\tjob.UUID,\n\t\t\t\t\t)))\n\n\t\t\t\t\tspin.Stop()\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif job.Status == model.JobSuccess {\n\t\t\t\t\tfmt.Println(aurora.Green(fmt.Sprintf(\n\t\t\t\t\t\t\"Deployment Request %s Succeeded!\",\n\t\t\t\t\t\tjob.UUID,\n\t\t\t\t\t)))\n\n\t\t\t\t\tspin.Stop()\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\ttime.Sleep(2 * time.Second)\n\t\t\t}\n\t\t} else {\n\t\t\tfmt.Println(aurora.Green(fmt.Sprintf(\n\t\t\t\t\"Deployment Request %s Submitted Successfully!\",\n\t\t\t\tjob.UUID,\n\t\t\t)))\n\t\t}\n\n\t\tspin.Stop()\n\t},\n}\n\nfunc init() {\n\tdeployCmd.Flags().StringVarP(&application, \"application\", \"a\", \"\", \"The Application ID\")\n\tdeployCmd.MarkFlagRequired(\"application\")\n\n\tdeployCmd.Flags().StringVarP(&version, \"version\", \"v\", \"\", \"The Application Version\")\n\tdeployCmd.MarkFlagRequired(\"version\")\n\n\tdeployCmd.Flags().StringVarP(&strategy, \"strategy\", \"s\", \"recreate\", \"The Deployment Strategy (recreate, ramped, canary or blue_green)\")\n\tdeployCmd.MarkFlagRequired(\"strategy\")\n\n\tdeployCmd.Flags().StringVarP(&maxSurge, \"max_surge\", \"g\", \"50%\", \"Deployment Strategy MaxSurge\")\n\n\tdeployCmd.Flags().StringVarP(&maxUnavailable, \"max_unavailable\", \"b\", \"50%\", \"Deployment Strategy MaxUnavailable\")\n\n\tdeployCmd.Flags().StringVarP(&namespace, \"namespace\", \"n\", \"default\", \"The Kubernetes Cluster Namespace (eg. default)\")\n\tdeployCmd.MarkFlagRequired(\"namespace\")\n\n\tdeployCmd.Flags().StringVarP(&cluster, \"cluster\", \"c\", \"\", \"The Kubernetes Cluster (eg. production)\")\n\tdeployCmd.MarkFlagRequired(\"cluster\")\n\n\tdeployCmd.Flags().StringVarP(&apiKey, \"api_key\", \"k\", \"\", \"API Key of the Beetle API Server\")\n\tdeployCmd.MarkFlagRequired(\"api_key\")\n\n\tdeployCmd.Flags().StringVarP(&apiURL, \"api_url\", \"u\", \"\", \"Beetle API Server URL (eg. https://example.com/)\")\n\tdeployCmd.MarkFlagRequired(\"api_url\")\n\n\tdeployCmd.Flags().BoolVarP(&watch, \"watch\", \"w\", false, \"Watch the deployment\")\n\n\trootCmd.AddCommand(deployCmd)\n}\n"
  },
  {
    "path": "core/cmd/license.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar licenseCmd = &cobra.Command{\n\tUse:   \"license\",\n\tShort: \"Get License\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tfmt.Println(`MIT License\n\nCopyright (c) 2020 Clivern\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.`)\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(licenseCmd)\n}\n"
  },
  {
    "path": "core/cmd/root.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar rootCmd = &cobra.Command{\n\tUse: \"beetle\",\n\tShort: `🔥 Kubernetes multi-cluster deployment automation service\n\nBeetle is in early stages of development, and we'd love to hear your\nfeedback at <https://github.com/Clivern/Beetle>`,\n}\n\n// Execute runs cmd tool\nfunc Execute() {\n\tif err := rootCmd.Execute(); err != nil {\n\t\tfmt.Println(err)\n\t\tos.Exit(1)\n\t}\n}\n"
  },
  {
    "path": "core/cmd/serve.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage cmd\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/clivern/beetle/core/controller\"\n\t\"github.com/clivern/beetle/core/middleware\"\n\t\"github.com/clivern/beetle/core/module\"\n\n\t\"github.com/drone/envsubst\"\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\nvar config string\n\nvar serveCmd = &cobra.Command{\n\tUse:   \"serve\",\n\tShort: \"Start beetle server\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tconfigUnparsed, err := ioutil.ReadFile(config)\n\n\t\tif err != nil {\n\t\t\tpanic(fmt.Sprintf(\n\t\t\t\t\"Error while reading config file [%s]: %s\",\n\t\t\t\tconfig,\n\t\t\t\terr.Error(),\n\t\t\t))\n\t\t}\n\n\t\tconfigParsed, err := envsubst.EvalEnv(string(configUnparsed))\n\n\t\tif err != nil {\n\t\t\tpanic(fmt.Sprintf(\n\t\t\t\t\"Error while parsing config file [%s]: %s\",\n\t\t\t\tconfig,\n\t\t\t\terr.Error(),\n\t\t\t))\n\t\t}\n\n\t\tviper.SetConfigType(\"yaml\")\n\t\terr = viper.ReadConfig(bytes.NewBufferString(configParsed))\n\n\t\tif err != nil {\n\t\t\tpanic(fmt.Sprintf(\n\t\t\t\t\"Error while loading configs [%s]: %s\",\n\t\t\t\tconfig,\n\t\t\t\terr.Error(),\n\t\t\t))\n\t\t}\n\n\t\tif viper.GetString(\"log.output\") != \"stdout\" {\n\t\t\tfs := module.FileSystem{}\n\t\t\tdir, _ := filepath.Split(viper.GetString(\"log.output\"))\n\n\t\t\tif !fs.DirExists(dir) {\n\t\t\t\tif _, err := fs.EnsureDir(dir, 0775); err != nil {\n\t\t\t\t\tpanic(fmt.Sprintf(\n\t\t\t\t\t\t\"Directory [%s] creation failed with error: %s\",\n\t\t\t\t\t\tdir,\n\t\t\t\t\t\terr.Error(),\n\t\t\t\t\t))\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !fs.FileExists(viper.GetString(\"log.output\")) {\n\t\t\t\tf, err := os.Create(viper.GetString(\"log.output\"))\n\t\t\t\tif err != nil {\n\t\t\t\t\tpanic(fmt.Sprintf(\n\t\t\t\t\t\t\"Error while creating log file [%s]: %s\",\n\t\t\t\t\t\tviper.GetString(\"log.output\"),\n\t\t\t\t\t\terr.Error(),\n\t\t\t\t\t))\n\t\t\t\t}\n\t\t\t\tdefer f.Close()\n\t\t\t}\n\t\t}\n\n\t\tif viper.GetString(\"log.output\") == \"stdout\" {\n\t\t\tgin.DefaultWriter = os.Stdout\n\t\t\tlog.SetOutput(os.Stdout)\n\t\t} else {\n\t\t\tf, _ := os.OpenFile(\n\t\t\t\tviper.GetString(\"log.output\"),\n\t\t\t\tos.O_APPEND|os.O_CREATE|os.O_WRONLY,\n\t\t\t\t0775,\n\t\t\t)\n\n\t\t\tgin.DefaultWriter = io.MultiWriter(f)\n\t\t\tlog.SetOutput(f)\n\t\t}\n\n\t\tlvl := strings.ToLower(viper.GetString(\"log.level\"))\n\t\tlevel, err := log.ParseLevel(lvl)\n\n\t\tif err != nil {\n\t\t\tlevel = log.InfoLevel\n\t\t}\n\n\t\tlog.SetLevel(level)\n\n\t\tif viper.GetString(\"app.mode\") == \"prod\" {\n\t\t\tgin.SetMode(gin.ReleaseMode)\n\t\t\tgin.DefaultWriter = ioutil.Discard\n\t\t\tgin.DisableConsoleColor()\n\t\t}\n\n\t\tif viper.GetString(\"log.format\") == \"json\" {\n\t\t\tlog.SetFormatter(&log.JSONFormatter{})\n\t\t} else {\n\t\t\tlog.SetFormatter(&log.TextFormatter{})\n\t\t}\n\n\t\t// Init DB Connection\n\t\tdb := module.Database{}\n\t\terr = db.AutoConnect()\n\n\t\tif err != nil {\n\t\t\tpanic(err.Error())\n\t\t}\n\n\t\t// Migrate Database\n\t\tsuccess := db.Migrate()\n\n\t\tif !success {\n\t\t\tpanic(\"Error! Unable to migrate database tables.\")\n\t\t}\n\n\t\tdefer db.Close()\n\n\t\tmessages := make(chan string, viper.GetInt(\"app.broker.native.capacity\"))\n\n\t\tr := gin.Default()\n\n\t\tr.Use(middleware.Correlation())\n\t\tr.Use(middleware.Auth())\n\t\tr.Use(middleware.Logger())\n\t\tr.Use(middleware.Metric())\n\n\t\tr.GET(\"/favicon.ico\", func(c *gin.Context) {\n\t\t\tc.String(http.StatusNoContent, \"\")\n\t\t})\n\n\t\tr.GET(\"/\", controller.HealthCheck)\n\t\tr.GET(\"/_health\", controller.HealthCheck)\n\t\tr.GET(\"/_ready\", controller.ReadyCheck)\n\t\tr.GET(viper.GetString(\"app.metrics.prometheus.endpoint\"), gin.WrapH(controller.Metrics()))\n\t\tr.GET(\"/api/v1/cluster\", controller.Clusters)\n\t\tr.GET(\"/api/v1/cluster/:cn\", controller.Cluster)\n\t\tr.GET(\"/api/v1/cluster/:cn/namespace\", controller.Namespaces)\n\t\tr.GET(\"/api/v1/cluster/:cn/namespace/:ns\", controller.Namespace)\n\t\tr.GET(\"/api/v1/cluster/:cn/namespace/:ns/app\", controller.Applications)\n\t\tr.GET(\"/api/v1/cluster/:cn/namespace/:ns/app/:id\", controller.Application)\n\t\tr.POST(\"/api/v1/cluster/:cn/namespace/:ns/app/:id/deployment\", func(c *gin.Context) {\n\t\t\tcontroller.CreateDeployment(c, messages)\n\t\t})\n\t\tr.GET(\"/api/v1/job\", controller.Jobs)\n\t\tr.GET(\"/api/v1/job/:uuid\", controller.GetJob)\n\t\tr.DELETE(\"/api/v1/job/:uuid\", controller.DeleteJob)\n\n\t\tfor i := 0; i < viper.GetInt(\"app.broker.native.workers\"); i++ {\n\t\t\tgo controller.Worker(i+1, messages)\n\t\t}\n\n\t\tgo controller.Daemon()\n\n\t\tvar runerr error\n\n\t\tif viper.GetBool(\"app.tls.status\") {\n\t\t\trunerr = r.RunTLS(\n\t\t\t\tfmt.Sprintf(\":%s\", strconv.Itoa(viper.GetInt(\"app.port\"))),\n\t\t\t\tviper.GetString(\"app.tls.pemPath\"),\n\t\t\t\tviper.GetString(\"app.tls.keyPath\"),\n\t\t\t)\n\t\t} else {\n\t\t\trunerr = r.Run(\n\t\t\t\tfmt.Sprintf(\":%s\", strconv.Itoa(viper.GetInt(\"app.port\"))),\n\t\t\t)\n\t\t}\n\n\t\tif runerr != nil {\n\t\t\tpanic(runerr.Error())\n\t\t}\n\n\t},\n}\n\nfunc init() {\n\tserveCmd.Flags().StringVarP(&config, \"config\", \"c\", \"config.prod.yml\", \"Absolute path to config file (required)\")\n\tserveCmd.MarkFlagRequired(\"config\")\n\trootCmd.AddCommand(serveCmd)\n}\n"
  },
  {
    "path": "core/cmd/version.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/clivern/beetle/core/module\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar (\n\t// Version buildinfo item\n\tVersion = \"dev\"\n\t// Commit buildinfo item\n\tCommit = \"none\"\n\t// Date buildinfo item\n\tDate = \"unknown\"\n\t// BuiltBy buildinfo item\n\tBuiltBy = \"unknown\"\n)\n\nvar versionCmd = &cobra.Command{\n\tUse:   \"version\",\n\tShort: \"Get current and latest version\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tfmt.Println(\n\t\t\tfmt.Sprintf(\n\t\t\t\t`Current Beetle Version %v Commit %v, Built @%v By %v.`,\n\t\t\t\tVersion,\n\t\t\t\tCommit,\n\t\t\t\tDate,\n\t\t\t\tBuiltBy,\n\t\t\t),\n\t\t)\n\n\t\tlatest, err := module.GetLatestRelease()\n\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"Error: %s \\n\", err.Error())\n\t\t\treturn\n\t\t}\n\n\t\tfmt.Printf(\n\t\t\t\"Latest release %s, Latest tag %s \\n\",\n\t\t\tlatest.Name,\n\t\t\tlatest.TagName,\n\t\t)\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(versionCmd)\n}\n"
  },
  {
    "path": "core/controller/application.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage controller\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"github.com/clivern/beetle/core/kubernetes\"\n\t\"github.com/clivern/beetle/core/model\"\n\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// Application controller\nfunc Application(c *gin.Context) {\n\tcn := c.Param(\"cn\")\n\tns := c.Param(\"ns\")\n\tid := c.Param(\"id\")\n\n\tconfig := model.Configs{}\n\n\tcluster, err := kubernetes.GetCluster(cn)\n\n\tif err != nil {\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\"cluster_name\":   cn,\n\t\t\t\"error\":          err.Error(),\n\t\t}).Info(`Cluster not found`)\n\n\t\tc.Status(http.StatusNotFound)\n\t\treturn\n\t}\n\n\tconfig, err = cluster.GetConfig(context.TODO(), ns)\n\n\tif err != nil {\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\"cluster_name\":   cn,\n\t\t\t\"namespace_name\": ns,\n\t\t\t\"error\":          err.Error(),\n\t\t}).Warn(`Error while fetching beetle configMap`)\n\t}\n\n\tfor _, app := range config.Applications {\n\t\tif app.ID == id {\n\t\t\tapplication, err := cluster.GetApplication(\n\t\t\t\tcontext.TODO(),\n\t\t\t\tns,\n\t\t\t\tapp.ID,\n\t\t\t\tapp.Name,\n\t\t\t\tapp.ImageFormat,\n\t\t\t)\n\n\t\t\tif err != nil {\n\t\t\t\tlog.WithFields(log.Fields{\n\t\t\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\t\t\"application_id\": id,\n\t\t\t\t\t\"cluster_name\":   cn,\n\t\t\t\t\t\"namespace_name\": ns,\n\t\t\t\t\t\"error\":          err.Error(),\n\t\t\t\t}).Warn(`Error while fetching application current version`)\n\t\t\t}\n\n\t\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\t\"id\":         application.ID,\n\t\t\t\t\"name\":       application.Name,\n\t\t\t\t\"format\":     application.Format,\n\t\t\t\t\"containers\": application.Containers,\n\t\t\t})\n\t\t\treturn\n\t\t}\n\t}\n\n\tlog.WithFields(log.Fields{\n\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\"application_id\": id,\n\t\t\"cluster_name\":   cn,\n\t\t\"namespace_name\": ns,\n\t}).Info(`Application not found`)\n\n\tc.Status(http.StatusNotFound)\n}\n"
  },
  {
    "path": "core/controller/applications.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage controller\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"github.com/clivern/beetle/core/kubernetes\"\n\t\"github.com/clivern/beetle/core/model\"\n\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// Applications controller\nfunc Applications(c *gin.Context) {\n\tcn := c.Param(\"cn\")\n\tns := c.Param(\"ns\")\n\tconfig := model.Configs{}\n\n\tcluster, err := kubernetes.GetCluster(cn)\n\n\tif err != nil {\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\"cluster_name\":   cn,\n\t\t\t\"error\":          err.Error(),\n\t\t}).Info(`Cluster not found`)\n\n\t\tc.Status(http.StatusNotFound)\n\t\treturn\n\t}\n\n\tconfig, err = cluster.GetConfig(context.TODO(), ns)\n\n\tif err != nil {\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\"cluster_name\":   cn,\n\t\t\t\"namespace_name\": ns,\n\t\t\t\"error\":          err.Error(),\n\t\t}).Warn(`Error while fetching beetle configMap`)\n\t}\n\n\tapplications := []model.Application{}\n\n\tfor _, app := range config.Applications {\n\t\tapplication, err := cluster.GetApplication(\n\t\t\tcontext.TODO(),\n\t\t\tns,\n\t\t\tapp.ID,\n\t\t\tapp.Name,\n\t\t\tapp.ImageFormat,\n\t\t)\n\n\t\tif err != nil {\n\t\t\tlog.WithFields(log.Fields{\n\t\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\t\"application_id\": app.ID,\n\t\t\t\t\"cluster_name\":   cn,\n\t\t\t\t\"namespace_name\": ns,\n\t\t\t\t\"error\":          err.Error(),\n\t\t\t}).Warn(`Error while fetching application current version`)\n\t\t\tcontinue\n\t\t}\n\n\t\tapplications = append(applications, application)\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"applications\": applications,\n\t})\n}\n"
  },
  {
    "path": "core/controller/cluster.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage controller\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"github.com/clivern/beetle/core/kubernetes\"\n\t\"github.com/clivern/beetle/core/model\"\n\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// Cluster controller\nfunc Cluster(c *gin.Context) {\n\tcn := c.Param(\"cn\")\n\tresult := model.Cluster{}\n\n\tcluster, err := kubernetes.GetCluster(cn)\n\n\tif err != nil {\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\"cluster_name\":   cn,\n\t\t\t\"error\":          err.Error(),\n\t\t}).Info(`Cluster not found`)\n\n\t\tc.Status(http.StatusNotFound)\n\t\treturn\n\t}\n\n\tstatus, err := cluster.Ping(context.TODO())\n\n\tif err != nil {\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\"cluster_name\":   cn,\n\t\t\t\"error\":          err.Error(),\n\t\t}).Error(`Error ping a cluster`)\n\t}\n\n\tresult.Name = cluster.Name\n\tresult.Health = status\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"name\":   result.Name,\n\t\t\"health\": result.Health,\n\t})\n}\n"
  },
  {
    "path": "core/controller/clusters.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage controller\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"github.com/clivern/beetle/core/kubernetes\"\n\t\"github.com/clivern/beetle/core/model\"\n\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// Clusters controller\nfunc Clusters(c *gin.Context) {\n\tresult := []model.Cluster{}\n\n\tclusters, err := kubernetes.GetClusters()\n\n\tif err != nil {\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\"error\":          err.Error(),\n\t\t}).Error(`Error fetching clusters`)\n\n\t\tc.Status(http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar status bool\n\n\tfor _, cluster := range clusters {\n\t\tstatus, err = cluster.Ping(context.TODO())\n\n\t\tif err != nil {\n\t\t\tlog.WithFields(log.Fields{\n\t\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\t\"cluster_name\":   cluster.Name,\n\t\t\t\t\"error\":          err.Error(),\n\t\t\t}).Error(`Error while ping a cluster`)\n\t\t}\n\n\t\tresult = append(result, model.Cluster{\n\t\t\tName:   cluster.Name,\n\t\t\tHealth: status,\n\t\t})\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"clusters\": result,\n\t})\n}\n"
  },
  {
    "path": "core/controller/daemon.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage controller\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/clivern/beetle/core/model\"\n\t\"github.com/clivern/beetle/core/module\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/viper\"\n)\n\nvar (\n\tpendingJobs = prometheus.NewGauge(\n\t\tprometheus.GaugeOpts{\n\t\t\tNamespace: \"beetle\",\n\t\t\tName:      \"workers_queue_pending_jobs\",\n\t\t\tHelp:      \"The pending jobs in the queue\",\n\t\t})\n\n\tfailedJobs = prometheus.NewGauge(\n\t\tprometheus.GaugeOpts{\n\t\t\tNamespace: \"beetle\",\n\t\t\tName:      \"workers_queue_failed_jobs\",\n\t\t\tHelp:      \"The failed jobs in the queue\",\n\t\t})\n\n\tsuccessJobs = prometheus.NewGauge(\n\t\tprometheus.GaugeOpts{\n\t\t\tNamespace: \"beetle\",\n\t\t\tName:      \"workers_queue_success_jobs\",\n\t\t\tHelp:      \"The successful jobs in the queue\",\n\t\t})\n\n\tonHoldJobs = prometheus.NewGauge(\n\t\tprometheus.GaugeOpts{\n\t\t\tNamespace: \"beetle\",\n\t\t\tName:      \"workers_queue_on_hold_jobs\",\n\t\t\tHelp:      \"The on hold jobs in the queue\",\n\t\t})\n)\n\nfunc init() {\n\tprometheus.MustRegister(pendingJobs)\n\tprometheus.MustRegister(failedJobs)\n\tprometheus.MustRegister(successJobs)\n\tprometheus.MustRegister(onHoldJobs)\n}\n\n// Daemon function\nfunc Daemon() {\n\tvar err error\n\tvar pendingJobsCount int\n\tvar failedJobsCount int\n\tvar successfulJobsCount int\n\tvar onHoldJobsCount int\n\tvar job model.Job\n\tvar parentJob model.Job\n\tvar deploymentRequest model.DeploymentRequest\n\tvar payload string\n\n\thttpClient := module.NewHTTPClient(20)\n\tdb := module.Database{}\n\n\tretry, err := strconv.Atoi(viper.GetString(\"app.webhook.retry\"))\n\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\n\tfor {\n\t\terr = db.AutoConnect()\n\n\t\tif err != nil {\n\t\t\tlog.WithFields(log.Fields{\n\t\t\t\t\"correlation_id\": \"\",\n\t\t\t\t\"error\":          err.Error(),\n\t\t\t}).Error(`Failure while connecting database`)\n\t\t\ttime.Sleep(2 * time.Second)\n\t\t\tcontinue\n\t\t}\n\t\t// Update Metrics\n\t\tpendingJobsCount = db.CountJobs(model.JobPending)\n\t\tfailedJobsCount = db.CountJobs(model.JobFailed)\n\t\tsuccessfulJobsCount = db.CountJobs(model.JobSuccess)\n\t\tonHoldJobsCount = db.CountJobs(model.JobOnHold)\n\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\":        \"\",\n\t\t\t\"pending_jobs_count\":    pendingJobsCount,\n\t\t\t\"failed_jobs_count\":     failedJobsCount,\n\t\t\t\"successful_jobs_count\": successfulJobsCount,\n\t\t\t\"on_hold_jobs_count\":    onHoldJobsCount,\n\t\t}).Debug(`Update metrics`)\n\n\t\tpendingJobs.Set(float64(pendingJobsCount))\n\t\tfailedJobs.Set(float64(failedJobsCount))\n\t\tsuccessJobs.Set(float64(successfulJobsCount))\n\t\tonHoldJobs.Set(float64(onHoldJobsCount))\n\n\t\t// Run Pending Jobs (HTTP Notification)\n\t\tjob = db.GetPendingJobByType(model.JobDeploymentNotify)\n\n\t\tif job.ID > 0 {\n\t\t\tif job.Retry > retry {\n\t\t\t\tnow := time.Now()\n\t\t\t\tjob.Status = model.JobFailed\n\t\t\t\tjob.RunAt = &now\n\t\t\t\tjob.Result = fmt.Sprintf(\"Failed to deliver the notification\")\n\t\t\t\tdb.UpdateJobByID(&job)\n\t\t\t} else {\n\t\t\t\tdeploymentRequest.LoadFromJSON([]byte(job.Payload))\n\n\t\t\t\tif job.Parent > 0 {\n\t\t\t\t\tparentJob = db.GetJobByID(job.Parent)\n\n\t\t\t\t\tif parentJob.ID > 0 {\n\t\t\t\t\t\tdeploymentRequest.Status = parentJob.Status\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tpayload, _ = deploymentRequest.ConvertToJSON()\n\n\t\t\t\tresponse, err := httpClient.Post(\n\t\t\t\t\tcontext.TODO(),\n\t\t\t\t\tviper.GetString(\"app.webhook.url\"),\n\t\t\t\t\tpayload,\n\t\t\t\t\tmap[string]string{},\n\t\t\t\t\tmap[string]string{\n\t\t\t\t\t\t\"Content-Type\":      \"application/json\",\n\t\t\t\t\t\t\"X-API-KEY\":         viper.GetString(\"app.webhook.apiKey\"),\n\t\t\t\t\t\t\"X-NOTIFICATION-ID\": job.UUID,\n\t\t\t\t\t\t\"X-ACTION-NAME\":     job.Type,\n\t\t\t\t\t\t\"X-DEPLOYMENT-ID\":   parentJob.UUID,\n\t\t\t\t\t},\n\t\t\t\t)\n\n\t\t\t\tif httpClient.GetStatusCode(response) != http.StatusOK || err != nil {\n\t\t\t\t\tjob.Status = model.JobFailed\n\t\t\t\t\tjob.Result = fmt.Sprintf(\"Failed to deliver the notification\")\n\t\t\t\t} else {\n\t\t\t\t\tjob.Status = model.JobSuccess\n\t\t\t\t\tjob.Result = fmt.Sprintf(\"Notification delivered successfully\")\n\t\t\t\t}\n\n\t\t\t\tif job.Status == model.JobFailed && job.Retry <= retry {\n\t\t\t\t\tjob.Status = model.JobPending\n\t\t\t\t}\n\n\t\t\t\tnow := time.Now()\n\t\t\t\tjob.Retry++\n\t\t\t\tjob.RunAt = &now\n\t\t\t\tdb.UpdateJobByID(&job)\n\t\t\t}\n\t\t}\n\n\t\ttime.Sleep(2 * time.Second)\n\t}\n}\n"
  },
  {
    "path": "core/controller/deployment.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage controller\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/clivern/beetle/core/model\"\n\t\"github.com/clivern/beetle/core/module\"\n\t\"github.com/clivern/beetle/core/util\"\n\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// CreateDeployment controller\nfunc CreateDeployment(c *gin.Context, messages chan<- string) {\n\trawBody, _ := c.GetRawData()\n\n\tdeploymentRequest := model.DeploymentRequest{}\n\n\t_, err := deploymentRequest.LoadFromJSON(rawBody)\n\n\tdeploymentRequest.Cluster = c.Param(\"cn\")\n\tdeploymentRequest.Namespace = c.Param(\"ns\")\n\tdeploymentRequest.Application = c.Param(\"id\")\n\n\tif err != nil {\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\"error\":          err.Error(),\n\t\t}).Info(`Invalid request`)\n\n\t\tc.JSON(http.StatusBadRequest, gin.H{\n\t\t\t\"error\": \"Invalid request!\",\n\t\t})\n\t\treturn\n\t}\n\n\terr = deploymentRequest.Validate([]string{\n\t\tmodel.RecreateStrategy,\n\t\tmodel.RampedStrategy,\n\t\tmodel.CanaryStrategy,\n\t\tmodel.BlueGreenStrategy,\n\t})\n\n\tif err != nil {\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\"error\":          err.Error(),\n\t\t}).Info(`Invalid request`)\n\n\t\tc.JSON(http.StatusBadRequest, gin.H{\n\t\t\t\"error\": err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tif deploymentRequest.MaxSurge == \"\" {\n\t\tdeploymentRequest.MaxSurge = \"25%\"\n\t}\n\n\tif deploymentRequest.MaxUnavailable == \"\" {\n\t\tdeploymentRequest.MaxUnavailable = \"25%\"\n\t}\n\n\tresult, err := deploymentRequest.ConvertToJSON()\n\n\tif err != nil {\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\"error\":          err.Error(),\n\t\t}).Info(`Invalid request`)\n\n\t\tc.JSON(http.StatusBadRequest, gin.H{\n\t\t\t\"error\": err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\t// Then create async job\n\tdb := module.Database{}\n\terr = db.AutoConnect()\n\n\tif err != nil {\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\"error\":          err.Error(),\n\t\t}).Error(`Failure while connecting database`)\n\n\t\tc.Status(http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdefer db.Close()\n\n\tuuid := util.GenerateUUID4()\n\n\tfor db.JobExistByUUID(uuid) {\n\t\tuuid = util.GenerateUUID4()\n\t}\n\n\tjob := db.CreateJob(&model.Job{\n\t\tUUID:    uuid,\n\t\tPayload: result,\n\t\tStatus:  model.JobPending,\n\t\tParent:  0,\n\t\tType:    model.JobDeploymentUpdate,\n\t})\n\n\tmessageObj := model.Message{\n\t\tUUID: c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\tJob:  job.ID,\n\t}\n\n\tmessage, _ := messageObj.ConvertToJSON()\n\n\t// Send the job to workers\n\tmessages <- message\n\n\tc.JSON(http.StatusAccepted, gin.H{\n\t\t\"id\":        job.ID,\n\t\t\"uuid\":      job.UUID,\n\t\t\"type\":      job.Type,\n\t\t\"status\":    job.Status,\n\t\t\"createdAt\": job.CreatedAt,\n\t})\n}\n"
  },
  {
    "path": "core/controller/health_check.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage controller\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// HealthCheck controller\nfunc HealthCheck(c *gin.Context) {\n\tstatus := \"ok\"\n\n\tlog.WithFields(log.Fields{\n\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\"status\":         status,\n\t}).Info(`Health check`)\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": status,\n\t})\n}\n"
  },
  {
    "path": "core/controller/health_check_test.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage controller\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/clivern/beetle/core/module\"\n\t\"github.com/clivern/beetle/pkg\"\n\n\t\"github.com/drone/envsubst\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/spf13/viper\"\n)\n\n// TestHealthCheck test cases\nfunc TestHealthCheck(t *testing.T) {\n\ttestingConfig := \"config.testing.yml\"\n\n\t// LoadConfigFile\n\tt.Run(\"LoadConfigFile\", func(t *testing.T) {\n\t\tfs := module.FileSystem{}\n\n\t\tdir, _ := os.Getwd()\n\t\tconfigFile := fmt.Sprintf(\"%s/%s\", dir, testingConfig)\n\n\t\tfor {\n\t\t\tif fs.FileExists(configFile) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tdir = filepath.Dir(dir)\n\t\t\tconfigFile = fmt.Sprintf(\"%s/%s\", dir, testingConfig)\n\t\t}\n\n\t\tt.Logf(\"Load Config File %s\", configFile)\n\n\t\tconfigUnparsed, _ := ioutil.ReadFile(configFile)\n\t\tconfigParsed, _ := envsubst.EvalEnv(string(configUnparsed))\n\t\tviper.SetConfigType(\"yaml\")\n\t\tviper.ReadConfig(bytes.NewBuffer([]byte(configParsed)))\n\t})\n\n\t// TestHealthCheckController\n\tt.Run(\"TestHealthCheckController\", func(t *testing.T) {\n\t\tgin.SetMode(gin.ReleaseMode)\n\t\tgin.DefaultWriter = ioutil.Discard\n\t\tgin.DisableConsoleColor()\n\n\t\trouter := gin.Default()\n\n\t\trouter.GET(\"/_health\", HealthCheck)\n\n\t\tw := httptest.NewRecorder()\n\t\treq, _ := http.NewRequest(\"GET\", \"/_health\", nil)\n\t\trouter.ServeHTTP(w, req)\n\n\t\tpkg.Expect(t, viper.GetString(\"app.mode\"), \"test\")\n\t\tpkg.Expect(t, w.Code, 200)\n\t\tpkg.Expect(t, strings.TrimSpace(w.Body.String()), `{\"status\":\"ok\"}`)\n\t})\n}\n"
  },
  {
    "path": "core/controller/job.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage controller\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/clivern/beetle/core/module\"\n\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// GetJob controller\nfunc GetJob(c *gin.Context) {\n\tuuid := c.Param(\"uuid\")\n\n\tdb := module.Database{}\n\n\terr := db.AutoConnect()\n\n\tif err != nil {\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\"error\":          err.Error(),\n\t\t}).Error(`Failure while connecting database`)\n\n\t\tc.Status(http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdefer db.Close()\n\n\tjob := db.GetJobByUUID(uuid)\n\n\tif job.ID < 1 {\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\"job_uuid\":       uuid,\n\t\t}).Info(fmt.Sprintf(`Job not found`))\n\n\t\tc.Status(http.StatusNotFound)\n\t\treturn\n\t}\n\n\tlog.WithFields(log.Fields{\n\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\"job_uuid\":       uuid,\n\t}).Info(`Retrieve a job`)\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"id\":        job.ID,\n\t\t\"uuid\":      job.UUID,\n\t\t\"status\":    job.Status,\n\t\t\"type\":      job.Type,\n\t\t\"runAt\":     job.RunAt,\n\t\t\"createdAt\": job.CreatedAt,\n\t\t\"updatedAt\": job.UpdatedAt,\n\t})\n}\n\n// DeleteJob controller\nfunc DeleteJob(c *gin.Context) {\n\tuuid := c.Param(\"uuid\")\n\n\tdb := module.Database{}\n\n\terr := db.AutoConnect()\n\n\tif err != nil {\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\"error\":          err.Error(),\n\t\t}).Error(`Failure while connecting database`)\n\n\t\tc.Status(http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdefer db.Close()\n\n\tjob := db.GetJobByUUID(uuid)\n\n\tif job.ID < 1 {\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\"job_uuid\":       uuid,\n\t\t}).Info(`Job not found`)\n\n\t\tc.Status(http.StatusNotFound)\n\t\treturn\n\t}\n\n\tlog.WithFields(log.Fields{\n\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\"job_uuid\":       uuid,\n\t}).Info(`Deleting a job`)\n\n\tdb.DeleteJobByID(job.ID)\n\n\tc.Status(http.StatusNoContent)\n\treturn\n}\n"
  },
  {
    "path": "core/controller/jobs.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage controller\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/clivern/beetle/core/module\"\n\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// Jobs controller\nfunc Jobs(c *gin.Context) {\n\tdb := module.Database{}\n\n\terr := db.AutoConnect()\n\n\tif err != nil {\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\"error\":          err.Error(),\n\t\t}).Error(`Failure while connecting database`)\n\n\t\tc.Status(http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdefer db.Close()\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"jobs\": db.GetJobs(),\n\t})\n}\n"
  },
  {
    "path": "core/controller/metrics.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage controller\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n\t\"github.com/spf13/viper\"\n)\n\nvar (\n\tworkersCount = prometheus.NewGauge(\n\t\tprometheus.GaugeOpts{\n\t\t\tNamespace: \"beetle\",\n\t\t\tName:      \"workers_count\",\n\t\t\tHelp:      \"Number of Async Workers\",\n\t\t})\n\n\tqueueCapacity = prometheus.NewGauge(\n\t\tprometheus.GaugeOpts{\n\t\t\tNamespace: \"beetle\",\n\t\t\tName:      \"workers_queue_capacity\",\n\t\t\tHelp:      \"The maximum number of messages queue can process\",\n\t\t})\n)\n\nfunc init() {\n\tprometheus.MustRegister(workersCount)\n\tprometheus.MustRegister(queueCapacity)\n}\n\n// Metrics controller\nfunc Metrics() http.Handler {\n\tworkersCount.Set(float64(viper.GetInt(\"app.broker.native.workers\")))\n\tqueueCapacity.Set(float64(viper.GetInt(\"app.broker.native.capacity\")))\n\n\treturn promhttp.Handler()\n}\n"
  },
  {
    "path": "core/controller/namespace.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage controller\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"github.com/clivern/beetle/core/kubernetes\"\n\t\"github.com/clivern/beetle/core/model\"\n\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// Namespace controller\nfunc Namespace(c *gin.Context) {\n\tcn := c.Param(\"cn\")\n\tns := c.Param(\"ns\")\n\n\tresult := model.Namespace{}\n\n\tclusters, err := kubernetes.GetClusters()\n\n\tif err != nil {\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\"error\":          err.Error(),\n\t\t}).Error(`Failure to get clusters`)\n\n\t\tc.Status(http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tfor _, cluster := range clusters {\n\t\tif cn != cluster.Name {\n\t\t\tcontinue\n\t\t}\n\n\t\tresult, err = cluster.GetNamespace(context.TODO(), ns)\n\n\t\tif err != nil {\n\t\t\tlog.WithFields(log.Fields{\n\t\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\t\"namespace_name\": ns,\n\t\t\t\t\"cluster_name\":   cn,\n\t\t\t\t\"error\":          err.Error(),\n\t\t\t}).Error(`Failure to get cluster namespace`)\n\t\t}\n\t}\n\n\tif result.Name == \"\" {\n\t\tc.Status(http.StatusNotFound)\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"name\":   result.Name,\n\t\t\"uid\":    result.UID,\n\t\t\"status\": result.Status,\n\t})\n}\n"
  },
  {
    "path": "core/controller/namespaces.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage controller\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"github.com/clivern/beetle/core/kubernetes\"\n\t\"github.com/clivern/beetle/core/model\"\n\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// Namespaces controller\nfunc Namespaces(c *gin.Context) {\n\tcn := c.Param(\"cn\")\n\tresult := []model.Namespace{}\n\n\tclusters, err := kubernetes.GetClusters()\n\n\tif err != nil {\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\"error\":          err.Error(),\n\t\t}).Error(`Failure to get clusters`)\n\n\t\tc.Status(http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tfor _, cluster := range clusters {\n\t\tif cn != cluster.Name {\n\t\t\tcontinue\n\t\t}\n\n\t\tresult, err = cluster.GetNamespaces(context.TODO())\n\n\t\tif err != nil {\n\t\t\tlog.WithFields(log.Fields{\n\t\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\t\"error\":          err.Error(),\n\t\t\t\t\"cluster_name\":   cn,\n\t\t\t}).Error(`Failure to get cluster namespaces`)\n\t\t}\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"namespaces\": result,\n\t})\n}\n"
  },
  {
    "path": "core/controller/ready_check.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage controller\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/clivern/beetle/core/module\"\n\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// ReadyCheck controller\nfunc ReadyCheck(c *gin.Context) {\n\tstatus := \"ok\"\n\n\tdb := module.Database{}\n\n\terr := db.AutoConnect()\n\n\tif err != nil {\n\t\tstatus = \"down\"\n\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\"status\":         status,\n\t\t\t\"error\":          err.Error(),\n\t\t}).Error(`Failed ready check`)\n\n\t\tc.Status(http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\terr = db.Ping()\n\n\tif err != nil {\n\t\tstatus = \"down\"\n\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\"status\":         status,\n\t\t\t\"error\":          err.Error(),\n\t\t}).Error(`Failed ready check`)\n\n\t\tc.Status(http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdefer db.Close()\n\n\tlog.WithFields(log.Fields{\n\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\"status\":         status,\n\t}).Info(`Passed ready check`)\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": status,\n\t})\n}\n"
  },
  {
    "path": "core/controller/ready_check_test.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage controller\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/clivern/beetle/core/module\"\n\t\"github.com/clivern/beetle/pkg\"\n\n\t\"github.com/drone/envsubst\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/spf13/viper\"\n)\n\n// TestReadyCheck test cases\nfunc TestReadyCheck(t *testing.T) {\n\ttestingConfig := \"config.testing.yml\"\n\n\t// LoadConfigFile\n\tt.Run(\"LoadConfigFile\", func(t *testing.T) {\n\t\tfs := module.FileSystem{}\n\n\t\tdir, _ := os.Getwd()\n\t\tconfigFile := fmt.Sprintf(\"%s/%s\", dir, testingConfig)\n\n\t\tfor {\n\t\t\tif fs.FileExists(configFile) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tdir = filepath.Dir(dir)\n\t\t\tconfigFile = fmt.Sprintf(\"%s/%s\", dir, testingConfig)\n\t\t}\n\n\t\tt.Logf(\"Load Config File %s\", configFile)\n\n\t\tconfigUnparsed, _ := ioutil.ReadFile(configFile)\n\t\tconfigParsed, _ := envsubst.EvalEnv(string(configUnparsed))\n\t\tviper.SetConfigType(\"yaml\")\n\t\tviper.ReadConfig(bytes.NewBuffer([]byte(configParsed)))\n\t})\n\n\t// TestReadyCheckController\n\tt.Run(\"TestReadyCheckController\", func(t *testing.T) {\n\t\tgin.SetMode(gin.ReleaseMode)\n\t\tgin.DefaultWriter = ioutil.Discard\n\t\tgin.DisableConsoleColor()\n\n\t\trouter := gin.Default()\n\n\t\trouter.GET(\"/_ready\", ReadyCheck)\n\n\t\tw := httptest.NewRecorder()\n\t\treq, _ := http.NewRequest(\"GET\", \"/_ready\", nil)\n\t\trouter.ServeHTTP(w, req)\n\n\t\tpkg.Expect(t, viper.GetString(\"app.mode\"), \"test\")\n\t\tpkg.Expect(t, w.Code, 200)\n\t\tpkg.Expect(t, strings.TrimSpace(w.Body.String()), `{\"status\":\"ok\"}`)\n\t})\n}\n"
  },
  {
    "path": "core/controller/worker.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage controller\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/clivern/beetle/core/kubernetes\"\n\t\"github.com/clivern/beetle/core/model\"\n\t\"github.com/clivern/beetle/core/module\"\n\t\"github.com/clivern/beetle/core/util\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/viper\"\n)\n\n// Worker controller\nfunc Worker(workerID int, messages <-chan string) {\n\tvar ok bool\n\tvar err error\n\tvar job model.Job\n\tvar cluster *kubernetes.Cluster\n\tvar uuid string\n\n\tmessageObj := model.Message{}\n\tdeploymentRequest := model.DeploymentRequest{}\n\n\tlog.WithFields(log.Fields{\n\t\t\"correlation_id\": util.GenerateUUID4(),\n\t\t\"worker_id\":      workerID,\n\t}).Info(`Worker started`)\n\n\tdb := module.Database{}\n\n\tfor message := range messages {\n\t\tok, err = messageObj.LoadFromJSON([]byte(message))\n\n\t\tif !ok || err != nil {\n\t\t\tlog.WithFields(log.Fields{\n\t\t\t\t\"correlation_id\": messageObj.UUID,\n\t\t\t\t\"worker_id\":      workerID,\n\t\t\t\t\"message\":        message,\n\t\t\t}).Warn(`Worker received invalid message`)\n\t\t\tcontinue\n\t\t}\n\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\": messageObj.UUID,\n\t\t\t\"worker_id\":      workerID,\n\t\t\t\"job_id\":         messageObj.Job,\n\t\t}).Info(`Worker received a new job`)\n\n\t\terr = db.AutoConnect()\n\n\t\tif err != nil {\n\t\t\tlog.WithFields(log.Fields{\n\t\t\t\t\"correlation_id\": messageObj.UUID,\n\t\t\t\t\"worker_id\":      workerID,\n\t\t\t\t\"error\":          err.Error(),\n\t\t\t}).Error(`Worker unable to connect to database`)\n\t\t\tcontinue\n\t\t}\n\n\t\tjob = db.GetJobByID(messageObj.Job)\n\n\t\tok, err = deploymentRequest.LoadFromJSON([]byte(job.Payload))\n\n\t\tif !ok || err != nil {\n\t\t\tlog.WithFields(log.Fields{\n\t\t\t\t\"correlation_id\": messageObj.UUID,\n\t\t\t\t\"worker_id\":      workerID,\n\t\t\t\t\"job_id\":         messageObj.Job,\n\t\t\t\t\"job_uuid\":       job.UUID,\n\t\t\t\t\"error\":          err.Error(),\n\t\t\t}).Error(`Invalid job payload`)\n\n\t\t\t// Job Failed\n\t\t\tnow := time.Now()\n\t\t\tjob.Status = model.JobFailed\n\t\t\tjob.RunAt = &now\n\t\t\tjob.Result = fmt.Sprintf(\"Invalid job payload, UUID %s\", messageObj.UUID)\n\t\t\tdb.UpdateJobByID(&job)\n\t\t\tdb.ReleaseChildJobs(job.ID)\n\t\t\tcontinue\n\t\t}\n\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\":     messageObj.UUID,\n\t\t\t\"worker_id\":          workerID,\n\t\t\t\"job_id\":             messageObj.Job,\n\t\t\t\"job_uuid\":           job.UUID,\n\t\t\t\"deployment_request\": deploymentRequest,\n\t\t}).Info(`Worker accepted deployment request`)\n\n\t\t// Notify if there is a webhook\n\t\tif strings.TrimSpace(viper.GetString(\"app.webhook.url\")) != \"\" {\n\t\t\tuuid = util.GenerateUUID4()\n\n\t\t\tfor db.JobExistByUUID(uuid) {\n\t\t\t\tuuid = util.GenerateUUID4()\n\t\t\t}\n\n\t\t\tdb.CreateJob(&model.Job{\n\t\t\t\tUUID:    uuid,\n\t\t\t\tPayload: job.Payload,\n\t\t\t\tStatus:  model.JobOnHold,\n\t\t\t\tParent:  messageObj.Job,\n\t\t\t\tType:    model.JobDeploymentNotify,\n\t\t\t})\n\n\t\t\tlog.WithFields(log.Fields{\n\t\t\t\t\"correlation_id\":     messageObj.UUID,\n\t\t\t\t\"worker_id\":          workerID,\n\t\t\t\t\"job_id\":             messageObj.Job,\n\t\t\t\t\"job_uuid\":           job.UUID,\n\t\t\t\t\"deployment_request\": deploymentRequest,\n\t\t\t\t\"webhook_url\":        viper.GetString(\"app.webhook.url\"),\n\t\t\t}).Info(`HTTP webhook enabled`)\n\t\t} else {\n\t\t\tlog.WithFields(log.Fields{\n\t\t\t\t\"correlation_id\":     messageObj.UUID,\n\t\t\t\t\"worker_id\":          workerID,\n\t\t\t\t\"job_id\":             messageObj.Job,\n\t\t\t\t\"job_uuid\":           job.UUID,\n\t\t\t\t\"deployment_request\": deploymentRequest,\n\t\t\t}).Info(`HTTP webhook disabled`)\n\t\t}\n\n\t\tcluster, err = kubernetes.GetCluster(deploymentRequest.Cluster)\n\n\t\tif err != nil {\n\t\t\tlog.WithFields(log.Fields{\n\t\t\t\t\"correlation_id\":     messageObj.UUID,\n\t\t\t\t\"worker_id\":          workerID,\n\t\t\t\t\"error\":              err.Error(),\n\t\t\t\t\"deployment_request\": deploymentRequest,\n\t\t\t}).Error(`Worker can not find the cluster`)\n\n\t\t\t// Job Failed\n\t\t\tnow := time.Now()\n\t\t\tjob.Status = model.JobFailed\n\t\t\tjob.RunAt = &now\n\t\t\tjob.Result = fmt.Sprintf(\"Worker can not find the cluster, UUID %s\", messageObj.UUID)\n\t\t\tdb.UpdateJobByID(&job)\n\t\t\tdb.ReleaseChildJobs(job.ID)\n\t\t\tcontinue\n\t\t}\n\n\t\tok, err = cluster.Ping(context.TODO())\n\n\t\tif !ok || err != nil {\n\t\t\tlog.WithFields(log.Fields{\n\t\t\t\t\"correlation_id\":     messageObj.UUID,\n\t\t\t\t\"worker_id\":          workerID,\n\t\t\t\t\"error\":              err.Error(),\n\t\t\t\t\"deployment_request\": deploymentRequest,\n\t\t\t}).Error(`Worker unable to ping cluster`)\n\n\t\t\t// Job Failed\n\t\t\tnow := time.Now()\n\t\t\tjob.Status = model.JobFailed\n\t\t\tjob.RunAt = &now\n\t\t\tjob.Result = fmt.Sprintf(\"Worker unable to ping cluster, UUID %s\", messageObj.UUID)\n\t\t\tdb.UpdateJobByID(&job)\n\t\t\tdb.ReleaseChildJobs(job.ID)\n\t\t\tcontinue\n\t\t}\n\n\t\tok, err = cluster.Deploy(deploymentRequest)\n\n\t\tif !ok || err != nil {\n\t\t\tlog.WithFields(log.Fields{\n\t\t\t\t\"correlation_id\":     messageObj.UUID,\n\t\t\t\t\"worker_id\":          workerID,\n\t\t\t\t\"error\":              err.Error(),\n\t\t\t\t\"deployment_request\": deploymentRequest,\n\t\t\t}).Error(`Worker unable deploy`)\n\n\t\t\t// Job Failed\n\t\t\tnow := time.Now()\n\t\t\tjob.Status = model.JobFailed\n\t\t\tjob.RunAt = &now\n\t\t\tjob.Result = fmt.Sprintf(\"Failure during deployment, UUID %s\", messageObj.UUID)\n\t\t\tdb.UpdateJobByID(&job)\n\t\t\tdb.ReleaseChildJobs(job.ID)\n\t\t\tcontinue\n\t\t}\n\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\":     messageObj.UUID,\n\t\t\t\"worker_id\":          workerID,\n\t\t\t\"deployment_request\": deploymentRequest,\n\t\t}).Info(`Deployment finished successfully`)\n\n\t\t// Job Succeeded\n\t\tnow := time.Now()\n\t\tjob.Status = model.JobSuccess\n\t\tjob.RunAt = &now\n\t\tjob.Result = \"Deployment finished successfully\"\n\t\tdb.UpdateJobByID(&job)\n\t\tdb.ReleaseChildJobs(job.ID)\n\t}\n}\n"
  },
  {
    "path": "core/kubernetes/application.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage kubernetes\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/clivern/beetle/core/model\"\n\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\n// GetApplication gets current application version\nfunc (c *Cluster) GetApplication(ctx context.Context, namespace, id, name, format string) (model.Application, error) {\n\tresult := model.Application{}\n\n\terr := c.Config()\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tdata, err := c.ClientSet.AppsV1().Deployments(namespace).List(ctx, metav1.ListOptions{\n\t\tLabelSelector: fmt.Sprintf(\n\t\t\t\"%s=%s,%s=%s\",\n\t\t\t\"beetle.clivern.com/status\",\n\t\t\t\"enabled\",\n\t\t\t\"beetle.clivern.com/application-id\",\n\t\t\tid,\n\t\t),\n\t})\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tresult.ID = id\n\tresult.Name = name\n\tresult.Format = format\n\tresult.Containers = []model.Container{}\n\n\tfor _, deployment := range data.Items {\n\t\tfor _, container := range deployment.Spec.Template.Spec.Containers {\n\t\t\tresult.Containers = append(result.Containers, model.Container{\n\t\t\t\tName:  container.Name,\n\t\t\t\tImage: container.Image,\n\t\t\t\tVersion: strings.Replace(\n\t\t\t\t\tcontainer.Image,\n\t\t\t\t\tstrings.Replace(format, \"[.Release]\", \"\", -1),\n\t\t\t\t\t\"\",\n\t\t\t\t\t-1,\n\t\t\t\t),\n\t\t\t\tDeployment: model.Deployment{\n\t\t\t\t\tName: deployment.ObjectMeta.Name,\n\t\t\t\t\tUID:  string(deployment.ObjectMeta.UID),\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "core/kubernetes/cluster.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage kubernetes\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/clivern/beetle/core/module\"\n\n\t\"github.com/spf13/viper\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/client-go/kubernetes\"\n\t\"k8s.io/client-go/kubernetes/fake\"\n\t\"k8s.io/client-go/rest\"\n\t\"k8s.io/client-go/tools/clientcmd\"\n)\n\n// Clusters struct\ntype Clusters struct {\n\tClusters []*Cluster `mapstructure:\",clusters\"`\n}\n\n// Cluster struct\ntype Cluster struct {\n\tName       string `mapstructure:\",name\"`\n\tKubeconfig string `mapstructure:\",kubeconfig\"`\n\tInCluster  bool   `mapstructure:\",inCluster\"`\n\tClientSet  kubernetes.Interface\n\tFake       bool\n}\n\n// GetClusters get a list of clusters\nfunc GetClusters() ([]*Cluster, error) {\n\tvar clusters Clusters\n\n\terr := viper.UnmarshalKey(\"app\", &clusters)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn clusters.Clusters, nil\n}\n\n// GetCluster get a list of clusters\nfunc GetCluster(name string) (*Cluster, error) {\n\tvar clusters Clusters\n\n\terr := viper.UnmarshalKey(\"app\", &clusters)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, cluster := range clusters.Clusters {\n\t\tif name == cluster.Name {\n\t\t\treturn cluster, nil\n\t\t}\n\t}\n\n\treturn &Cluster{}, fmt.Errorf(\"Unable to find cluster %s\", name)\n}\n\n// Override overrides the client set for testing\nfunc (c *Cluster) Override(objects ...runtime.Object) {\n\tc.Fake = true\n\tc.ClientSet = fake.NewSimpleClientset(objects...)\n}\n\n// Config configs the client set for testing\nfunc (c *Cluster) Config() error {\n\tif c.Fake {\n\t\treturn nil\n\t}\n\n\tvar config *rest.Config\n\tvar err error\n\n\tif !c.InCluster {\n\t\tfs := module.FileSystem{}\n\n\t\tif !fs.FileExists(c.Kubeconfig) {\n\t\t\treturn fmt.Errorf(\n\t\t\t\t\"cluster [%s] config file [%s] not exist\",\n\t\t\t\tc.Name,\n\t\t\t\tc.Kubeconfig,\n\t\t\t)\n\t\t}\n\n\t\tconfig, err = clientcmd.BuildConfigFromFlags(\"\", c.Kubeconfig)\n\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\tconfig, err = rest.InClusterConfig()\n\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tclientset, err := kubernetes.NewForConfig(config)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.ClientSet = clientset\n\n\treturn nil\n}\n\n// Ping check the cluster\nfunc (c *Cluster) Ping(ctx context.Context) (bool, error) {\n\terr := c.Config()\n\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tdata, err := c.ClientSet.CoreV1().RESTClient().Get().AbsPath(\"/api/v1\").DoRaw(ctx)\n\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\treturn (string(data) != \"\"), nil\n}\n"
  },
  {
    "path": "core/kubernetes/cluster_test.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage kubernetes\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/clivern/beetle/core/module\"\n\t\"github.com/clivern/beetle/pkg\"\n\n\t\"github.com/drone/envsubst\"\n\t\"github.com/spf13/viper\"\n)\n\n// TestCluster test cases\nfunc TestCluster(t *testing.T) {\n\ttestingConfig := \"config.testing.yml\"\n\n\t// LoadConfigFile\n\tt.Run(\"LoadConfigFile\", func(t *testing.T) {\n\t\tfs := module.FileSystem{}\n\n\t\tdir, _ := os.Getwd()\n\t\tconfigFile := fmt.Sprintf(\"%s/%s\", dir, testingConfig)\n\n\t\tfor {\n\t\t\tif fs.FileExists(configFile) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tdir = filepath.Dir(dir)\n\t\t\tconfigFile = fmt.Sprintf(\"%s/%s\", dir, testingConfig)\n\t\t}\n\n\t\tt.Logf(\"Load Config File %s\", configFile)\n\n\t\tconfigUnparsed, _ := ioutil.ReadFile(configFile)\n\t\tconfigParsed, _ := envsubst.EvalEnv(string(configUnparsed))\n\t\tviper.SetConfigType(\"yaml\")\n\t\tviper.ReadConfig(bytes.NewBuffer([]byte(configParsed)))\n\t})\n\n\t// TestGetClusters\n\tt.Run(\"TestGetClusters\", func(t *testing.T) {\n\t\tclusters, err := GetClusters()\n\n\t\tpkg.Expect(t, nil, err)\n\t\tpkg.Expect(t, clusters[0].Name, \"production\")\n\t\tpkg.Expect(t, clusters[0].Kubeconfig, \"/app/configs/production-cluster-kubeconfig.yaml\")\n\n\t\tpkg.Expect(t, clusters[1].Name, \"staging\")\n\t\tpkg.Expect(t, clusters[1].Kubeconfig, \"/app/configs/staging-cluster-kubeconfig.yaml\")\n\t})\n\n\t// TestGetCluster\n\tt.Run(\"TestGetCluster\", func(t *testing.T) {\n\t\tcluster, err := GetCluster(\"production\")\n\n\t\tpkg.Expect(t, nil, err)\n\t\tpkg.Expect(t, cluster.Name, \"production\")\n\t\tpkg.Expect(t, cluster.Kubeconfig, \"/app/configs/production-cluster-kubeconfig.yaml\")\n\n\t\tcluster, err = GetCluster(\"not-found\")\n\n\t\tpkg.Expect(t, fmt.Errorf(\"Unable to find cluster not-found\"), err)\n\t\tpkg.Expect(t, cluster.Name, \"\")\n\t\tpkg.Expect(t, cluster.Kubeconfig, \"\")\n\t})\n}\n"
  },
  {
    "path": "core/kubernetes/config.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage kubernetes\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/clivern/beetle/core/model\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\n// GetConfig gets a beetle configs for a specific namespace\nfunc (c *Cluster) GetConfig(ctx context.Context, namespace string) (model.Configs, error) {\n\tresult := model.Configs{}\n\n\terr := c.Config()\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tdata, err := c.ClientSet.AppsV1().Deployments(namespace).List(ctx, metav1.ListOptions{\n\t\tLabelSelector: fmt.Sprintf(\n\t\t\t\"%s=%s\",\n\t\t\t\"beetle.clivern.com/status\",\n\t\t\t\"enabled\",\n\t\t),\n\t})\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tfor _, deployment := range data.Items {\n\t\tapplicationName := \"\"\n\t\timageFormat := \"\"\n\t\tapplicationID := \"\"\n\t\tstatus := \"disabled\"\n\n\t\tfor key, value := range deployment.ObjectMeta.Annotations {\n\t\t\tif key == \"beetle.clivern.com/application-name\" {\n\t\t\t\tapplicationName = value\n\t\t\t}\n\t\t\tif key == \"beetle.clivern.com/image-format\" {\n\t\t\t\timageFormat = value\n\t\t\t}\n\t\t}\n\t\tfor key, value := range deployment.ObjectMeta.Labels {\n\t\t\tif key == \"beetle.clivern.com/status\" {\n\t\t\t\tstatus = value\n\t\t\t}\n\t\t\tif key == \"beetle.clivern.com/application-id\" {\n\t\t\t\tapplicationID = value\n\t\t\t}\n\t\t}\n\n\t\tif status == \"enabled\" && applicationID != \"\" && imageFormat != \"\" {\n\t\t\tresult.Applications = append(result.Applications, model.App{\n\t\t\t\tID:          applicationID,\n\t\t\t\tName:        applicationName,\n\t\t\t\tImageFormat: imageFormat,\n\t\t\t})\n\t\t} else {\n\t\t\tlog.WithFields(log.Fields{\n\t\t\t\t\"application_id\": applicationID,\n\t\t\t}).Debug(`Application status disabled`)\n\t\t}\n\t}\n\n\tresult.Exists = true\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "core/kubernetes/configmap.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage kubernetes\n\nimport (\n\t\"context\"\n\n\t\"github.com/clivern/beetle/core/model\"\n\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\n// GetConfigMap gets a configmap data\nfunc (c *Cluster) GetConfigMap(ctx context.Context, namespace, name string) (model.ConfigMap, error) {\n\tresult := model.ConfigMap{}\n\n\terr := c.Config()\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tconfigmap, err := c.ClientSet.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{})\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tresult.Name = configmap.ObjectMeta.Name\n\tresult.Namespace = configmap.ObjectMeta.Namespace\n\tresult.UID = string(configmap.ObjectMeta.UID)\n\tresult.CreationTimestamp = configmap.ObjectMeta.CreationTimestamp.String()\n\tresult.Data = configmap.Data\n\tresult.Labels = configmap.ObjectMeta.Labels\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "core/kubernetes/deployment.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage kubernetes\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/clivern/beetle/core/model\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\ttypes \"k8s.io/apimachinery/pkg/types\"\n)\n\n// GetDeployments gets a list of deployments\nfunc (c *Cluster) GetDeployments(ctx context.Context, namespace, label string) ([]model.Deployment, error) {\n\tresult := []model.Deployment{}\n\n\terr := c.Config()\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tdata, err := c.ClientSet.AppsV1().Deployments(namespace).List(ctx, metav1.ListOptions{\n\t\tLabelSelector: label,\n\t})\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tfor _, deployment := range data.Items {\n\t\tresult = append(result, model.Deployment{\n\t\t\tName: deployment.ObjectMeta.Name,\n\t\t\tUID:  string(deployment.ObjectMeta.UID),\n\t\t})\n\t}\n\n\treturn result, nil\n}\n\n// GetDeployment gets a deployment by name\nfunc (c *Cluster) GetDeployment(ctx context.Context, namespace, name string) (model.Deployment, error) {\n\tresult := model.Deployment{}\n\n\terr := c.Config()\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tdeployment, err := c.ClientSet.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{})\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tresult.Name = deployment.ObjectMeta.Name\n\tresult.UID = string(deployment.ObjectMeta.UID)\n\n\treturn result, nil\n}\n\n// PatchDeployment updates the deployment\nfunc (c *Cluster) PatchDeployment(ctx context.Context, namespace, name, data string) (bool, error) {\n\terr := c.Config()\n\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\t_, err = c.ClientSet.AppsV1().Deployments(namespace).Patch(\n\t\tctx,\n\t\tname,\n\t\ttypes.JSONPatchType,\n\t\t[]byte(data),\n\t\tmetav1.PatchOptions{},\n\t)\n\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\treturn true, nil\n}\n\n// FetchDeploymentStatus get deployment status\nfunc (c *Cluster) FetchDeploymentStatus(ctx context.Context, namespace, name string, limit int) (bool, error) {\n\terr := c.Config()\n\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\t// Wait till k8s pick the deployment\n\ttime.Sleep(10 * time.Second)\n\n\tdeployment, err := c.ClientSet.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{})\n\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tstatus := true\n\n\tfor i := 0; i < limit; i++ {\n\t\tstatus = true\n\n\t\tdeployment, err = c.ClientSet.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{})\n\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\tif int(deployment.Generation) != int(deployment.Status.ObservedGeneration) {\n\t\t\tstatus = false\n\t\t}\n\n\t\tif int(deployment.Status.UnavailableReplicas) > 0 {\n\t\t\tstatus = false\n\t\t}\n\n\t\tif int(int32(*deployment.Spec.Replicas)) != int(deployment.Status.AvailableReplicas) {\n\t\t\tstatus = false\n\t\t}\n\n\t\tif !status {\n\t\t\tlog.WithFields(log.Fields{\n\t\t\t\t\"deployment.Generation\":                 int(deployment.Generation),\n\t\t\t\t\"deployment.Status.ObservedGeneration\":  int(deployment.Status.ObservedGeneration),\n\t\t\t\t\"deployment.Spec.Replicas\":              int(int32(*deployment.Spec.Replicas)),\n\t\t\t\t\"deployment.Status.AvailableReplicas\":   int(deployment.Status.AvailableReplicas),\n\t\t\t\t\"deployment.Status.UnavailableReplicas\": int(deployment.Status.UnavailableReplicas),\n\t\t\t}).Debug(`Deployment Success`)\n\t\t\ttime.Sleep(2 * time.Second)\n\t\t} else {\n\t\t\tlog.WithFields(log.Fields{\n\t\t\t\t\"deployment.Generation\":                 int(deployment.Generation),\n\t\t\t\t\"deployment.Status.ObservedGeneration\":  int(deployment.Status.ObservedGeneration),\n\t\t\t\t\"deployment.Spec.Replicas\":              int(int32(*deployment.Spec.Replicas)),\n\t\t\t\t\"deployment.Status.AvailableReplicas\":   int(deployment.Status.AvailableReplicas),\n\t\t\t\t\"deployment.Status.UnavailableReplicas\": int(deployment.Status.UnavailableReplicas),\n\t\t\t}).Debug(`Deployment Success`)\n\n\t\t\treturn true, nil\n\t\t}\n\t}\n\n\tlog.WithFields(log.Fields{\n\t\t\"deployment.Generation\":                 int(deployment.Generation),\n\t\t\"deployment.Status.ObservedGeneration\":  int(deployment.Status.ObservedGeneration),\n\t\t\"deployment.Spec.Replicas\":              int(int32(*deployment.Spec.Replicas)),\n\t\t\"deployment.Status.AvailableReplicas\":   int(deployment.Status.AvailableReplicas),\n\t\t\"deployment.Status.UnavailableReplicas\": int(deployment.Status.UnavailableReplicas),\n\t}).Debug(`Deployment failure`)\n\n\treturn false, fmt.Errorf(fmt.Sprintf(\n\t\t\"Deployment %s failed: namespace %s, Generation %d, ObservedGeneration %d,\"+\n\t\t\t\" UnavailableReplicas %d, Replicas %d, AvailableReplicas %d\",\n\t\tname,\n\t\tnamespace,\n\t\tint(deployment.Generation),\n\t\tint(deployment.Status.ObservedGeneration),\n\t\tint(deployment.Status.UnavailableReplicas),\n\t\tint(int32(*deployment.Spec.Replicas)),\n\t\tint(deployment.Status.AvailableReplicas),\n\t))\n}\n"
  },
  {
    "path": "core/kubernetes/deployment_strategy.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage kubernetes\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/clivern/beetle/core/model\"\n\t\"github.com/clivern/beetle/core/util\"\n)\n\n// Deploy deploys an application\nfunc (c *Cluster) Deploy(deploymentRequest model.DeploymentRequest) (bool, error) {\n\n\tswitch strategy := deploymentRequest.Strategy; strategy {\n\n\tcase model.RecreateStrategy:\n\t\treturn c.RecreateStrategy(deploymentRequest)\n\n\tcase model.RampedStrategy:\n\t\treturn c.RampedStrategy(deploymentRequest)\n\n\tcase model.CanaryStrategy:\n\t\treturn c.CanaryStrategy(deploymentRequest)\n\n\tcase model.BlueGreenStrategy:\n\t\treturn c.BlueGreenStrategy(deploymentRequest)\n\n\tdefault:\n\t\treturn false, fmt.Errorf(\"Invalid deployment strategy %s\", strategy)\n\t}\n}\n\n// RecreateStrategy terminates the old version and release the new one.\n//\n// # This method is like running this command\n//\n// $ kubectl patch deployment toad-deployment --type=json -p '[\n//\n//\t{\"op\":\"replace\", \"path\":\"/spec/strategy\", \"value\":{\"type\":\"Recreate\"}},\n//\t{\"op\":\"replace\",\"path\":\"/spec/template/spec/containers/0/image\",\"value\":\"clivern/toad:release-0.2.4\"}\n//\n// ]'\nfunc (c *Cluster) RecreateStrategy(deploymentRequest model.DeploymentRequest) (bool, error) {\n\tresult := model.Application{}\n\tpatch := make(map[string][]model.PatchStringValue)\n\n\tconfig, err := c.GetConfig(context.TODO(), deploymentRequest.Namespace)\n\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tfor _, app := range config.Applications {\n\t\tif app.ID == deploymentRequest.Application {\n\t\t\tresult, err = c.GetApplication(\n\t\t\t\tcontext.TODO(),\n\t\t\t\tdeploymentRequest.Namespace,\n\t\t\t\tapp.ID,\n\t\t\t\tapp.Name,\n\t\t\t\tapp.ImageFormat,\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\n\ti := 0\n\tfor _, container := range result.Containers {\n\t\tif _, ok := patch[container.Deployment.Name]; !ok {\n\t\t\tpatch[container.Deployment.Name] = []model.PatchStringValue{}\n\t\t}\n\t\tpatch[container.Deployment.Name] = append(patch[container.Deployment.Name], model.PatchStringValue{\n\t\t\tOp:    \"replace\",\n\t\t\tPath:  fmt.Sprintf(\"/spec/template/spec/containers/%d/image\", i),\n\t\t\tValue: strings.Replace(container.Image, container.Version, deploymentRequest.Version, -1),\n\t\t})\n\t\ti++\n\t}\n\n\tdata := \"\"\n\tstatus := true\n\n\tfor deploymentName, deploymentPatch := range patch {\n\t\tdata, err = util.ConvertToJSON(deploymentPatch)\n\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\t// Enforce Recreate strategy\n\t\tdata = strings.Replace(\n\t\t\tdata,\n\t\t\t`[`,\n\t\t\t`[{\"op\":\"replace\",\"path\":\"/spec/strategy\",\"value\":{\"type\":\"Recreate\"}},`,\n\t\t\t-1,\n\t\t)\n\n\t\tstatus, err = c.PatchDeployment(\n\t\t\tcontext.TODO(),\n\t\t\tdeploymentRequest.Namespace,\n\t\t\tdeploymentName,\n\t\t\tdata,\n\t\t)\n\n\t\tif !status || err != nil {\n\t\t\treturn false, err\n\t\t}\n\t}\n\n\tfor deploymentName := range patch {\n\t\tstatus, err = c.FetchDeploymentStatus(context.TODO(), deploymentRequest.Namespace, deploymentName, 600)\n\n\t\tif !status || err != nil {\n\t\t\treturn false, err\n\t\t}\n\t}\n\n\treturn true, nil\n}\n\n// RampedStrategy releases a new version on a rolling update fashion, one after the other.\n//\n// it will set maxSurge as 25% and maxUnavailable as 25%\n//\n// # This method is like running this command\n//\n// $ kubectl patch deployment toad-deployment --type=json -p '[\n//\n//\t    {\"op\":\"replace\", \"path\":\"/spec/strategy\", \"value\":{\"type\":\"RollingUpdate\"}},\n//\t    {\"op\":\"replace\", \"path\":\"/spec/strategy/rollingUpdate\", \"value\":{\"maxSurge\":\"\"}},\n//\t\t   {\"op\":\"replace\", \"path\":\"/spec/strategy/rollingUpdate\", \"value\":{\"maxUnavailable\":\"\"}},\n//\t    {\"op\":\"replace\",\"path\":\"/spec/template/spec/containers/0/image\",\"value\":\"clivern/toad:release-0.2.4\"}\n//\n// ]'\nfunc (c *Cluster) RampedStrategy(deploymentRequest model.DeploymentRequest) (bool, error) {\n\tresult := model.Application{}\n\tpatch := make(map[string][]model.PatchStringValue)\n\n\tconfig, err := c.GetConfig(context.TODO(), deploymentRequest.Namespace)\n\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tfor _, app := range config.Applications {\n\t\tif app.ID == deploymentRequest.Application {\n\t\t\tresult, err = c.GetApplication(\n\t\t\t\tcontext.TODO(),\n\t\t\t\tdeploymentRequest.Namespace,\n\t\t\t\tapp.ID,\n\t\t\t\tapp.Name,\n\t\t\t\tapp.ImageFormat,\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\n\ti := 0\n\tfor _, container := range result.Containers {\n\t\tif _, ok := patch[container.Deployment.Name]; !ok {\n\t\t\tpatch[container.Deployment.Name] = []model.PatchStringValue{}\n\t\t}\n\t\tpatch[container.Deployment.Name] = append(patch[container.Deployment.Name], model.PatchStringValue{\n\t\t\tOp:    \"replace\",\n\t\t\tPath:  fmt.Sprintf(\"/spec/template/spec/containers/%d/image\", i),\n\t\t\tValue: strings.Replace(container.Image, container.Version, deploymentRequest.Version, -1),\n\t\t})\n\t\ti++\n\t}\n\n\tdata := \"\"\n\tstatus := true\n\n\tfor deploymentName, deploymentPatch := range patch {\n\t\tdata, err = util.ConvertToJSON(deploymentPatch)\n\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\tdiff := \"\"\n\n\t\tif strings.Contains(deploymentRequest.MaxSurge, \"%\") && strings.Contains(deploymentRequest.MaxUnavailable, \"%\") {\n\t\t\tdiff = fmt.Sprintf(\n\t\t\t\t`[{\"op\":\"replace\",\"path\":\"/spec/strategy\",\"value\":{\"type\":\"RollingUpdate\"}},`+\n\t\t\t\t\t`{\"op\":\"replace\", \"path\":\"/spec/strategy/rollingUpdate\", \"value\":{\"maxSurge\":\"%s\"}},`+\n\t\t\t\t\t`{\"op\":\"replace\", \"path\":\"/spec/strategy/rollingUpdate\", \"value\":{\"maxUnavailable\":\"%s\"}},`,\n\t\t\t\tdeploymentRequest.MaxSurge,\n\t\t\t\tdeploymentRequest.MaxUnavailable,\n\t\t\t)\n\t\t} else {\n\t\t\tmaxSurge, err := strconv.Atoi(deploymentRequest.MaxSurge)\n\n\t\t\tif err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\n\t\t\tmaxUnavailable, err := strconv.Atoi(deploymentRequest.MaxUnavailable)\n\n\t\t\tif err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\n\t\t\tdiff = fmt.Sprintf(\n\t\t\t\t`[{\"op\":\"replace\",\"path\":\"/spec/strategy\",\"value\":{\"type\":\"RollingUpdate\"}},`+\n\t\t\t\t\t`{\"op\":\"replace\", \"path\":\"/spec/strategy/rollingUpdate\", \"value\":{\"maxSurge\":%d}},`+\n\t\t\t\t\t`{\"op\":\"replace\", \"path\":\"/spec/strategy/rollingUpdate\", \"value\":{\"maxUnavailable\":%d}},`,\n\t\t\t\tmaxSurge,\n\t\t\t\tmaxUnavailable,\n\t\t\t)\n\t\t}\n\n\t\t// Enforce RollingUpdate strategy\n\t\tdata = strings.Replace(\n\t\t\tdata,\n\t\t\t`[`,\n\t\t\tdiff,\n\t\t\t-1,\n\t\t)\n\n\t\tstatus, err = c.PatchDeployment(\n\t\t\tcontext.TODO(),\n\t\t\tdeploymentRequest.Namespace,\n\t\t\tdeploymentName,\n\t\t\tdata,\n\t\t)\n\n\t\tif !status || err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t}\n\n\tfor deploymentName := range patch {\n\t\tstatus, err = c.FetchDeploymentStatus(context.TODO(), deploymentRequest.Namespace, deploymentName, 1000)\n\n\t\tif !status || err != nil {\n\t\t\treturn false, err\n\t\t}\n\t}\n\n\treturn true, nil\n}\n\n// BlueGreenStrategy releases a new version alongside the old version then switch traffic.\nfunc (c *Cluster) BlueGreenStrategy(_ model.DeploymentRequest) (bool, error) {\n\treturn true, nil\n}\n\n// CanaryStrategy releases a new version to a subset of users, then proceed to a full rollout.\nfunc (c *Cluster) CanaryStrategy(_ model.DeploymentRequest) (bool, error) {\n\treturn true, nil\n}\n"
  },
  {
    "path": "core/kubernetes/namespace.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage kubernetes\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/clivern/beetle/core/model\"\n\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\n// GetNamespaces gets a list of cluster namespaces\nfunc (c *Cluster) GetNamespaces(ctx context.Context) ([]model.Namespace, error) {\n\tresult := []model.Namespace{}\n\n\terr := c.Config()\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tdata, err := c.ClientSet.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tfor _, namespace := range data.Items {\n\t\tresult = append(result, model.Namespace{\n\t\t\tName:   namespace.ObjectMeta.Name,\n\t\t\tUID:    string(namespace.ObjectMeta.UID),\n\t\t\tStatus: strings.ToLower(string(namespace.Status.Phase)),\n\t\t})\n\t}\n\n\treturn result, nil\n}\n\n// GetNamespace gets a namespace by name\nfunc (c *Cluster) GetNamespace(ctx context.Context, name string) (model.Namespace, error) {\n\tresult := model.Namespace{}\n\n\terr := c.Config()\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tnamespace, err := c.ClientSet.CoreV1().Namespaces().Get(ctx, name, metav1.GetOptions{})\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tresult.Name = namespace.ObjectMeta.Name\n\tresult.UID = string(namespace.ObjectMeta.UID)\n\tresult.Status = strings.ToLower(string(namespace.Status.Phase))\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "core/kubernetes/namespace_test.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage kubernetes\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/clivern/beetle/core/module\"\n\t\"github.com/clivern/beetle/pkg\"\n\n\t\"github.com/drone/envsubst\"\n\t\"github.com/spf13/viper\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\n// TestNamespace test cases\nfunc TestNamespace(t *testing.T) {\n\ttestingConfig := \"config.testing.yml\"\n\n\t// LoadConfigFile\n\tt.Run(\"LoadConfigFile\", func(t *testing.T) {\n\t\tfs := module.FileSystem{}\n\n\t\tdir, _ := os.Getwd()\n\t\tconfigFile := fmt.Sprintf(\"%s/%s\", dir, testingConfig)\n\n\t\tfor {\n\t\t\tif fs.FileExists(configFile) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tdir = filepath.Dir(dir)\n\t\t\tconfigFile = fmt.Sprintf(\"%s/%s\", dir, testingConfig)\n\t\t}\n\n\t\tt.Logf(\"Load Config File %s\", configFile)\n\n\t\tconfigUnparsed, _ := ioutil.ReadFile(configFile)\n\t\tconfigParsed, _ := envsubst.EvalEnv(string(configUnparsed))\n\t\tviper.SetConfigType(\"yaml\")\n\t\tviper.ReadConfig(bytes.NewBuffer([]byte(configParsed)))\n\t})\n\n\t// TestGetNamespaces\n\tt.Run(\"TestGetNamespaces\", func(t *testing.T) {\n\t\tcluster, err := GetCluster(\"production\")\n\n\t\tpkg.Expect(t, nil, err)\n\t\tpkg.Expect(t, cluster.Name, \"production\")\n\t\tpkg.Expect(t, cluster.Kubeconfig, \"/app/configs/production-cluster-kubeconfig.yaml\")\n\n\t\tcluster.Override(\n\t\t\t&v1.Namespace{\n\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\tName: \"default\",\n\t\t\t\t\tUID:  \"9d0cdf8a-dedc-11e9-bf91-42010a800167\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t&v1.Namespace{\n\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\tName: \"beetle\",\n\t\t\t\t\tUID:  \"9d0cdf8a-dedc-11e9-bf91-42010a800168\",\n\t\t\t\t},\n\t\t\t},\n\t\t)\n\n\t\tnamespaces, err := cluster.GetNamespaces(context.TODO())\n\n\t\tpkg.Expect(t, nil, err)\n\t\tpkg.Expect(t, namespaces[1].Name, \"default\")\n\t\tpkg.Expect(t, namespaces[1].UID, \"9d0cdf8a-dedc-11e9-bf91-42010a800167\")\n\t\tpkg.Expect(t, namespaces[1].Status, \"\")\n\n\t\tpkg.Expect(t, namespaces[0].Name, \"beetle\")\n\t\tpkg.Expect(t, namespaces[0].UID, \"9d0cdf8a-dedc-11e9-bf91-42010a800168\")\n\t\tpkg.Expect(t, namespaces[0].Status, \"\")\n\t})\n\n\t// TestGetNamespaceBeetle\n\tt.Run(\"TestGetNamespaceBeetle\", func(t *testing.T) {\n\t\tcluster, err := GetCluster(\"production\")\n\n\t\tpkg.Expect(t, nil, err)\n\t\tpkg.Expect(t, cluster.Name, \"production\")\n\t\tpkg.Expect(t, cluster.Kubeconfig, \"/app/configs/production-cluster-kubeconfig.yaml\")\n\n\t\tcluster.Override(\n\t\t\t&v1.Namespace{\n\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\tName: \"default\",\n\t\t\t\t\tUID:  \"9d0cdf8a-dedc-11e9-bf91-42010a800167\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t&v1.Namespace{\n\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\tName: \"beetle\",\n\t\t\t\t\tUID:  \"9d0cdf8a-dedc-11e9-bf91-42010a800168\",\n\t\t\t\t},\n\t\t\t},\n\t\t)\n\n\t\tnamespace, err := cluster.GetNamespace(context.TODO(), \"beetle\")\n\n\t\tpkg.Expect(t, nil, err)\n\t\tpkg.Expect(t, namespace.Name, \"beetle\")\n\t\tpkg.Expect(t, namespace.UID, \"9d0cdf8a-dedc-11e9-bf91-42010a800168\")\n\t\tpkg.Expect(t, namespace.Status, \"\")\n\t})\n\n\t// TestGetNamespaceDefault\n\tt.Run(\"TestGetNamespaceDefault\", func(t *testing.T) {\n\t\tcluster, err := GetCluster(\"production\")\n\n\t\tpkg.Expect(t, nil, err)\n\t\tpkg.Expect(t, cluster.Name, \"production\")\n\t\tpkg.Expect(t, cluster.Kubeconfig, \"/app/configs/production-cluster-kubeconfig.yaml\")\n\n\t\tcluster.Override(\n\t\t\t&v1.Namespace{\n\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\tName: \"default\",\n\t\t\t\t\tUID:  \"9d0cdf8a-dedc-11e9-bf91-42010a800167\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t&v1.Namespace{\n\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\tName: \"beetle\",\n\t\t\t\t\tUID:  \"9d0cdf8a-dedc-11e9-bf91-42010a800168\",\n\t\t\t\t},\n\t\t\t},\n\t\t)\n\n\t\tnamespace, err := cluster.GetNamespace(context.TODO(), \"default\")\n\n\t\tpkg.Expect(t, nil, err)\n\t\tpkg.Expect(t, namespace.Name, \"default\")\n\t\tpkg.Expect(t, namespace.UID, \"9d0cdf8a-dedc-11e9-bf91-42010a800167\")\n\t\tpkg.Expect(t, namespace.Status, \"\")\n\t})\n}\n"
  },
  {
    "path": "core/kubernetes/pod.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage kubernetes\n"
  },
  {
    "path": "core/middleware/auth.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage middleware\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/viper\"\n)\n\n// Auth middleware\nfunc Auth() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tpath := c.Request.URL.Path\n\t\tmethod := c.Request.Method\n\n\t\tif strings.Contains(path, \"/api/\") {\n\t\t\tapiKey := c.GetHeader(\"X-API-KEY\")\n\t\t\tif viper.GetString(\"app.api.key\") != \"\" && apiKey != viper.GetString(\"app.api.key\") {\n\t\t\t\tlog.WithFields(log.Fields{\n\t\t\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\t\t\"http_method\":    method,\n\t\t\t\t\t\"http_path\":      path,\n\t\t\t\t\t\"api_key\":        apiKey,\n\t\t\t\t}).Info(`Unauthorized access`)\n\n\t\t\t\tc.AbortWithStatus(http.StatusUnauthorized)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "core/middleware/correlation.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage middleware\n\nimport (\n\t\"strings\"\n\n\t\"github.com/clivern/beetle/core/util\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\n// Correlation middleware\nfunc Correlation() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tcorralationID := c.Request.Header.Get(\"X-Correlation-ID\")\n\n\t\tif strings.TrimSpace(corralationID) == \"\" {\n\t\t\tc.Request.Header.Add(\"X-Correlation-ID\", util.GenerateUUID4())\n\t\t}\n\t\tc.Next()\n\t}\n}\n"
  },
  {
    "path": "core/middleware/log.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage middleware\n\nimport (\n\t\"bytes\"\n\t\"io/ioutil\"\n\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// Logger middleware\nfunc Logger() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\t// before request\n\t\tvar bodyBytes []byte\n\n\t\t// Workaround for issue https://github.com/gin-gonic/gin/issues/1651\n\t\tif c.Request.Body != nil {\n\t\t\tbodyBytes, _ = ioutil.ReadAll(c.Request.Body)\n\t\t}\n\n\t\tc.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))\n\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\"http_method\":    c.Request.Method,\n\t\t\t\"http_path\":      c.Request.URL.Path,\n\t\t\t\"request_body\":   string(bodyBytes),\n\t\t}).Info(\"Request started\")\n\n\t\tc.Next()\n\n\t\t// after request\n\t\tstatus := c.Writer.Status()\n\t\tsize := c.Writer.Size()\n\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t\t\"http_status\":    status,\n\t\t\t\"response_size\":  size,\n\t\t}).Info(`Request finished`)\n\t}\n}\n"
  },
  {
    "path": "core/middleware/metric.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage middleware\n\nimport (\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar (\n\thttpRequests = prometheus.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: \"beetle\",\n\t\t\tName:      \"total_http_requests\",\n\t\t\tHelp:      \"How many HTTP requests processed, partitioned by status code and HTTP method.\",\n\t\t}, []string{\"code\", \"method\", \"handler\", \"host\", \"url\"})\n\n\trequestDuration = prometheus.NewHistogramVec(\n\t\tprometheus.HistogramOpts{\n\t\t\tSubsystem: \"beetle\",\n\t\t\tName:      \"request_duration_seconds\",\n\t\t\tHelp:      \"The HTTP request latencies in seconds.\",\n\t\t},\n\t\t[]string{\"code\", \"method\", \"url\"},\n\t)\n\n\tresponseSize = prometheus.NewSummary(\n\t\tprometheus.SummaryOpts{\n\t\t\tNamespace: \"beetle\",\n\t\t\tName:      \"response_size_bytes\",\n\t\t\tHelp:      \"The HTTP response sizes in bytes.\",\n\t\t},\n\t)\n)\n\nfunc init() {\n\tprometheus.MustRegister(httpRequests)\n\tprometheus.MustRegister(requestDuration)\n\tprometheus.MustRegister(responseSize)\n}\n\n// Metric middleware\nfunc Metric() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\t// before request\n\t\tstart := time.Now()\n\n\t\tc.Next()\n\n\t\t// after request\n\t\telapsed := float64(time.Since(start)) / float64(time.Second)\n\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"correlation_id\": c.Request.Header.Get(\"X-Correlation-ID\"),\n\t\t}).Info(`Collecting metrics`)\n\n\t\t// Collect Metrics\n\t\thttpRequests.WithLabelValues(\n\t\t\tstrconv.Itoa(c.Writer.Status()),\n\t\t\tc.Request.Method,\n\t\t\tc.HandlerName(),\n\t\t\tc.Request.Host,\n\t\t\tc.Request.URL.Path,\n\t\t).Inc()\n\n\t\trequestDuration.WithLabelValues(\n\t\t\tstrconv.Itoa(c.Writer.Status()),\n\t\t\tc.Request.Method,\n\t\t\tc.Request.URL.Path,\n\t\t).Observe(elapsed)\n\n\t\tresponseSize.Observe(float64(c.Writer.Size()))\n\t}\n}\n"
  },
  {
    "path": "core/migration/schema.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage migration\n\nimport (\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/jinzhu/gorm\"\n)\n\n// Job struct\ntype Job struct {\n\tgorm.Model\n\n\tUUID    string    `json:\"uuid\"`\n\tPayload string    `json:\"payload\"`\n\tStatus  string    `json:\"status\"`\n\tType    string    `json:\"type\"`\n\tResult  string    `json:\"result\"`\n\tRetry   int       `json:\"retry\"`\n\tParent  int       `json:\"parent\"`\n\tRunAt   time.Time `json:\"run_at\"`\n}\n\n// LoadFromJSON update object from json\nfunc (j *Job) LoadFromJSON(data []byte) (bool, error) {\n\terr := json.Unmarshal(data, &j)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// ConvertToJSON convert object to json\nfunc (j *Job) ConvertToJSON() (string, error) {\n\tdata, err := json.Marshal(&j)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(data), nil\n}\n"
  },
  {
    "path": "core/model/application.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage model\n\nimport (\n\t\"encoding/json\"\n)\n\n// Container struct\ntype Container struct {\n\tName       string     `json:\"name\"`\n\tImage      string     `json:\"image\"`\n\tVersion    string     `json:\"version\"`\n\tDeployment Deployment `json:\"deployment\"`\n}\n\n// Application struct\ntype Application struct {\n\tID         string      `json:\"id\"`\n\tName       string      `json:\"name\"`\n\tFormat     string      `json:\"format\"`\n\tContainers []Container `json:\"containers\"`\n}\n\n// Applications struct\ntype Applications struct {\n\tApplications []Application `json:\"applications\"`\n}\n\n// Deployment struct\ntype Deployment struct {\n\tName string `json:\"name\"`\n\tUID  string `json:\"uid\"`\n}\n\n// LoadFromJSON update object from json\nfunc (c *Application) LoadFromJSON(data []byte) (bool, error) {\n\terr := json.Unmarshal(data, &c)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// ConvertToJSON convert object to json\nfunc (c *Application) ConvertToJSON() (string, error) {\n\tdata, err := json.Marshal(&c)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(data), nil\n}\n\n// LoadFromJSON update object from json\nfunc (c *Applications) LoadFromJSON(data []byte) (bool, error) {\n\terr := json.Unmarshal(data, &c)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// ConvertToJSON convert object to json\nfunc (c *Applications) ConvertToJSON() (string, error) {\n\tdata, err := json.Marshal(&c)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(data), nil\n}\n\n// LoadFromJSON update object from json\nfunc (d *Deployment) LoadFromJSON(data []byte) (bool, error) {\n\terr := json.Unmarshal(data, &d)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// ConvertToJSON convert object to json\nfunc (d *Deployment) ConvertToJSON() (string, error) {\n\tdata, err := json.Marshal(&d)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(data), nil\n}\n"
  },
  {
    "path": "core/model/cluster.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage model\n\nimport (\n\t\"encoding/json\"\n)\n\n// Cluster struct\ntype Cluster struct {\n\tName   string `json:\"name\"`\n\tHealth bool   `json:\"health\"`\n}\n\n// Clusters struct\ntype Clusters struct {\n\tClusters []Cluster `json:\"clusters\"`\n}\n\n// LoadFromJSON update object from json\nfunc (c *Cluster) LoadFromJSON(data []byte) (bool, error) {\n\terr := json.Unmarshal(data, &c)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// ConvertToJSON convert object to json\nfunc (c *Cluster) ConvertToJSON() (string, error) {\n\tdata, err := json.Marshal(&c)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(data), nil\n}\n\n// LoadFromJSON update object from json\nfunc (c *Clusters) LoadFromJSON(data []byte) (bool, error) {\n\terr := json.Unmarshal(data, &c)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// ConvertToJSON convert object to json\nfunc (c *Clusters) ConvertToJSON() (string, error) {\n\tdata, err := json.Marshal(&c)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(data), nil\n}\n"
  },
  {
    "path": "core/model/configmap.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage model\n\nimport (\n\t\"encoding/json\"\n)\n\n// ConfigMap struct\ntype ConfigMap struct {\n\tName              string            `json:\"name\"`\n\tNamespace         string            `json:\"namespace\"`\n\tUID               string            `json:\"uid\"`\n\tCreationTimestamp string            `json:\"creation_timestamp\"`\n\tData              map[string]string `json:\"data\"`\n\tLabels            map[string]string `json:\"labels\"`\n}\n\n// LoadFromJSON update object from json\nfunc (d *ConfigMap) LoadFromJSON(data []byte) (bool, error) {\n\terr := json.Unmarshal(data, &d)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// ConvertToJSON convert object to json\nfunc (d *ConfigMap) ConvertToJSON() (string, error) {\n\tdata, err := json.Marshal(&d)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(data), nil\n}\n"
  },
  {
    "path": "core/model/configs.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage model\n\n// App struct\ntype App struct {\n\tID          string\n\tName        string\n\tImageFormat string\n}\n\n// Configs struct\ntype Configs struct {\n\tExists       bool\n\tVersion      string\n\tApplications []App\n}\n"
  },
  {
    "path": "core/model/dsn.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage model\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n)\n\n// DSN struct\ntype DSN struct {\n\tDriver   string `json:\"driver\"`\n\tUsername string `json:\"username\"`\n\tPassword string `json:\"password\"`\n\tHostname string `json:\"hostname\"`\n\tPort     int    `json:\"port\"`\n\tName     string `json:\"name\"`\n}\n\n// ToString gets the dsn string\nfunc (d *DSN) ToString() string {\n\tif d.Driver == \"mysql\" {\n\t\treturn fmt.Sprintf(\n\t\t\t\"%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True\",\n\t\t\td.Username,\n\t\t\td.Password,\n\t\t\td.Hostname,\n\t\t\td.Port,\n\t\t\td.Name,\n\t\t)\n\t}\n\n\t// sqlite3 by default\n\treturn d.Name\n}\n\n// LoadFromJSON update object from json\nfunc (d *DSN) LoadFromJSON(data []byte) (bool, error) {\n\terr := json.Unmarshal(data, &d)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// ConvertToJSON convert object to json\nfunc (d *DSN) ConvertToJSON() (string, error) {\n\tdata, err := json.Marshal(&d)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(data), nil\n}\n"
  },
  {
    "path": "core/model/dsn_test.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage model\n\nimport (\n\t\"testing\"\n\n\t\"github.com/clivern/beetle/pkg\"\n)\n\n// TestDsnToString test cases\nfunc TestDsnToString(t *testing.T) {\n\tt.Run(\"TestDsnToStringForMySQL\", func(t *testing.T) {\n\t\tdsn := DSN{\n\t\t\tDriver:   \"mysql\",\n\t\t\tUsername: \"root\",\n\t\t\tPassword: \"root\",\n\t\t\tHostname: \"127.0.0.1\",\n\t\t\tPort:     3306,\n\t\t\tName:     \"beetle\",\n\t\t}\n\t\tpkg.Expect(t, \"root:root@tcp(127.0.0.1:3306)/beetle?charset=utf8&parseTime=True\", dsn.ToString())\n\t})\n\n\tt.Run(\"TestDsnToStringForSQLLite\", func(t *testing.T) {\n\t\tdsn := DSN{\n\t\t\tDriver: \"sqlite3\",\n\t\t\tName:   \"/path/to/beetle.db\",\n\t\t}\n\t\tpkg.Expect(t, \"/path/to/beetle.db\", dsn.ToString())\n\t})\n}\n"
  },
  {
    "path": "core/model/job.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage model\n\nimport (\n\t\"encoding/json\"\n\t\"time\"\n)\n\nvar (\n\t// JobPending pending job type\n\tJobPending = \"pending\"\n\n\t// JobFailed failed job type\n\tJobFailed = \"failed\"\n\n\t// JobSuccess success job type\n\tJobSuccess = \"success\"\n\n\t// JobOnHold on hold job type\n\tJobOnHold = \"on_hold\"\n\n\t// JobDeploymentUpdate deployment update\n\tJobDeploymentUpdate = \"deployment.update\"\n\n\t// JobDeploymentNotify deployment notify\n\tJobDeploymentNotify = \"deployment.notify\"\n)\n\n// Job struct\ntype Job struct {\n\tID        int        `json:\"id\"`\n\tUUID      string     `json:\"uuid\"`\n\tPayload   string     `json:\"payload\"`\n\tStatus    string     `json:\"status\"`\n\tType      string     `json:\"type\"`\n\tResult    string     `json:\"result\"`\n\tRetry     int        `json:\"retry\"`\n\tParent    int        `json:\"parent\"`\n\tRunAt     *time.Time `json:\"run_at\"`\n\tCreatedAt time.Time  `json:\"created_at\"`\n\tUpdatedAt time.Time  `json:\"updated_at\"`\n}\n\n// Jobs struct\ntype Jobs struct {\n\tJobs []Job `json:\"jobs\"`\n}\n\n// LoadFromJSON update object from json\nfunc (j *Job) LoadFromJSON(data []byte) (bool, error) {\n\terr := json.Unmarshal(data, &j)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// ConvertToJSON convert object to json\nfunc (j *Job) ConvertToJSON() (string, error) {\n\tdata, err := json.Marshal(&j)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(data), nil\n}\n\n// LoadFromJSON update object from json\nfunc (j *Jobs) LoadFromJSON(data []byte) (bool, error) {\n\terr := json.Unmarshal(data, &j)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// ConvertToJSON convert object to json\nfunc (j *Jobs) ConvertToJSON() (string, error) {\n\tdata, err := json.Marshal(&j)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(data), nil\n}\n"
  },
  {
    "path": "core/model/message.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage model\n\nimport (\n\t\"encoding/json\"\n)\n\n// Message struct\ntype Message struct {\n\tUUID string `json:\"uuid\"`\n\tJob  int    `json:\"job\"`\n}\n\n// LoadFromJSON update object from json\nfunc (m *Message) LoadFromJSON(data []byte) (bool, error) {\n\terr := json.Unmarshal(data, &m)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// ConvertToJSON convert object to json\nfunc (m *Message) ConvertToJSON() (string, error) {\n\tdata, err := json.Marshal(&m)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(data), nil\n}\n"
  },
  {
    "path": "core/model/metric.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage model\n\nimport (\n\t\"encoding/json\"\n\t\"strconv\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n)\n\nconst (\n\t// COUNTER is a Prometheus COUNTER metric\n\tCOUNTER string = \"counter\"\n\t// GAUGE is a Prometheus GAUGE metric\n\tGAUGE string = \"gauge\"\n\t// HISTOGRAM is a Prometheus HISTOGRAM metric\n\tHISTOGRAM string = \"histogram\"\n\t// SUMMARY is a Prometheus SUMMARY metric\n\tSUMMARY string = \"summary\"\n)\n\n// Metric struct\ntype Metric struct {\n\tType    string            `json:\"type\"`\n\tName    string            `json:\"name\"`\n\tHelp    string            `json:\"help\"`\n\tMethod  string            `json:\"method\"`\n\tValue   string            `json:\"value\"`\n\tLabels  prometheus.Labels `json:\"labels\"`\n\tBuckets []float64         `json:\"buckets\"`\n}\n\n// LoadFromJSON update object from json\nfunc (m *Metric) LoadFromJSON(data []byte) (bool, error) {\n\terr := json.Unmarshal(data, &m)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// ConvertToJSON convert object to json\nfunc (m *Metric) ConvertToJSON() (string, error) {\n\tdata, err := json.Marshal(&m)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(data), nil\n}\n\n// LabelKeys gets a list of label keys\nfunc (m *Metric) LabelKeys() []string {\n\tkeys := []string{}\n\n\tfor k := range m.Labels {\n\t\tkeys = append(keys, k)\n\t}\n\n\treturn keys\n}\n\n// LabelValues gets a list of label values\nfunc (m *Metric) LabelValues() []string {\n\tvalues := []string{}\n\n\tfor _, v := range m.Labels {\n\t\tvalues = append(values, v)\n\t}\n\n\treturn values\n}\n\n// GetValueAsFloat gets a list of label values\nfunc (m *Metric) GetValueAsFloat() (float64, error) {\n\tvalue, err := strconv.ParseFloat(m.Value, 64)\n\n\tif err != nil {\n\t\treturn 0, nil\n\t}\n\n\treturn value, nil\n}\n"
  },
  {
    "path": "core/model/migration.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage model\n\nimport (\n\t\"encoding/json\"\n\t\"time\"\n)\n\n// Migration struct\ntype Migration struct {\n\tID    int       `json:\"id\"`\n\tFlag  string    `json:\"file\"`\n\tRunAt time.Time `json:\"run_at\"`\n}\n\n// LoadFromJSON update object from json\nfunc (m *Migration) LoadFromJSON(data []byte) (bool, error) {\n\terr := json.Unmarshal(data, &m)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// ConvertToJSON convert object to json\nfunc (m *Migration) ConvertToJSON() (string, error) {\n\tdata, err := json.Marshal(&m)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(data), nil\n}\n"
  },
  {
    "path": "core/model/namespace.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage model\n\nimport (\n\t\"encoding/json\"\n)\n\n// Namespace struct\ntype Namespace struct {\n\tName   string `json:\"name\"`\n\tUID    string `json:\"uid\"`\n\tStatus string `json:\"status\"`\n}\n\n// Namespaces struct\ntype Namespaces struct {\n\tNamespaces []Namespace `json:\"namespaces\"`\n}\n\n// LoadFromJSON update object from json\nfunc (d *Namespace) LoadFromJSON(data []byte) (bool, error) {\n\terr := json.Unmarshal(data, &d)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// ConvertToJSON convert object to json\nfunc (d *Namespace) ConvertToJSON() (string, error) {\n\tdata, err := json.Marshal(&d)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(data), nil\n}\n\n// LoadFromJSON update object from json\nfunc (d *Namespaces) LoadFromJSON(data []byte) (bool, error) {\n\terr := json.Unmarshal(data, &d)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// ConvertToJSON convert object to json\nfunc (d *Namespaces) ConvertToJSON() (string, error) {\n\tdata, err := json.Marshal(&d)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(data), nil\n}\n"
  },
  {
    "path": "core/model/patch.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage model\n\n// PatchStringValue specifies a patch operation for a string.\ntype PatchStringValue struct {\n\tOp    string `json:\"op\"`\n\tPath  string `json:\"path\"`\n\tValue string `json:\"value\"`\n}\n\n// PatchUInt32Value specifies a patch operation for a uint32.\ntype PatchUInt32Value struct {\n\tOp    string `json:\"op\"`\n\tPath  string `json:\"path\"`\n\tValue uint32 `json:\"value\"`\n}\n"
  },
  {
    "path": "core/model/request.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage model\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"reflect\"\n)\n\nvar (\n\t// RecreateStrategy var\n\tRecreateStrategy = \"recreate\"\n\t// RampedStrategy var\n\tRampedStrategy = \"ramped\"\n\t// CanaryStrategy var\n\tCanaryStrategy = \"canary\"\n\t// BlueGreenStrategy var\n\tBlueGreenStrategy = \"blue_green\"\n)\n\n// DeploymentRequest struct\ntype DeploymentRequest struct {\n\tCluster     string `json:\"cluster\"`\n\tNamespace   string `json:\"namespace\"`\n\tApplication string `json:\"application\"`\n\tVersion     string `json:\"version\"`\n\tStrategy    string `json:\"strategy\"`\n\tStatus      string `json:\"status\"`\n\n\t// Ramped Strategy\n\tMaxSurge       string `json:\"maxSurge\"`\n\tMaxUnavailable string `json:\"maxUnavailable\"`\n}\n\n// LoadFromJSON update object from json\nfunc (d *DeploymentRequest) LoadFromJSON(data []byte) (bool, error) {\n\terr := json.Unmarshal(data, &d)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// ConvertToJSON convert object to json\nfunc (d *DeploymentRequest) ConvertToJSON() (string, error) {\n\tdata, err := json.Marshal(&d)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(data), nil\n}\n\n// Validate validates the request\nfunc (d *DeploymentRequest) Validate(strategies []string) error {\n\tif d.Version == \"\" {\n\t\treturn fmt.Errorf(\n\t\t\t\"Error! version is required\",\n\t\t)\n\t}\n\n\tif !In(d.Strategy, strategies) {\n\t\treturn fmt.Errorf(\n\t\t\t\"Error! strategy %s is invalid\",\n\t\t\td.Strategy,\n\t\t)\n\t}\n\n\treturn nil\n}\n\n// In check if value is on array\nfunc In(val interface{}, array interface{}) bool {\n\tswitch reflect.TypeOf(array).Kind() {\n\tcase reflect.Slice:\n\t\ts := reflect.ValueOf(array)\n\n\t\tfor i := 0; i < s.Len(); i++ {\n\t\t\tif reflect.DeepEqual(val, s.Index(i).Interface()) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "core/module/database.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage module\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/clivern/beetle/core/migration\"\n\t\"github.com/clivern/beetle/core/model\"\n\n\t\"github.com/jinzhu/gorm\"\n\t_ \"github.com/jinzhu/gorm/dialects/mysql\"\n\t_ \"github.com/jinzhu/gorm/dialects/sqlite\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/viper\"\n)\n\n// Database struct\ntype Database struct {\n\tConnection *gorm.DB\n}\n\n// Connect connects to a MySQL database\nfunc (db *Database) Connect(dsn model.DSN) error {\n\tvar err error\n\n\t// Reuse db connections http://go-database-sql.org/surprises.html\n\tif db.Ping() == nil {\n\t\treturn nil\n\t}\n\n\tdb.Connection, err = gorm.Open(dsn.Driver, dsn.ToString())\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Ping check the db connection\nfunc (db *Database) Ping() error {\n\n\tif db.Connection == nil {\n\t\treturn fmt.Errorf(\"No DB Connections Found\")\n\t}\n\n\terr := db.Connection.DB().Ping()\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Cleanup stale connections http://go-database-sql.org/surprises.html\n\tdb.Connection.DB().SetMaxOpenConns(5)\n\tdb.Connection.DB().SetConnMaxLifetime(time.Duration(10) * time.Second)\n\tdbStats := db.Connection.DB().Stats()\n\n\tlog.WithFields(log.Fields{\n\t\t\"dbStats.maxOpenConnections\": int(dbStats.MaxOpenConnections),\n\t\t\"dbStats.openConnections\":    int(dbStats.OpenConnections),\n\t\t\"dbStats.inUse\":              int(dbStats.InUse),\n\t\t\"dbStats.idle\":               int(dbStats.Idle),\n\t}).Debug(`Open DB Connection`)\n\n\treturn nil\n}\n\n// AutoConnect connects to a MySQL database using loaded configs\nfunc (db *Database) AutoConnect() error {\n\tvar err error\n\n\t// Reuse db connections http://go-database-sql.org/surprises.html\n\tif db.Ping() == nil {\n\t\treturn nil\n\t}\n\n\tdsn := model.DSN{\n\t\tDriver:   viper.GetString(\"app.database.driver\"),\n\t\tUsername: viper.GetString(\"app.database.username\"),\n\t\tPassword: viper.GetString(\"app.database.password\"),\n\t\tHostname: viper.GetString(\"app.database.host\"),\n\t\tPort:     viper.GetInt(\"app.database.port\"),\n\t\tName:     viper.GetString(\"app.database.name\"),\n\t}\n\n\tdb.Connection, err = gorm.Open(dsn.Driver, dsn.ToString())\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Migrate migrates the database\nfunc (db *Database) Migrate() bool {\n\tstatus := true\n\tdb.Connection.AutoMigrate(&migration.Job{})\n\tstatus = status && db.Connection.HasTable(&migration.Job{})\n\treturn status\n}\n\n// Rollback drop tables\nfunc (db *Database) Rollback() bool {\n\tstatus := true\n\tdb.Connection.DropTableIfExists(&migration.Job{})\n\tstatus = status && !db.Connection.HasTable(&migration.Job{})\n\treturn status\n}\n\n// HasTable checks if table exists\nfunc (db *Database) HasTable(table string) bool {\n\treturn db.Connection.HasTable(table)\n}\n\n// CreateJob creates a new job\nfunc (db *Database) CreateJob(job *model.Job) *model.Job {\n\tdb.Connection.Create(job)\n\treturn job\n}\n\n// JobExistByID check if job exists\nfunc (db *Database) JobExistByID(id int) bool {\n\tjob := model.Job{}\n\n\tdb.Connection.Where(\"id = ?\", id).First(&job)\n\n\treturn job.ID > 0\n}\n\n// GetJobByID gets a job by id\nfunc (db *Database) GetJobByID(id int) model.Job {\n\tjob := model.Job{}\n\n\tdb.Connection.Where(\"id = ?\", id).First(&job)\n\n\treturn job\n}\n\n// GetJobs gets jobs\nfunc (db *Database) GetJobs() []model.Job {\n\tjobs := []model.Job{}\n\n\tdb.Connection.Select(\"*\").Find(&jobs)\n\n\treturn jobs\n}\n\n// JobExistByUUID check if job exists\nfunc (db *Database) JobExistByUUID(uuid string) bool {\n\tjob := model.Job{}\n\n\tdb.Connection.Where(\"uuid = ?\", uuid).First(&job)\n\n\treturn job.ID > 0\n}\n\n// GetJobByUUID gets a job by uuid\nfunc (db *Database) GetJobByUUID(uuid string) model.Job {\n\tjob := model.Job{}\n\n\tdb.Connection.Where(\"uuid = ?\", uuid).First(&job)\n\n\treturn job\n}\n\n// GetPendingJobByType gets a job by uuid\nfunc (db *Database) GetPendingJobByType(jobType string) model.Job {\n\tjob := model.Job{}\n\n\tdb.Connection.Where(\"status = ? AND type = ?\", model.JobPending, jobType).First(&job)\n\n\treturn job\n}\n\n// CountJobs count jobs by status\nfunc (db *Database) CountJobs(status string) int {\n\tcount := 0\n\n\tdb.Connection.Model(&model.Job{}).Where(\"status = ?\", status).Count(&count)\n\n\treturn count\n}\n\n// DeleteJobByID deletes a job by id\nfunc (db *Database) DeleteJobByID(id int) {\n\tdb.Connection.Unscoped().Where(\"id=?\", id).Delete(&migration.Job{})\n}\n\n// DeleteJobByUUID deletes a job by uuid\nfunc (db *Database) DeleteJobByUUID(uuid string) {\n\tdb.Connection.Unscoped().Where(\"uuid=?\", uuid).Delete(&migration.Job{})\n}\n\n// UpdateJobByID updates a job by ID\nfunc (db *Database) UpdateJobByID(job *model.Job) {\n\tdb.Connection.Save(&job)\n}\n\n// Close closes MySQL database connection\nfunc (db *Database) Close() error {\n\treturn db.Connection.Close()\n}\n\n// ReleaseChildJobs count jobs by status\nfunc (db *Database) ReleaseChildJobs(parentID int) {\n\tdb.Connection.Model(&model.Job{}).Where(\n\t\t\"parent = ? AND status = ?\",\n\t\tparentID,\n\t\tmodel.JobOnHold,\n\t).Update(\"status\", model.JobPending)\n}\n"
  },
  {
    "path": "core/module/database_test.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage module\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/clivern/beetle/core/model\"\n\t\"github.com/clivern/beetle/pkg\"\n\n\t\"github.com/drone/envsubst\"\n\t\"github.com/spf13/viper\"\n)\n\nvar testingConfig = \"config.testing.yml\"\n\n// TestDatabase test cases\nfunc TestDatabase(t *testing.T) {\n\t// LoadConfigFile\n\tt.Run(\"LoadConfigFile\", func(t *testing.T) {\n\t\tfs := FileSystem{}\n\n\t\tdir, _ := os.Getwd()\n\t\tconfigFile := fmt.Sprintf(\"%s/%s\", dir, testingConfig)\n\n\t\tfor {\n\t\t\tif fs.FileExists(configFile) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tdir = filepath.Dir(dir)\n\t\t\tconfigFile = fmt.Sprintf(\"%s/%s\", dir, testingConfig)\n\t\t}\n\n\t\tt.Logf(\"Load Config File %s\", configFile)\n\n\t\tconfigUnparsed, _ := ioutil.ReadFile(configFile)\n\t\tconfigParsed, _ := envsubst.EvalEnv(string(configUnparsed))\n\t\tviper.SetConfigType(\"yaml\")\n\t\tviper.ReadConfig(bytes.NewBuffer([]byte(configParsed)))\n\t})\n\n\t// TestDatabaseConnection\n\tt.Run(\"TestDatabaseConnection\", func(t *testing.T) {\n\t\tdb := Database{}\n\t\terr := db.Connect(model.DSN{\n\t\t\tDriver:   viper.GetString(\"app.database.driver\"),\n\t\t\tUsername: viper.GetString(\"app.database.username\"),\n\t\t\tPassword: viper.GetString(\"app.database.password\"),\n\t\t\tHostname: viper.GetString(\"app.database.host\"),\n\t\t\tPort:     viper.GetInt(\"app.database.port\"),\n\t\t\tName:     viper.GetString(\"app.database.name\"),\n\t\t})\n\t\tpkg.Expect(t, nil, err)\n\n\t\tdefer db.Close()\n\t\tpkg.Expect(t, true, db.Rollback())\n\t\tpkg.Expect(t, true, db.Migrate())\n\t\tpkg.Expect(t, true, db.HasTable(\"jobs\"))\n\t})\n\n\t// TestJobCRUD\n\tt.Run(\"TestJobCRUD\", func(t *testing.T) {\n\t\tdb := Database{}\n\t\terr := db.Connect(model.DSN{\n\t\t\tDriver:   viper.GetString(\"app.database.driver\"),\n\t\t\tUsername: viper.GetString(\"app.database.username\"),\n\t\t\tPassword: viper.GetString(\"app.database.password\"),\n\t\t\tHostname: viper.GetString(\"app.database.host\"),\n\t\t\tPort:     viper.GetInt(\"app.database.port\"),\n\t\t\tName:     viper.GetString(\"app.database.name\"),\n\t\t})\n\t\tpkg.Expect(t, nil, err)\n\n\t\tdefer db.Close()\n\n\t\tpkg.Expect(t, true, db.Rollback())\n\t\tpkg.Expect(t, true, db.Migrate())\n\t\tpkg.Expect(t, true, db.HasTable(\"jobs\"))\n\n\t\t// Delete the job if it exists\n\t\tdb.DeleteJobByID(1)\n\t\tdb.DeleteJobByUUID(\"dddde755-5f99-4e51-a517-77878986a07e\")\n\n\t\t// Create the job\n\t\tjob := db.CreateJob(&model.Job{\n\t\t\tUUID:   \"dddde755-5f99-4e51-a517-77878986a07e\",\n\t\t\tParent: 0,\n\t\t})\n\n\t\tpkg.Expect(t, 1, job.ID)\n\t\tpkg.Expect(t, \"dddde755-5f99-4e51-a517-77878986a07e\", job.UUID)\n\n\t\tjob1 := db.GetJobByID(1)\n\t\tjob2 := db.GetJobByUUID(\"dddde755-5f99-4e51-a517-77878986a07e\")\n\n\t\tpkg.Expect(t, job1.ID, job2.ID)\n\t\tpkg.Expect(t, job1.UUID, job2.UUID)\n\n\t\tjob1.UUID = \"dddde755-5f99-4e51-a517-77878986a07n\"\n\t\tdb.UpdateJobByID(&job1)\n\n\t\tjob3 := db.GetJobByID(1)\n\t\tpkg.Expect(t, \"dddde755-5f99-4e51-a517-77878986a07n\", job3.UUID)\n\t\tpkg.Expect(t, job1.UUID, job3.UUID)\n\n\t\tpkg.Expect(t, 0, db.GetJobByUUID(\"dddde755-5f99-4e51-a517-77878986a07ek\").ID)\n\n\t\tpkg.Expect(t, false, db.JobExistByUUID(\"dddde755-5f99-4e51-a517-77878986a07eo\"))\n\t\tpkg.Expect(t, false, db.JobExistByID(20))\n\t})\n}\n"
  },
  {
    "path": "core/module/file_system.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage module\n\nimport (\n\t\"os\"\n)\n\n// FileSystem struct\ntype FileSystem struct{}\n\n// PathExists reports whether the path exists\nfunc (fs *FileSystem) PathExists(path string) bool {\n\tif _, err := os.Stat(path); os.IsNotExist(err) {\n\t\treturn false\n\t}\n\treturn true\n}\n\n// FileExists reports whether the named file exists\nfunc (fs *FileSystem) FileExists(path string) bool {\n\tif fi, err := os.Stat(path); err == nil {\n\t\tif fi.Mode().IsRegular() {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// DirExists reports whether the dir exists\nfunc (fs *FileSystem) DirExists(path string) bool {\n\tif fi, err := os.Stat(path); err == nil {\n\t\tif fi.Mode().IsDir() {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// EnsureDir ensures that directory exists\nfunc (fs *FileSystem) EnsureDir(dirName string, mode int) (bool, error) {\n\terr := os.MkdirAll(dirName, os.FileMode(mode))\n\n\tif err == nil || os.IsExist(err) {\n\t\treturn true, nil\n\t}\n\treturn false, err\n}\n"
  },
  {
    "path": "core/module/http.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage module\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n)\n\n// HTTPClient struct\ntype HTTPClient struct {\n\tTimeout time.Duration\n}\n\n// NewHTTPClient creates an instance of http client\nfunc NewHTTPClient(timeout int) *HTTPClient {\n\treturn &HTTPClient{\n\t\tTimeout: time.Duration(timeout),\n\t}\n}\n\n// Get http call\nfunc (h *HTTPClient) Get(ctx context.Context, endpoint string, parameters, headers map[string]string) (*http.Response, error) {\n\n\tendpoint, err := h.buildParameters(endpoint, parameters)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq, _ := http.NewRequest(\"GET\", endpoint, nil)\n\n\treq = req.WithContext(ctx)\n\n\tfor k, v := range headers {\n\t\treq.Header.Add(k, v)\n\t}\n\n\tclient := http.Client{\n\t\tTimeout: time.Second * h.Timeout,\n\t}\n\n\tresp, err := client.Do(req)\n\n\tif err != nil {\n\t\treturn resp, err\n\t}\n\n\treturn resp, err\n}\n\n// Post http call\nfunc (h *HTTPClient) Post(ctx context.Context, endpoint string, data string, parameters, headers map[string]string) (*http.Response, error) {\n\n\tendpoint, err := h.buildParameters(endpoint, parameters)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq, _ := http.NewRequest(\"POST\", endpoint, bytes.NewBufferString(data))\n\n\treq = req.WithContext(ctx)\n\n\tfor k, v := range headers {\n\t\treq.Header.Add(k, v)\n\t}\n\n\tclient := http.Client{\n\t\tTimeout: time.Second * h.Timeout,\n\t}\n\n\tresp, err := client.Do(req)\n\n\tif err != nil {\n\t\treturn resp, err\n\t}\n\n\treturn resp, err\n}\n\n// Put http call\nfunc (h *HTTPClient) Put(ctx context.Context, endpoint string, data string, parameters, headers map[string]string) (*http.Response, error) {\n\n\tendpoint, err := h.buildParameters(endpoint, parameters)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq, _ := http.NewRequest(\"PUT\", endpoint, bytes.NewBufferString(data))\n\n\treq = req.WithContext(ctx)\n\n\tfor k, v := range headers {\n\t\treq.Header.Add(k, v)\n\t}\n\n\tclient := http.Client{\n\t\tTimeout: time.Second * h.Timeout,\n\t}\n\n\tresp, err := client.Do(req)\n\n\tif err != nil {\n\t\treturn resp, err\n\t}\n\n\treturn resp, err\n}\n\n// Patch http call\nfunc (h *HTTPClient) Patch(ctx context.Context, endpoint string, data string, parameters, headers map[string]string) (*http.Response, error) {\n\n\tendpoint, err := h.buildParameters(endpoint, parameters)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq, _ := http.NewRequest(\"PATCH\", endpoint, bytes.NewBufferString(data))\n\n\treq = req.WithContext(ctx)\n\n\tfor k, v := range headers {\n\t\treq.Header.Add(k, v)\n\t}\n\n\tclient := http.Client{\n\t\tTimeout: time.Second * h.Timeout,\n\t}\n\n\tresp, err := client.Do(req)\n\n\tif err != nil {\n\t\treturn resp, err\n\t}\n\n\treturn resp, err\n}\n\n// Delete http call\nfunc (h *HTTPClient) Delete(ctx context.Context, endpoint string, parameters, headers map[string]string) (*http.Response, error) {\n\n\tendpoint, err := h.buildParameters(endpoint, parameters)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq, _ := http.NewRequest(\"DELETE\", endpoint, nil)\n\n\treq = req.WithContext(ctx)\n\n\tfor k, v := range headers {\n\t\treq.Header.Add(k, v)\n\t}\n\n\tclient := http.Client{\n\t\tTimeout: time.Second * h.Timeout,\n\t}\n\n\tresp, err := client.Do(req)\n\n\tif err != nil {\n\t\treturn resp, err\n\t}\n\n\treturn resp, err\n}\n\n// buildParameters add parameters to URL\nfunc (h *HTTPClient) buildParameters(endpoint string, parameters map[string]string) (string, error) {\n\tu, err := url.Parse(endpoint)\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tq := u.Query()\n\n\tfor k, v := range parameters {\n\t\tq.Set(k, v)\n\t}\n\n\tu.RawQuery = q.Encode()\n\n\treturn u.String(), nil\n}\n\n// BuildData build body data\nfunc (h *HTTPClient) BuildData(parameters map[string]string) string {\n\tvar items []string\n\n\tfor k, v := range parameters {\n\t\titems = append(items, fmt.Sprintf(\"%s=%s\", k, v))\n\t}\n\n\treturn strings.Join(items, \"&\")\n}\n\n// ToString response body to string\nfunc (h *HTTPClient) ToString(response *http.Response) (string, error) {\n\tdefer response.Body.Close()\n\n\tbody, err := ioutil.ReadAll(response.Body)\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn string(body), nil\n}\n\n// GetStatusCode response status code\nfunc (h *HTTPClient) GetStatusCode(response *http.Response) int {\n\treturn response.StatusCode\n}\n\n// GetHeaderValue get response header value\nfunc (h *HTTPClient) GetHeaderValue(response *http.Response, key string) string {\n\treturn response.Header.Get(key)\n}\n"
  },
  {
    "path": "core/module/http_test.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage module\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/clivern/beetle/pkg\"\n)\n\n// TestHttpGet test cases\nfunc TestHttpGet(t *testing.T) {\n\tt.Run(\"TestHttpGet\", func(t *testing.T) {\n\t\thttpClient := NewHTTPClient(20)\n\t\tresponse, error := httpClient.Get(\n\t\t\tcontext.TODO(),\n\t\t\t\"https://httpbin.org/get\",\n\t\t\tmap[string]string{\"arg1\": \"value1\"},\n\t\t\tmap[string]string{\"X-Api-Key\": \"hipp-123\"},\n\t\t)\n\n\t\tpkg.Expect(t, http.StatusOK, httpClient.GetStatusCode(response))\n\t\tpkg.Expect(t, nil, error)\n\n\t\tbody, error := httpClient.ToString(response)\n\n\t\tpkg.Expect(t, true, strings.Contains(body, \"value1\"))\n\t\tpkg.Expect(t, true, strings.Contains(body, \"arg1\"))\n\t\tpkg.Expect(t, true, strings.Contains(body, \"arg1=value1\"))\n\t\tpkg.Expect(t, true, strings.Contains(body, \"X-Api-Key\"))\n\t\tpkg.Expect(t, true, strings.Contains(body, \"hipp-123\"))\n\t\tpkg.Expect(t, nil, error)\n\t})\n}\n\n// TestHttpDelete test cases\nfunc TestHttpDelete(t *testing.T) {\n\tt.Run(\"TestHttpDelete\", func(t *testing.T) {\n\t\thttpClient := NewHTTPClient(20)\n\t\tresponse, error := httpClient.Delete(\n\t\t\tcontext.TODO(),\n\t\t\t\"https://httpbin.org/delete\",\n\t\t\tmap[string]string{\"arg1\": \"value1\"},\n\t\t\tmap[string]string{\"X-Api-Key\": \"hipp-123\"},\n\t\t)\n\n\t\tpkg.Expect(t, http.StatusOK, httpClient.GetStatusCode(response))\n\t\tpkg.Expect(t, nil, error)\n\n\t\tbody, error := httpClient.ToString(response)\n\n\t\tpkg.Expect(t, true, strings.Contains(body, \"value1\"))\n\t\tpkg.Expect(t, true, strings.Contains(body, \"arg1\"))\n\t\tpkg.Expect(t, true, strings.Contains(body, \"arg1=value1\"))\n\t\tpkg.Expect(t, true, strings.Contains(body, \"X-Api-Key\"))\n\t\tpkg.Expect(t, true, strings.Contains(body, \"hipp-123\"))\n\t\tpkg.Expect(t, nil, error)\n\t})\n}\n\n// TestHttpPost test cases\nfunc TestHttpPost(t *testing.T) {\n\tt.Run(\"TestHttpPost\", func(t *testing.T) {\n\t\thttpClient := NewHTTPClient(20)\n\t\tresponse, error := httpClient.Post(\n\t\t\tcontext.TODO(),\n\t\t\t\"https://httpbin.org/post\",\n\t\t\t`{\"Username\":\"admin\", \"Password\":\"12345\"}`,\n\t\t\tmap[string]string{\"arg1\": \"value1\"},\n\t\t\tmap[string]string{\"X-Api-Key\": \"hipp-123\"},\n\t\t)\n\n\t\tpkg.Expect(t, http.StatusOK, httpClient.GetStatusCode(response))\n\t\tpkg.Expect(t, nil, error)\n\n\t\tbody, error := httpClient.ToString(response)\n\n\t\tpkg.Expect(t, true, strings.Contains(body, `\"12345\"`))\n\t\tpkg.Expect(t, true, strings.Contains(body, `\"Username\"`))\n\t\tpkg.Expect(t, true, strings.Contains(body, `\"admin\"`))\n\t\tpkg.Expect(t, true, strings.Contains(body, `\"Password\"`))\n\t\tpkg.Expect(t, true, strings.Contains(body, \"value1\"))\n\t\tpkg.Expect(t, true, strings.Contains(body, \"arg1\"))\n\t\tpkg.Expect(t, true, strings.Contains(body, \"arg1=value1\"))\n\t\tpkg.Expect(t, true, strings.Contains(body, \"X-Api-Key\"))\n\t\tpkg.Expect(t, true, strings.Contains(body, \"hipp-123\"))\n\t\tpkg.Expect(t, nil, error)\n\t})\n}\n\n// TestHttpPut test cases\nfunc TestHttpPut(t *testing.T) {\n\tt.Run(\"TestHttpPut\", func(t *testing.T) {\n\t\thttpClient := NewHTTPClient(20)\n\t\tresponse, error := httpClient.Put(\n\t\t\tcontext.TODO(),\n\t\t\t\"https://httpbin.org/put\",\n\t\t\t`{\"Username\":\"admin\", \"Password\":\"12345\"}`,\n\t\t\tmap[string]string{\"arg1\": \"value1\"},\n\t\t\tmap[string]string{\"X-Api-Key\": \"hipp-123\"},\n\t\t)\n\n\t\tpkg.Expect(t, http.StatusOK, httpClient.GetStatusCode(response))\n\t\tpkg.Expect(t, nil, error)\n\n\t\tbody, error := httpClient.ToString(response)\n\n\t\tpkg.Expect(t, true, strings.Contains(body, `\"12345\"`))\n\t\tpkg.Expect(t, true, strings.Contains(body, `\"Username\"`))\n\t\tpkg.Expect(t, true, strings.Contains(body, `\"admin\"`))\n\t\tpkg.Expect(t, true, strings.Contains(body, `\"Password\"`))\n\t\tpkg.Expect(t, true, strings.Contains(body, \"value1\"))\n\t\tpkg.Expect(t, true, strings.Contains(body, \"arg1\"))\n\t\tpkg.Expect(t, true, strings.Contains(body, \"arg1=value1\"))\n\t\tpkg.Expect(t, true, strings.Contains(body, \"X-Api-Key\"))\n\t\tpkg.Expect(t, true, strings.Contains(body, \"hipp-123\"))\n\t\tpkg.Expect(t, nil, error)\n\t})\n}\n\n// TestHttpGetStatusCode1 test cases\nfunc TestHttpGetStatusCode1(t *testing.T) {\n\tt.Run(\"TestHttpGetStatusCode1\", func(t *testing.T) {\n\t\thttpClient := NewHTTPClient(20)\n\t\tresponse, error := httpClient.Get(\n\t\t\tcontext.TODO(),\n\t\t\t\"https://httpbin.org/status/200\",\n\t\t\tmap[string]string{\"arg1\": \"value1\"},\n\t\t\tmap[string]string{\"X-Api-Key\": \"hipp-123\"},\n\t\t)\n\n\t\tpkg.Expect(t, http.StatusOK, httpClient.GetStatusCode(response))\n\t\tpkg.Expect(t, nil, error)\n\n\t\tbody, error := httpClient.ToString(response)\n\n\t\tpkg.Expect(t, \"\", body)\n\t\tpkg.Expect(t, nil, error)\n\t})\n}\n\n// TestHttpGetStatusCode2 test cases\nfunc TestHttpGetStatusCode2(t *testing.T) {\n\tt.Run(\"TestHttpGetStatusCode2\", func(t *testing.T) {\n\t\thttpClient := NewHTTPClient(20)\n\t\tresponse, error := httpClient.Get(\n\t\t\tcontext.TODO(),\n\t\t\t\"https://httpbin.org/status/500\",\n\t\t\tmap[string]string{\"arg1\": \"value1\"},\n\t\t\tmap[string]string{\"X-Api-Key\": \"hipp-123\"},\n\t\t)\n\n\t\tpkg.Expect(t, http.StatusInternalServerError, httpClient.GetStatusCode(response))\n\t\tpkg.Expect(t, nil, error)\n\n\t\tbody, error := httpClient.ToString(response)\n\n\t\tpkg.Expect(t, \"\", body)\n\t\tpkg.Expect(t, nil, error)\n\t})\n}\n\n// TestHttpGetStatusCode3 test cases\nfunc TestHttpGetStatusCode3(t *testing.T) {\n\tt.Run(\"TestHttpGetStatusCode3\", func(t *testing.T) {\n\t\thttpClient := NewHTTPClient(20)\n\t\tresponse, error := httpClient.Get(\n\t\t\tcontext.TODO(),\n\t\t\t\"https://httpbin.org/status/404\",\n\t\t\tmap[string]string{\"arg1\": \"value1\"},\n\t\t\tmap[string]string{\"X-Api-Key\": \"hipp-123\"},\n\t\t)\n\n\t\tpkg.Expect(t, http.StatusNotFound, httpClient.GetStatusCode(response))\n\t\tpkg.Expect(t, nil, error)\n\n\t\tbody, error := httpClient.ToString(response)\n\n\t\tpkg.Expect(t, \"\", body)\n\t\tpkg.Expect(t, nil, error)\n\t})\n}\n\n// TestHttpGetStatusCode4 test cases\nfunc TestHttpGetStatusCode4(t *testing.T) {\n\tt.Run(\"TestHttpGetStatusCode4\", func(t *testing.T) {\n\t\thttpClient := NewHTTPClient(20)\n\t\tresponse, error := httpClient.Get(\n\t\t\tcontext.TODO(),\n\t\t\t\"https://httpbin.org/status/201\",\n\t\t\tmap[string]string{\"arg1\": \"value1\"},\n\t\t\tmap[string]string{\"X-Api-Key\": \"hipp-123\"},\n\t\t)\n\n\t\tpkg.Expect(t, http.StatusCreated, httpClient.GetStatusCode(response))\n\t\tpkg.Expect(t, nil, error)\n\n\t\tbody, error := httpClient.ToString(response)\n\n\t\tpkg.Expect(t, \"\", body)\n\t\tpkg.Expect(t, nil, error)\n\t})\n}\n\n// TestBuildParameters test cases\nfunc TestBuildParameters(t *testing.T) {\n\tt.Run(\"TestBuildParameters\", func(t *testing.T) {\n\t\thttpClient := NewHTTPClient(20)\n\t\turl, error := httpClient.buildParameters(\"http://127.0.0.1\", map[string]string{\"arg1\": \"value1\"})\n\n\t\tpkg.Expect(t, \"http://127.0.0.1?arg1=value1\", url)\n\t\tpkg.Expect(t, nil, error)\n\t})\n}\n\n// TestBuildData test cases\nfunc TestBuildData(t *testing.T) {\n\tt.Run(\"TestBuildData\", func(t *testing.T) {\n\t\thttpClient := NewHTTPClient(20)\n\t\tpkg.Expect(t, httpClient.BuildData(map[string]string{}), \"\")\n\t\tpkg.Expect(t, httpClient.BuildData(map[string]string{\"arg1\": \"value1\"}), \"arg1=value1\")\n\t})\n}\n"
  },
  {
    "path": "core/module/prometheus.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage module\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/clivern/beetle/core/model\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// Prometheus struct\ntype Prometheus struct{}\n\n// NewPrometheus create a new instance of prometheus backend\nfunc NewPrometheus() *Prometheus {\n\treturn &Prometheus{}\n}\n\n// Send sends metrics to prometheus\nfunc (p *Prometheus) Send(metrics []model.Metric) error {\n\tlog.Info(fmt.Sprintf(\n\t\t\"Send %d metrics to prometheus backend\",\n\t\tlen(metrics),\n\t))\n\n\tfor _, metric := range metrics {\n\t\tswitch metric.Type {\n\t\tcase model.COUNTER:\n\t\t\tp.Counter(metric)\n\n\t\tcase model.GAUGE:\n\t\t\tp.Gauge(metric)\n\n\t\tcase model.HISTOGRAM:\n\t\t\tp.Histogram(metric)\n\n\t\tcase model.SUMMARY:\n\t\t\tp.Summary(metric)\n\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"metric with type %s not implemented yet\", metric.Type)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Summary updates or creates a summary\nfunc (p *Prometheus) Summary(item model.Metric) error {\n\tvar metric prometheus.Summary\n\n\tvalue, _ := item.GetValueAsFloat()\n\n\topts := prometheus.SummaryOpts{\n\t\tName: item.Name,\n\t\tHelp: item.Help,\n\t}\n\tif len(item.Labels) > 0 {\n\t\tvec := prometheus.NewSummaryVec(opts, item.LabelKeys())\n\t\terr := prometheus.Register(vec)\n\t\tif err != nil {\n\t\t\tif are, ok := err.(prometheus.AlreadyRegisteredError); ok {\n\t\t\t\tvec = are.ExistingCollector.(*prometheus.SummaryVec)\n\t\t\t} else {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tmetric = vec.With(item.Labels).(prometheus.Summary)\n\t} else {\n\t\tmetric = prometheus.NewSummary(opts)\n\t\terr := prometheus.Register(metric)\n\t\tif err != nil {\n\t\t\tif are, ok := err.(prometheus.AlreadyRegisteredError); ok {\n\t\t\t\tmetric = are.ExistingCollector.(prometheus.Summary)\n\t\t\t} else {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tif item.Method == \"observe\" {\n\t\tmetric.Observe(value)\n\t} else {\n\t\treturn fmt.Errorf(\"method %s is not implemented yet\", item.Method)\n\t}\n\n\treturn nil\n}\n\n// Counter updates or creates a counter\nfunc (p *Prometheus) Counter(item model.Metric) error {\n\tvar metric prometheus.Counter\n\n\tvalue, _ := item.GetValueAsFloat()\n\n\topts := prometheus.CounterOpts{\n\t\tName: item.Name,\n\t\tHelp: item.Help,\n\t}\n\n\tif len(item.Labels) > 0 {\n\t\tvec := prometheus.NewCounterVec(opts, item.LabelKeys())\n\n\t\terr := prometheus.Register(vec)\n\n\t\tif err != nil {\n\t\t\tif are, ok := err.(prometheus.AlreadyRegisteredError); ok {\n\t\t\t\tvec = are.ExistingCollector.(*prometheus.CounterVec)\n\t\t\t} else {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tmetric = vec.With(item.Labels)\n\t} else {\n\t\tmetric = prometheus.NewCounter(opts)\n\t\terr := prometheus.Register(metric)\n\t\tif err != nil {\n\t\t\tif are, ok := err.(prometheus.AlreadyRegisteredError); ok {\n\t\t\t\tmetric = are.ExistingCollector.(prometheus.Counter)\n\t\t\t} else {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tswitch item.Method {\n\tcase \"inc\":\n\t\tmetric.Inc()\n\tcase \"add\":\n\t\tmetric.Add(value)\n\tdefault:\n\t\treturn fmt.Errorf(\"method %s is not implemented yet\", item.Method)\n\t}\n\n\treturn nil\n}\n\n// Histogram updates or creates a histogram\nfunc (p *Prometheus) Histogram(item model.Metric) error {\n\tvar metric prometheus.Histogram\n\n\tvalue, _ := item.GetValueAsFloat()\n\n\topts := prometheus.HistogramOpts{\n\t\tName:    item.Name,\n\t\tHelp:    item.Help,\n\t\tBuckets: item.Buckets,\n\t}\n\n\tif len(item.Labels) > 0 {\n\t\tvec := prometheus.NewHistogramVec(opts, item.LabelKeys())\n\t\terr := prometheus.Register(vec)\n\t\tif err != nil {\n\t\t\tif are, ok := err.(prometheus.AlreadyRegisteredError); ok {\n\t\t\t\tvec = are.ExistingCollector.(*prometheus.HistogramVec)\n\t\t\t} else {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tmetric = vec.With(item.Labels).(prometheus.Histogram)\n\t} else {\n\t\tmetric = prometheus.NewHistogram(opts)\n\t\terr := prometheus.Register(metric)\n\t\tif err != nil {\n\t\t\tif are, ok := err.(prometheus.AlreadyRegisteredError); ok {\n\t\t\t\tmetric = are.ExistingCollector.(prometheus.Histogram)\n\t\t\t} else {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tif item.Method == \"observe\" {\n\t\tmetric.Observe(value)\n\t} else {\n\t\treturn fmt.Errorf(\"method %s is not implemented yet\", item.Method)\n\t}\n\n\treturn nil\n}\n\n// Gauge updates or creates a gauge\nfunc (p *Prometheus) Gauge(item model.Metric) error {\n\tvar metric prometheus.Gauge\n\n\tvalue, _ := item.GetValueAsFloat()\n\n\topts := prometheus.GaugeOpts{\n\t\tName: item.Name,\n\t\tHelp: item.Help,\n\t}\n\tif len(item.Labels) > 0 {\n\t\tvec := prometheus.NewGaugeVec(opts, item.LabelKeys())\n\t\terr := prometheus.Register(vec)\n\t\tif err != nil {\n\t\t\tif are, ok := err.(prometheus.AlreadyRegisteredError); ok {\n\t\t\t\tvec = are.ExistingCollector.(*prometheus.GaugeVec)\n\t\t\t} else {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tmetric = vec.With(item.Labels)\n\t} else {\n\t\tmetric = prometheus.NewGauge(opts)\n\t\terr := prometheus.Register(metric)\n\t\tif err != nil {\n\t\t\tif are, ok := err.(prometheus.AlreadyRegisteredError); ok {\n\t\t\t\tmetric = are.ExistingCollector.(prometheus.Gauge)\n\t\t\t} else {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tswitch item.Method {\n\tcase \"set\":\n\t\tmetric.Set(value)\n\tcase \"inc\":\n\t\tmetric.Inc()\n\tcase \"dec\":\n\t\tmetric.Dec()\n\tcase \"add\":\n\t\tmetric.Add(value)\n\tcase \"sub\":\n\t\tmetric.Sub(value)\n\tdefault:\n\t\treturn fmt.Errorf(\"method %s is not implemented yet\", item.Method)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "core/module/remote.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage module\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\n// ReleaseURL remote release URL\nconst ReleaseURL = \"https://api.github.com/repos/Clivern/Beetle/releases/latest\"\n\n// LatestRelease struct\ntype LatestRelease struct {\n\tName    string `json:\"name\"`\n\tTagName string `json:\"tag_name\"`\n}\n\n// LoadFromJSON update object from json\nfunc (lr *LatestRelease) LoadFromJSON(data []byte) (bool, error) {\n\terr := json.Unmarshal(data, &lr)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// ConvertToJSON convert object to json\nfunc (lr *LatestRelease) ConvertToJSON() (string, error) {\n\tdata, err := json.Marshal(&lr)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(data), nil\n}\n\n// GetLatestRelease gets the latest beetle release\nfunc GetLatestRelease() (LatestRelease, error) {\n\tresult := LatestRelease{}\n\n\thttpClient := NewHTTPClient(20)\n\n\tresponse, err := httpClient.Get(\n\t\tcontext.TODO(),\n\t\tReleaseURL,\n\t\tmap[string]string{},\n\t\tmap[string]string{},\n\t)\n\n\tif http.StatusOK != httpClient.GetStatusCode(response) || err != nil {\n\t\treturn result, fmt.Errorf(\"Error: Unable to fetch latest release\")\n\t}\n\n\tbody, err := httpClient.ToString(response)\n\n\tif err != nil {\n\t\treturn result, fmt.Errorf(\"Error: Unable to fetch latest release\")\n\t}\n\n\tok, err := result.LoadFromJSON([]byte(body))\n\n\tif !ok || err != nil {\n\t\treturn result, fmt.Errorf(\"Error: Invalid remote response\")\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "core/module/remote_test.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage module\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/clivern/beetle/pkg\"\n)\n\n// TestRemote test cases\nfunc TestRemote(t *testing.T) {\n\tt.Run(\"TestRemote\", func(t *testing.T) {\n\t\tresult, err := GetLatestRelease()\n\t\tpkg.Expect(t, true, strings.Contains(result.Name, \".\"))\n\t\tpkg.Expect(t, true, strings.Contains(result.TagName, \".\"))\n\t\tpkg.Expect(t, nil, err)\n\t})\n}\n"
  },
  {
    "path": "core/util/helpers.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage util\n\nimport (\n\t\"encoding/json\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"strings\"\n\n\t\"github.com/satori/go.uuid\"\n)\n\n// InArray check if value is on array\nfunc InArray(val interface{}, array interface{}) bool {\n\tswitch reflect.TypeOf(array).Kind() {\n\tcase reflect.Slice:\n\t\ts := reflect.ValueOf(array)\n\n\t\tfor i := 0; i < s.Len(); i++ {\n\t\t\tif reflect.DeepEqual(val, s.Index(i).Interface()) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false\n}\n\n// GenerateUUID4 create a UUID\nfunc GenerateUUID4() string {\n\tu := uuid.Must(uuid.NewV4(), nil)\n\treturn u.String()\n}\n\n// ListFiles lists all files inside a dir\nfunc ListFiles(basePath string) []string {\n\tvar files []string\n\n\terr := filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error {\n\t\tif basePath != path && !info.IsDir() {\n\t\t\tfiles = append(files, path)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn files\n\t}\n\n\treturn files\n}\n\n// ReadFile get the file content\nfunc ReadFile(path string) string {\n\tdata, err := ioutil.ReadFile(path)\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\treturn string(data)\n}\n\n// FilterFiles filters files list based on specific sub-strings\nfunc FilterFiles(files, filters []string) []string {\n\tvar filteredFiles []string\n\n\tfor _, file := range files {\n\t\tok := true\n\t\tfor _, filter := range filters {\n\n\t\t\tok = ok && strings.Contains(file, filter)\n\t\t}\n\t\tif ok {\n\t\t\tfilteredFiles = append(filteredFiles, file)\n\t\t}\n\t}\n\n\treturn filteredFiles\n}\n\n// Unset remove element at position i\nfunc Unset(a []string, i int) []string {\n\ta[i] = a[len(a)-1]\n\ta[len(a)-1] = \"\"\n\treturn a[:len(a)-1]\n}\n\n// ConvertToJSON convert object to json\nfunc ConvertToJSON(val interface{}) (string, error) {\n\tdata, err := json.Marshal(val)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(data), nil\n}\n"
  },
  {
    "path": "core/util/helpers_test.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage util\n\nimport (\n\t\"testing\"\n\n\t\"github.com/clivern/beetle/core/model\"\n\t\"github.com/clivern/beetle/pkg\"\n)\n\n// TestInArray test cases\nfunc TestInArray(t *testing.T) {\n\t// TestInArray\n\tt.Run(\"TestInArray\", func(t *testing.T) {\n\t\tpkg.Expect(t, InArray(\"A\", []string{\"A\", \"B\", \"C\", \"D\"}), true)\n\t\tpkg.Expect(t, InArray(\"B\", []string{\"A\", \"B\", \"C\", \"D\"}), true)\n\t\tpkg.Expect(t, InArray(\"H\", []string{\"A\", \"B\", \"C\", \"D\"}), false)\n\t\tpkg.Expect(t, InArray(1, []int{2, 3, 1}), true)\n\t\tpkg.Expect(t, InArray(9, []int{2, 3, 1}), false)\n\n\t\tpayload := []model.PatchStringValue{\n\t\t\tmodel.PatchStringValue{\n\t\t\t\tOp:    \"replace\",\n\t\t\t\tPath:  \"/spec/template/spec/containers/0/image\",\n\t\t\t\tValue: \"clivern/toad:release-0.2.4\",\n\t\t\t},\n\t\t}\n\n\t\tdata, err := ConvertToJSON(payload)\n\n\t\tpkg.Expect(t, data, `[{\"op\":\"replace\",\"path\":\"/spec/template/spec/containers/0/image\",\"value\":\"clivern/toad:release-0.2.4\"}]`)\n\t\tpkg.Expect(t, err, nil)\n\t})\n}\n"
  },
  {
    "path": "deployment/docker/README.md",
    "content": "⚠️ This only to test beetle with prometheus and grafana."
  },
  {
    "path": "deployment/docker/docker-compose.yml",
    "content": "version: '3'\n\nservices:\n\n    # Redis Service\n    redis:\n        image: 'redis:7.2-alpine'\n        volumes:\n            - 'redis_data:/data'\n        ports:\n            - '6379:6379'\n        restart: always\n\n    # Prometheus Service\n    prometheus:\n        image: 'prom/prometheus:v2.53.0'\n        volumes:\n            - './:/etc/prometheus'\n        command: '--config.file=/etc/prometheus/prometheus.yml'\n        ports:\n            - '9090:9090'\n        restart: always\n\n    # Grafana Service\n    grafana:\n        image: 'grafana/grafana:9.5.20'\n        environment:\n            - GF_SECURITY_ADMIN_USER=${ADMIN_USER:-admin}\n            - GF_SECURITY_ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin}\n            - GF_USERS_ALLOW_SIGN_UP=false\n        ports:\n            - '3000:3000'\n        depends_on:\n            - prometheus\n        restart: always\n\nvolumes:\n    redis_data: null"
  },
  {
    "path": "deployment/docker/prometheus.yml",
    "content": "# my global config\nglobal:\n  evaluation_interval: 15s\n  scrape_interval: 15s\nrule_files: ~\nscrape_configs:\n  -\n    job_name: prometheus\n    scrape_interval: 5s\n    static_configs:\n      -\n        targets:\n          - \"localhost:9090\"\n  -\n    job_name: beetle\n    metrics_path: /metrics\n    scrape_interval: 5s\n    static_configs:\n      -\n        targets:\n          - \"xx.ngrok.io\""
  },
  {
    "path": "deployment/k8s/incluster/README.md",
    "content": "## Running Beetle inside Kubernetes Cluster\n\n```bash\n$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.41.2/deploy/static/provider/cloud/deploy.yaml\n$ kubectl get pods -n ingress-nginx \\\n  -l app.kubernetes.io/name=ingress-nginx --watch\n$ kubectl get svc --namespace=ingress-nginx\n```\n\nUpdate `beetle.yaml` with the database credentials. The following part inside the file\n\n```bash\n....\n\n# Application Database\ndatabase:\n    # Database driver (sqlite3, mysql)\n    driver: ${BEETLE_DATABASE_DRIVER:-mysql}\n    # Hostname\n    host: ${BEETLE_DATABASE_MYSQL_HOST:-REPLACE_WITH_MYSQL_HOSTNAME}\n    # Port\n    port: ${BEETLE_DATABASE_MYSQL_PORT:-3306}\n    # Database\n    name: ${BEETLE_DATABASE_MYSQL_DATABASE:-REPLACE_WITH_MYSQL_DATABASE}\n    # Username\n    username: ${BEETLE_DATABASE_MYSQL_USERNAME:-REPLACE_WITH_MYSQL_USERNAME}\n    # Password\n    password: ${BEETLE_DATABASE_MYSQL_PASSWORD:-REPLACE_WITH_MYSQL_PASSWORD}\n....\n```\n\nDeploy a sample application and Beetle API server\n\n```bash\n$ kubectl apply -f sample_app.yaml --record\n$ kubectl apply -f beetle.yaml --record\n\n$ kubectl get ingress\n\n# Update /etc/hosts with the ingress IP\n# 167.x.x.x    example.com\n\n$ kubectl describe ingress toad-ing\n$ kubectl describe ingress beetle-ing\n\n$ curl http://example.com/toad/_ready\n$ curl http://example.com/beetle/_ready\n```\n\nInteract with Beetle API server\n\n```bash\n# Get clusters\n$ curl http://example.com/beetle/api/v1/cluster -H \"X-API-KEY: 1234\" -s | jq .\n\n# Get cluster\n$ curl http://example.com/beetle/api/v1/cluster/production -H \"X-API-KEY: 1234\" -s | jq .\n\n# Get cluster namespaces\n$ curl http://example.com/beetle/api/v1/cluster/production/namespace -H \"X-API-KEY: 1234\" -s | jq .\n\n# Get namespace\n$ curl http://example.com/beetle/api/v1/cluster/production/namespace/default -H \"X-API-KEY: 1234\" -s | jq .\n\n# Get namespace applications\n$ curl http://example.com/beetle/api/v1/cluster/production/namespace/default/app -H \"X-API-KEY: 1234\" -s | jq .\n\n# Get application `toad`\n$ curl http://example.com/beetle/api/v1/cluster/production/namespace/default/app/toad -H \"X-API-KEY: 1234\" -s | jq .\n\n# Get Async Jobs\n$ curl -X GET http://example.com/beetle/api/v1/job -H \"X-API-KEY: 1234\" -s | jq .\n\n# Deploy a new version with recreate strategy\n$ curl -X POST \\\n     -H \"X-API-KEY: 1234\" \\\n     -d '{\"version\":\"0.2.4\",\"strategy\":\"recreate\"}' \\\n     http://example.com/beetle/api/v1/cluster/production/namespace/default/app/toad/deployment\n\n# Get application `toad` version\n$ curl http://example.com/beetle/api/v1/cluster/production/namespace/default/app/toad -H \"X-API-KEY: 1234\" -s | jq .\n\n# Another deployment with ramped strategy\n$ curl -X POST \\\n     -H \"X-API-KEY: 1234\" \\\n     -d '{\"version\":\"0.2.3\",\"strategy\":\"ramped\", \"maxSurge\": \"1\", \"maxUnavailable\": \"0\"}' \\\n     http://example.com/beetle/api/v1/cluster/production/namespace/default/app/toad/deployment\n```\n"
  },
  {
    "path": "deployment/k8s/incluster/beetle.yaml",
    "content": "---\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: beetle-service-account\n  namespace: default\n\n\n---\napiVersion: rbac.authorization.k8s.io/v1beta1\nkind: ClusterRole\nmetadata:\n  name: beetle-service-account\n  namespace: default\nrules:\n- apiGroups: [\"\"]\n  resources: [\"namespaces\"]\n  verbs: [\"get\", \"list\"]\n- apiGroups: [\"\"]\n  resources: [\"nodes\"]\n  verbs: [\"get\", \"list\", \"watch\"]\n- apiGroups: [\"\"]\n  resources: [\"pods\"]\n  verbs: [\"get\", \"list\", \"watch\"]\n- apiGroups: [\"\"]\n  resources: [\"configmaps\"]\n  verbs: [\"get\", \"list\"]\n- apiGroups: [\"extensions\", \"apps\"]\n  resources: [\"deployments\"]\n  verbs: [\"get\", \"list\", \"watch\", \"create\", \"update\", \"patch\", \"delete\"]\n\n\n---\nkind: ClusterRoleBinding\napiVersion: rbac.authorization.k8s.io/v1beta1\nmetadata:\n  name: beetle-service-account\nroleRef:\n  kind: ClusterRole\n  name: beetle-service-account\n  apiGroup: rbac.authorization.k8s.io\nsubjects:\n- kind: ServiceAccount\n  name: beetle-service-account\n  namespace: default\n\n\n---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: incluster-beetle-configs\n  namespace: default\ndata:\n  config.dist.yml: |-\n    ---\n    # App configs\n    app:\n        # Env mode (dev or prod)\n        mode: ${BEETLE_APP_MODE:-prod}\n        # HTTP port\n        port: ${BEETLE_API_PORT:-8080}\n        # App URL\n        domain: ${BEETLE_APP_DOMAIN:-http://127.0.0.1:8080}\n        # TLS configs\n        tls:\n            status: ${BEETLE_API_TLS_STATUS:-off}\n            pemPath: ${BEETLE_API_TLS_PEMPATH:-cert/server.pem}\n            keyPath: ${BEETLE_API_TLS_KEYPATH:-cert/server.key}\n\n        # Message Broker Configs\n        broker:\n            # Broker driver (native)\n            driver: ${BEETLE_BROKER_DRIVER:-native}\n            # Native driver configs\n            native:\n                # Queue max capacity\n                capacity: ${BEETLE_BROKER_NATIVE_CAPACITY:-5000}\n                # Number of concurrent workers\n                workers: ${BEETLE_BROKER_NATIVE_WORKERS:-4}\n\n        # API Configs\n        api:\n            key: ${BEETLE_API_KEY:-1234}\n\n        # Runtime, Requests/Response and Beetle Metrics\n        metrics:\n            prometheus:\n                # Route for the metrics endpoint\n                endpoint: ${BEETLE_METRICS_PROM_ENDPOINT:-/metrics}\n\n        # Application Database\n        database:\n            # Database driver (sqlite3, mysql)\n            driver: ${BEETLE_DATABASE_DRIVER:-mysql}\n            # Hostname\n            host: ${BEETLE_DATABASE_MYSQL_HOST:-REPLACE_WITH_MYSQL_HOSTNAME}\n            # Port\n            port: ${BEETLE_DATABASE_MYSQL_PORT:-3306}\n            # Database\n            name: ${BEETLE_DATABASE_MYSQL_DATABASE:-REPLACE_WITH_MYSQL_DATABASE}\n            # Username\n            username: ${BEETLE_DATABASE_MYSQL_USERNAME:-REPLACE_WITH_MYSQL_USERNAME}\n            # Password\n            password: ${BEETLE_DATABASE_MYSQL_PASSWORD:-REPLACE_WITH_MYSQL_PASSWORD}\n\n        # Kubernetes Clusters\n        clusters:\n            -\n                name: ${BEETLE_KUBE_CLUSTER_01_NAME:-production}\n                inCluster: ${BEETLE_KUBE_CLUSTER_01_IN_CLUSTER:-true}\n                kubeconfig: ${BEETLE_KUBE_CLUSTER_01_CONFIG_FILE:- }\n\n        # HTTP Webhook\n        webhook:\n            url: ${BEETLE_WEBHOOK_URL:-https://httpbin.org/anything}\n            retry: ${BEETLE_WEBHOOK_RETRY:-3}\n            apiKey: ${BEETLE_WEBHOOK_API_KEY:-12345}\n\n    # Log configs\n    log:\n        # Log level, it can be debug, info, warn, error, panic, fatal\n        level: ${BEETLE_LOG_LEVEL:-info}\n        # output can be stdout or abs path to log file /var/logs/beetle.log\n        output: ${BEETLE_LOG_OUTPUT:-stdout}\n        # Format can be json\n        format: ${BEETLE_LOG_FORMAT:-json}\n\n\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: beetle-deployment\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: beetle\n  template:\n    metadata:\n      labels:\n        app: beetle\n      name: beetle\n    spec:\n      serviceAccount: beetle-service-account\n      serviceAccountName: beetle-service-account\n      containers:\n        -\n          image: \"clivern/beetle:1.0.2\"\n          name: beetle-app\n          volumeMounts:\n            -\n              mountPath: /app/configs\n              name: incluster-beetle-configs-volume\n      volumes:\n        -\n          configMap:\n            name: incluster-beetle-configs\n          name: incluster-beetle-configs-volume\n\n\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: beetle-svc\n  labels:\n    app: beetle\nspec:\n  ports:\n    -\n      port: 80\n      targetPort: 8080\n  selector:\n    app: beetle\n  type: LoadBalancer\n\n\n---\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  annotations:\n    # example.com/beetle rewrites to example.com/\n    # example.com/beetle/ rewrites to example.com/\n    # example.com/beetle/_ready rewrites to example.com/_ready\n    nginx.ingress.kubernetes.io/rewrite-target: /$2\n  name: beetle-ing\nspec:\n  rules:\n  - host: example.com\n    http:\n      paths:\n      - path: /beetle(/|$)(.*)\n        pathType: Prefix\n        backend:\n          service:\n            name: beetle-svc\n            port:\n              number: 80\n"
  },
  {
    "path": "deployment/k8s/incluster/sample_app.yaml",
    "content": "---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  labels:\n    beetle.clivern.com/status: enabled\n    beetle.clivern.com/application-id: toad\n  annotations:\n    beetle.clivern.com/application-name: Toad\n    beetle.clivern.com/image-format: \"clivern/toad:release-[.Release]\"\n  name: toad-deployment\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: toad\n  template:\n    metadata:\n      labels:\n        app: toad\n      name: toad\n    spec:\n      containers:\n        -\n          image: \"clivern/toad:release-0.2.3\"\n          name: toad-app\n\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: toad-svc\n  labels:\n    app: toad\nspec:\n  ports:\n    -\n      port: 80\n      targetPort: 8080\n  selector:\n    app: toad\n  type: LoadBalancer\n\n\n---\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  annotations:\n    # example.com/toad rewrites to example.com/\n    # example.com/toad/ rewrites to example.com/\n    # example.com/toad/_ready rewrites to example.com/_ready\n    nginx.ingress.kubernetes.io/rewrite-target: /$2\n  name: toad-ing\nspec:\n  rules:\n  - host: example.com\n    http:\n      paths:\n      - path: /toad(/|$)(.*)\n        pathType: Prefix\n        backend:\n          service:\n            name: toad-svc\n            port:\n              number: 80\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/clivern/beetle\n\ngo 1.20\n\nrequire (\n\tgithub.com/briandowns/spinner v1.23.0\n\tgithub.com/drone/envsubst v1.0.3\n\tgithub.com/gin-gonic/gin v1.10.0\n\tgithub.com/jinzhu/gorm v1.9.16\n\tgithub.com/logrusorgru/aurora/v3 v3.0.0\n\tgithub.com/olekukonko/tablewriter v0.0.5\n\tgithub.com/prometheus/client_golang v1.18.0\n\tgithub.com/satori/go.uuid v1.2.0\n\tgithub.com/sirupsen/logrus v1.9.3\n\tgithub.com/spf13/cobra v1.8.1\n\tgithub.com/spf13/viper v1.18.2\n\tk8s.io/api v0.27.4\n\tk8s.io/apimachinery v0.27.4\n\tk8s.io/client-go v0.27.4\n)\n\nrequire (\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/bytedance/sonic v1.11.6 // indirect\n\tgithub.com/bytedance/sonic/loader v0.1.1 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.2.0 // indirect\n\tgithub.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect\n\tgithub.com/cloudwego/base64x v0.1.4 // indirect\n\tgithub.com/cloudwego/iasm v0.2.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/emicklei/go-restful/v3 v3.9.0 // indirect\n\tgithub.com/evanphx/json-patch v4.12.0+incompatible // indirect\n\tgithub.com/fatih/color v1.14.1 // indirect\n\tgithub.com/fsnotify/fsnotify v1.7.0 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.3 // indirect\n\tgithub.com/gin-contrib/sse v0.1.0 // indirect\n\tgithub.com/go-logr/logr v1.2.3 // indirect\n\tgithub.com/go-openapi/jsonpointer v0.19.6 // indirect\n\tgithub.com/go-openapi/jsonreference v0.20.1 // indirect\n\tgithub.com/go-openapi/swag v0.22.3 // indirect\n\tgithub.com/go-playground/locales v0.14.1 // indirect\n\tgithub.com/go-playground/universal-translator v0.18.1 // indirect\n\tgithub.com/go-playground/validator/v10 v10.20.0 // indirect\n\tgithub.com/go-sql-driver/mysql v1.5.0 // indirect\n\tgithub.com/goccy/go-json v0.10.2 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/protobuf v1.5.3 // indirect\n\tgithub.com/google/gnostic v0.5.7-v3refs // indirect\n\tgithub.com/google/go-cmp v0.5.9 // indirect\n\tgithub.com/google/gofuzz v1.1.0 // indirect\n\tgithub.com/google/uuid v1.4.0 // indirect\n\tgithub.com/hashicorp/hcl v1.0.0 // indirect\n\tgithub.com/imdario/mergo v0.3.6 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/jinzhu/inflection v1.0.0 // indirect\n\tgithub.com/josharian/intern v1.0.0 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.2.7 // indirect\n\tgithub.com/leodido/go-urn v1.4.0 // indirect\n\tgithub.com/magiconair/properties v1.8.7 // indirect\n\tgithub.com/mailru/easyjson v0.7.7 // indirect\n\tgithub.com/mattn/go-colorable v0.1.13 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.9 // indirect\n\tgithub.com/mattn/go-sqlite3 v1.14.0 // indirect\n\tgithub.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect\n\tgithub.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect\n\tgithub.com/mitchellh/mapstructure v1.5.0 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.2 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/prometheus/client_model v0.5.0 // indirect\n\tgithub.com/prometheus/common v0.45.0 // indirect\n\tgithub.com/prometheus/procfs v0.12.0 // indirect\n\tgithub.com/sagikazarmark/locafero v0.4.0 // indirect\n\tgithub.com/sagikazarmark/slog-shim v0.1.0 // indirect\n\tgithub.com/sourcegraph/conc v0.3.0 // indirect\n\tgithub.com/spf13/afero v1.11.0 // indirect\n\tgithub.com/spf13/cast v1.6.0 // indirect\n\tgithub.com/spf13/jwalterweatherman v1.1.0 // indirect\n\tgithub.com/spf13/pflag v1.0.5 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgithub.com/twitchyliquid64/golang-asm v0.15.1 // indirect\n\tgithub.com/ugorji/go/codec v1.2.12 // indirect\n\tgo.uber.org/atomic v1.9.0 // indirect\n\tgo.uber.org/multierr v1.9.0 // indirect\n\tgolang.org/x/arch v0.8.0 // indirect\n\tgolang.org/x/crypto v0.23.0 // indirect\n\tgolang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect\n\tgolang.org/x/net v0.25.0 // indirect\n\tgolang.org/x/oauth2 v0.15.0 // indirect\n\tgolang.org/x/sys v0.20.0 // indirect\n\tgolang.org/x/term v0.20.0 // indirect\n\tgolang.org/x/text v0.15.0 // indirect\n\tgolang.org/x/time v0.5.0 // indirect\n\tgoogle.golang.org/appengine v1.6.7 // indirect\n\tgoogle.golang.org/protobuf v1.34.1 // indirect\n\tgopkg.in/inf.v0 v0.9.1 // indirect\n\tgopkg.in/ini.v1 v1.67.0 // indirect\n\tgopkg.in/yaml.v2 v2.4.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\tk8s.io/klog/v2 v2.90.1 // indirect\n\tk8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect\n\tk8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect\n\tsigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect\n\tsigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect\n\tsigs.k8s.io/yaml v1.3.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=\ncloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=\ncloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ncloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=\ngithub.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A=\ngithub.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE=\ngithub.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=\ngithub.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=\ngithub.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=\ngithub.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=\ngithub.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=\ngithub.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=\ngithub.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=\ngithub.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=\ngithub.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=\ngithub.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=\ngithub.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=\ngithub.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=\ngithub.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=\ngithub.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=\ngithub.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=\ngithub.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g=\ngithub.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g=\ngithub.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE=\ngithub.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=\ngithub.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=\ngithub.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=\ngithub.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=\ngithub.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=\ngithub.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=\ngithub.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=\ngithub.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=\ngithub.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=\ngithub.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=\ngithub.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=\ngithub.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=\ngithub.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=\ngithub.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=\ngithub.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=\ngithub.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=\ngithub.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=\ngithub.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=\ngithub.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=\ngithub.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=\ngithub.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=\ngithub.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=\ngithub.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=\ngithub.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=\ngithub.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=\ngithub.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8=\ngithub.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=\ngithub.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=\ngithub.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=\ngithub.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=\ngithub.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=\ngithub.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=\ngithub.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=\ngithub.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=\ngithub.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=\ngithub.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=\ngithub.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=\ngithub.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=\ngithub.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=\ngithub.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=\ngithub.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=\ngithub.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=\ngithub.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=\ngithub.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=\ngithub.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54=\ngithub.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=\ngithub.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=\ngithub.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=\ngithub.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=\ngithub.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=\ngithub.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=\ngithub.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=\ngithub.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=\ngithub.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=\ngithub.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=\ngithub.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=\ngithub.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=\ngithub.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=\ngithub.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=\ngithub.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=\ngithub.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=\ngithub.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=\ngithub.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=\ngithub.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=\ngithub.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=\ngithub.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=\ngithub.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/logrusorgru/aurora/v3 v3.0.0 h1:R6zcoZZbvVcGMvDCKo45A9U/lzYyzl5NfYIvznmDfE4=\ngithub.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc=\ngithub.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=\ngithub.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=\ngithub.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=\ngithub.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\ngithub.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=\ngithub.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=\ngithub.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=\ngithub.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=\ngithub.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=\ngithub.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=\ngithub.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=\ngithub.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=\ngithub.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=\ngithub.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=\ngithub.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=\ngithub.com/onsi/ginkgo/v2 v2.9.1 h1:zie5Ly042PD3bsCvsSOPvRnFwyo3rKe64TJlD6nu0mk=\ngithub.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E=\ngithub.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=\ngithub.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=\ngithub.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=\ngithub.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=\ngithub.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=\ngithub.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=\ngithub.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=\ngithub.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=\ngithub.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=\ngithub.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=\ngithub.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=\ngithub.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=\ngithub.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=\ngithub.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=\ngithub.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=\ngithub.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=\ngithub.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=\ngithub.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=\ngithub.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=\ngithub.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=\ngithub.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=\ngithub.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=\ngithub.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=\ngithub.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=\ngithub.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=\ngithub.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=\ngithub.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=\ngithub.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=\ngithub.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=\ngithub.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=\ngithub.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=\ngithub.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=\ngithub.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=\ngithub.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=\ngithub.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=\ngithub.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=\ngithub.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=\ngithub.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=\ngithub.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=\ngithub.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=\ngithub.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=\ngithub.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=\ngithub.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=\ngithub.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=\ngithub.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=\ngithub.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=\ngithub.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=\ngithub.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=\ngithub.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=\ngithub.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=\ngithub.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=\ngithub.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=\ngithub.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=\ngithub.com/spf13/viper v1.18.1 h1:rmuU42rScKWlhhJDyXZRKJQHXFX02chSVW1IvkPGiVM=\ngithub.com/spf13/viper v1.18.1/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=\ngithub.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=\ngithub.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=\ngithub.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=\ngithub.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=\ngithub.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=\ngithub.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=\ngithub.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=\ngithub.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=\ngithub.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=\ngithub.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=\ngithub.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=\ngo.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=\ngo.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=\ngo.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=\ngolang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=\ngolang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=\ngolang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=\ngolang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=\ngolang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=\ngolang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=\ngolang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=\ngolang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=\ngolang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=\ngolang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=\ngolang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=\ngolang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=\ngolang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=\ngolang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=\ngolang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=\ngolang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=\ngolang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=\ngolang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=\ngolang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=\ngolang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=\ngolang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=\ngolang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=\ngolang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=\ngolang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=\ngolang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=\ngolang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=\ngolang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=\ngolang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=\ngolang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=\ngolang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=\ngolang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=\ngolang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=\ngolang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=\ngolang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=\ngolang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=\ngolang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=\ngolang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=\ngoogle.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=\ngoogle.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=\ngoogle.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=\ngoogle.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=\ngoogle.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=\ngoogle.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=\ngoogle.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=\ngopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=\ngopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=\ngopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nk8s.io/api v0.27.4 h1:0pCo/AN9hONazBKlNUdhQymmnfLRbSZjd5H5H3f0bSs=\nk8s.io/api v0.27.4/go.mod h1:O3smaaX15NfxjzILfiln1D8Z3+gEYpjEpiNA/1EVK1Y=\nk8s.io/apimachinery v0.27.4 h1:CdxflD4AF61yewuid0fLl6bM4a3q04jWel0IlP+aYjs=\nk8s.io/apimachinery v0.27.4/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E=\nk8s.io/client-go v0.27.4 h1:vj2YTtSJ6J4KxaC88P4pMPEQECWMY8gqPqsTgUKzvjk=\nk8s.io/client-go v0.27.4/go.mod h1:ragcly7lUlN0SRPk5/ZkGnDjPknzb37TICq07WhI6Xc=\nk8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw=\nk8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=\nk8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg=\nk8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg=\nk8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY=\nk8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=\nnullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\nsigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=\nsigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=\nsigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=\nsigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=\nsigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=\nsigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=\n"
  },
  {
    "path": "pkg/expect.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage pkg\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\n// Expect compare two values for testing\nfunc Expect(t *testing.T, got, want interface{}) {\n\tt.Logf(`Comparing values %v, %v`, got, want)\n\n\tif !reflect.DeepEqual(got, want) {\n\t\tt.Errorf(`got %v, want %v`, got, want)\n\t}\n}\n"
  },
  {
    "path": "pkg/server_mock.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage pkg\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n)\n\n// ServerMock mocks http server\nfunc ServerMock(uri, response string, statusCode int) *httptest.Server {\n\thandler := http.NewServeMux()\n\thandler.HandleFunc(uri, func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(statusCode)\n\t\tw.Write([]byte(response))\n\t})\n\n\tsrv := httptest.NewServer(handler)\n\n\treturn srv\n}\n"
  },
  {
    "path": "renovate.json",
    "content": "{\n    \"extends\": [\n        \"config:base\"\n    ]\n}\n"
  },
  {
    "path": "sdk/application.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage sdk\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/clivern/beetle/core/model\"\n)\n\n// GetApplications Get Applications List\nfunc (c *Client) GetApplications(ctx context.Context, cluster, namespace string) (model.Applications, error) {\n\tvar result model.Applications\n\n\tresponse, err := c.HTTPClient.Get(\n\t\tctx,\n\t\tfmt.Sprintf(\"%s/api/v1/cluster/%s/namespace/%s/app\", c.APIURL, cluster, namespace),\n\t\tmap[string]string{},\n\t\tmap[string]string{\"X-API-KEY\": c.APIKey},\n\t)\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tstatusCode := c.HTTPClient.GetStatusCode(response)\n\n\tif statusCode != http.StatusOK {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid status code %d\", statusCode))\n\t}\n\n\tbody, err := c.HTTPClient.ToString(response)\n\n\tif err != nil {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid response: %s\", err.Error()))\n\t}\n\n\tok, err := result.LoadFromJSON([]byte(body))\n\n\tif err != nil {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid response: %s\", err.Error()))\n\t}\n\n\tif !ok {\n\t\treturn result, fmt.Errorf(\"Invalid response\")\n\t}\n\n\treturn result, nil\n}\n\n// GetApplication Get Application\nfunc (c *Client) GetApplication(ctx context.Context, cluster, namespace, application string) (model.Application, error) {\n\tvar result model.Application\n\n\tresponse, err := c.HTTPClient.Get(\n\t\tctx,\n\t\tfmt.Sprintf(\"%s/api/v1/cluster/%s/namespace/%s/app/%s\", c.APIURL, cluster, namespace, application),\n\t\tmap[string]string{},\n\t\tmap[string]string{\"X-API-KEY\": c.APIKey},\n\t)\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tstatusCode := c.HTTPClient.GetStatusCode(response)\n\n\tif statusCode != http.StatusOK {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid status code %d\", statusCode))\n\t}\n\n\tbody, err := c.HTTPClient.ToString(response)\n\n\tif err != nil {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid response: %s\", err.Error()))\n\t}\n\n\tok, err := result.LoadFromJSON([]byte(body))\n\n\tif err != nil {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid response: %s\", err.Error()))\n\t}\n\n\tif !ok {\n\t\treturn result, fmt.Errorf(\"Invalid response\")\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "sdk/application_test.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage sdk\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/clivern/beetle/core/model\"\n\t\"github.com/clivern/beetle/core/module\"\n\t\"github.com/clivern/beetle/pkg\"\n\n\t\"github.com/drone/envsubst\"\n\t\"github.com/spf13/viper\"\n)\n\n// TestApplicationCRUD test cases\nfunc TestApplicationCRUD(t *testing.T) {\n\ttestingConfig := \"config.testing.yml\"\n\n\thttpClient := Client{}\n\thttpClient.SetHTTPClient(module.NewHTTPClient(20))\n\thttpClient.SetAPIKey(\"\")\n\n\t// LoadConfigFile\n\tt.Run(\"LoadConfigFile\", func(t *testing.T) {\n\t\tfs := module.FileSystem{}\n\n\t\tdir, _ := os.Getwd()\n\t\tconfigFile := fmt.Sprintf(\"%s/%s\", dir, testingConfig)\n\n\t\tfor {\n\t\t\tif fs.FileExists(configFile) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tdir = filepath.Dir(dir)\n\t\t\tconfigFile = fmt.Sprintf(\"%s/%s\", dir, testingConfig)\n\t\t}\n\n\t\tt.Logf(\"Load Config File %s\", configFile)\n\n\t\tconfigUnparsed, _ := ioutil.ReadFile(configFile)\n\t\tconfigParsed, _ := envsubst.EvalEnv(string(configUnparsed))\n\t\tviper.SetConfigType(\"yaml\")\n\t\tviper.ReadConfig(bytes.NewBuffer([]byte(configParsed)))\n\t})\n\n\t// TestGetApplications\n\tt.Run(\"TestGetApplications\", func(t *testing.T) {\n\t\tsrv := pkg.ServerMock(\n\t\t\t\"/api/v1/cluster/production/namespace/default/app\",\n\t\t\t`{\"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\"}}]}]}`,\n\t\t\thttp.StatusOK,\n\t\t)\n\n\t\tdefer srv.Close()\n\n\t\thttpClient.SetAPIURL(srv.URL)\n\t\tresult, err := httpClient.GetApplications(context.TODO(), \"production\", \"default\")\n\n\t\tpkg.Expect(t, nil, err)\n\t\tpkg.Expect(t, result, model.Applications{\n\t\t\tApplications: []model.Application{\n\t\t\t\tmodel.Application{\n\t\t\t\t\tID:     \"toad\",\n\t\t\t\t\tName:   \"Toad App\",\n\t\t\t\t\tFormat: \"clivern/toad:release-[.Release]\",\n\t\t\t\t\tContainers: []model.Container{\n\t\t\t\t\t\tmodel.Container{\n\t\t\t\t\t\t\tName:    \"toad\",\n\t\t\t\t\t\t\tImage:   \"clivern/toad:release-0.2.3\",\n\t\t\t\t\t\t\tVersion: \"0.2.3\",\n\t\t\t\t\t\t\tDeployment: model.Deployment{\n\t\t\t\t\t\t\t\tName: \"toad-deployment\",\n\t\t\t\t\t\t\t\tUID:  \"0f77903a-ce69-4aa5-a025-cad4b4a3209e\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t})\n\n\t// TestGetApplication\n\tt.Run(\"TestGetApplication\", func(t *testing.T) {\n\t\tsrv := pkg.ServerMock(\n\t\t\t\"/api/v1/cluster/production/namespace/default/app/toad\",\n\t\t\t`{\"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\"}}]}`,\n\t\t\thttp.StatusOK,\n\t\t)\n\n\t\tdefer srv.Close()\n\n\t\thttpClient.SetAPIURL(srv.URL)\n\t\tresult, err := httpClient.GetApplication(context.TODO(), \"production\", \"default\", \"toad\")\n\n\t\tpkg.Expect(t, nil, err)\n\t\tpkg.Expect(t, result, model.Application{\n\t\t\tID:     \"toad\",\n\t\t\tName:   \"Toad App\",\n\t\t\tFormat: \"clivern/toad:release-[.Release]\",\n\t\t\tContainers: []model.Container{\n\t\t\t\tmodel.Container{\n\t\t\t\t\tName:    \"toad\",\n\t\t\t\t\tImage:   \"clivern/toad:release-0.2.3\",\n\t\t\t\t\tVersion: \"0.2.3\",\n\t\t\t\t\tDeployment: model.Deployment{\n\t\t\t\t\t\tName: \"toad-deployment\",\n\t\t\t\t\t\tUID:  \"0f77903a-ce69-4aa5-a025-cad4b4a3209e\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t})\n}\n"
  },
  {
    "path": "sdk/client.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage sdk\n\nimport (\n\t\"github.com/clivern/beetle/core/module\"\n)\n\n// Client struct\ntype Client struct {\n\tAPIKey     string\n\tAPIURL     string\n\tHTTPClient *module.HTTPClient\n}\n\n// SetHTTPClient sets http client\nfunc (c *Client) SetHTTPClient(httpClient *module.HTTPClient) {\n\tc.HTTPClient = httpClient\n}\n\n// SetAPIURL sets api url\nfunc (c *Client) SetAPIURL(APIURL string) {\n\tc.APIURL = APIURL\n}\n\n// SetAPIKey sets api key\nfunc (c *Client) SetAPIKey(APIKey string) {\n\tc.APIKey = APIKey\n}\n"
  },
  {
    "path": "sdk/cluster.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage sdk\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/clivern/beetle/core/model\"\n)\n\n// GetClusters Get Clusters List\nfunc (c *Client) GetClusters(ctx context.Context) (model.Clusters, error) {\n\tvar result model.Clusters\n\n\tresponse, err := c.HTTPClient.Get(\n\t\tctx,\n\t\tfmt.Sprintf(\"%s/api/v1/cluster\", c.APIURL),\n\t\tmap[string]string{},\n\t\tmap[string]string{\"X-API-KEY\": c.APIKey},\n\t)\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tstatusCode := c.HTTPClient.GetStatusCode(response)\n\n\tif statusCode != http.StatusOK {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid status code %d\", statusCode))\n\t}\n\n\tbody, err := c.HTTPClient.ToString(response)\n\n\tif err != nil {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid response: %s\", err.Error()))\n\t}\n\n\tok, err := result.LoadFromJSON([]byte(body))\n\n\tif err != nil {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid response: %s\", err.Error()))\n\t}\n\n\tif !ok {\n\t\treturn result, fmt.Errorf(\"Invalid response\")\n\t}\n\n\treturn result, nil\n}\n\n// GetCluster Get Cluster\nfunc (c *Client) GetCluster(ctx context.Context, cluster string) (model.Cluster, error) {\n\tvar result model.Cluster\n\n\tresponse, err := c.HTTPClient.Get(\n\t\tctx,\n\t\tfmt.Sprintf(\"%s/api/v1/cluster/%s\", c.APIURL, cluster),\n\t\tmap[string]string{},\n\t\tmap[string]string{\"X-API-KEY\": c.APIKey},\n\t)\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tstatusCode := c.HTTPClient.GetStatusCode(response)\n\n\tif statusCode != http.StatusOK {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid status code %d\", statusCode))\n\t}\n\n\tbody, err := c.HTTPClient.ToString(response)\n\n\tif err != nil {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid response: %s\", err.Error()))\n\t}\n\n\tok, err := result.LoadFromJSON([]byte(body))\n\n\tif err != nil {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid response: %s\", err.Error()))\n\t}\n\n\tif !ok {\n\t\treturn result, fmt.Errorf(\"Invalid response\")\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "sdk/cluster_test.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage sdk\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/clivern/beetle/core/model\"\n\t\"github.com/clivern/beetle/core/module\"\n\t\"github.com/clivern/beetle/pkg\"\n\n\t\"github.com/drone/envsubst\"\n\t\"github.com/spf13/viper\"\n)\n\n// TestClusterCRUD test cases\nfunc TestClusterCRUD(t *testing.T) {\n\ttestingConfig := \"config.testing.yml\"\n\n\thttpClient := Client{}\n\thttpClient.SetHTTPClient(module.NewHTTPClient(20))\n\thttpClient.SetAPIKey(\"\")\n\n\t// LoadConfigFile\n\tt.Run(\"LoadConfigFile\", func(t *testing.T) {\n\t\tfs := module.FileSystem{}\n\n\t\tdir, _ := os.Getwd()\n\t\tconfigFile := fmt.Sprintf(\"%s/%s\", dir, testingConfig)\n\n\t\tfor {\n\t\t\tif fs.FileExists(configFile) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tdir = filepath.Dir(dir)\n\t\t\tconfigFile = fmt.Sprintf(\"%s/%s\", dir, testingConfig)\n\t\t}\n\n\t\tt.Logf(\"Load Config File %s\", configFile)\n\n\t\tconfigUnparsed, _ := ioutil.ReadFile(configFile)\n\t\tconfigParsed, _ := envsubst.EvalEnv(string(configUnparsed))\n\t\tviper.SetConfigType(\"yaml\")\n\t\tviper.ReadConfig(bytes.NewBuffer([]byte(configParsed)))\n\t})\n\n\t// TestGetClusters\n\tt.Run(\"TestGetClusters\", func(t *testing.T) {\n\t\tsrv := pkg.ServerMock(\n\t\t\t\"/api/v1/cluster\",\n\t\t\t`{\"clusters\": [{\"name\": \"staging\", \"health\": false},{\"name\": \"production\", \"health\": true}]}`,\n\t\t\thttp.StatusOK,\n\t\t)\n\n\t\tdefer srv.Close()\n\n\t\thttpClient.SetAPIURL(srv.URL)\n\t\tresult, err := httpClient.GetClusters(context.TODO())\n\n\t\tpkg.Expect(t, nil, err)\n\t\tpkg.Expect(t, result, model.Clusters{\n\t\t\tClusters: []model.Cluster{\n\t\t\t\tmodel.Cluster{Name: \"staging\", Health: false},\n\t\t\t\tmodel.Cluster{Name: \"production\", Health: true},\n\t\t\t},\n\t\t})\n\t})\n\n\t// TestGetCluster\n\tt.Run(\"TestGetCluster\", func(t *testing.T) {\n\t\tsrv := pkg.ServerMock(\n\t\t\t\"/api/v1/cluster/staging\",\n\t\t\t`{\"name\": \"staging\", \"health\": false}`,\n\t\t\thttp.StatusOK,\n\t\t)\n\n\t\tdefer srv.Close()\n\n\t\thttpClient.SetAPIURL(srv.URL)\n\t\tresult, err := httpClient.GetCluster(context.TODO(), \"staging\")\n\n\t\tpkg.Expect(t, nil, err)\n\t\tpkg.Expect(t, result, model.Cluster{Name: \"staging\", Health: false})\n\t})\n}\n"
  },
  {
    "path": "sdk/deployment.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage sdk\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/clivern/beetle/core/model\"\n)\n\n// CreateDeployment Get Application\nfunc (c *Client) CreateDeployment(ctx context.Context, request model.DeploymentRequest) (model.Job, error) {\n\tvar result model.Job\n\n\trequestBody, err := request.ConvertToJSON()\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tresponse, err := c.HTTPClient.Post(\n\t\tctx,\n\t\tfmt.Sprintf(\"%s/api/v1/cluster/%s/namespace/%s/app/%s/deployment\", c.APIURL, request.Cluster, request.Namespace, request.Application),\n\t\trequestBody,\n\t\tmap[string]string{},\n\t\tmap[string]string{\"X-API-KEY\": c.APIKey},\n\t)\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tstatusCode := c.HTTPClient.GetStatusCode(response)\n\n\tif statusCode != http.StatusAccepted {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid status code %d\", statusCode))\n\t}\n\n\tbody, err := c.HTTPClient.ToString(response)\n\n\tif err != nil {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid response: %s\", err.Error()))\n\t}\n\n\tok, err := result.LoadFromJSON([]byte(body))\n\n\tif err != nil {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid response: %s\", err.Error()))\n\t}\n\n\tif !ok {\n\t\treturn result, fmt.Errorf(\"Invalid response\")\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "sdk/deployment_test.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage sdk\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/clivern/beetle/core/model\"\n\t\"github.com/clivern/beetle/core/module\"\n\t\"github.com/clivern/beetle/pkg\"\n\n\t\"github.com/drone/envsubst\"\n\t\"github.com/spf13/viper\"\n)\n\n// TestDeploymentCRUD test cases\nfunc TestDeploymentCRUD(t *testing.T) {\n\ttestingConfig := \"config.testing.yml\"\n\n\thttpClient := Client{}\n\thttpClient.SetHTTPClient(module.NewHTTPClient(20))\n\thttpClient.SetAPIKey(\"\")\n\n\t// LoadConfigFile\n\tt.Run(\"LoadConfigFile\", func(t *testing.T) {\n\t\tfs := module.FileSystem{}\n\n\t\tdir, _ := os.Getwd()\n\t\tconfigFile := fmt.Sprintf(\"%s/%s\", dir, testingConfig)\n\n\t\tfor {\n\t\t\tif fs.FileExists(configFile) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tdir = filepath.Dir(dir)\n\t\t\tconfigFile = fmt.Sprintf(\"%s/%s\", dir, testingConfig)\n\t\t}\n\n\t\tt.Logf(\"Load Config File %s\", configFile)\n\n\t\tconfigUnparsed, _ := ioutil.ReadFile(configFile)\n\t\tconfigParsed, _ := envsubst.EvalEnv(string(configUnparsed))\n\t\tviper.SetConfigType(\"yaml\")\n\t\tviper.ReadConfig(bytes.NewBuffer([]byte(configParsed)))\n\t})\n\n\t// TestCreateDeployment\n\tt.Run(\"TestCreateDeployment\", func(t *testing.T) {\n\t\tsrv := pkg.ServerMock(\n\t\t\t\"/api/v1/cluster/production/namespace/default/app/toad/deployment\",\n\t\t\t`{\"id\":1,\"uuid\":\"4f540ab1-2c29-47e6-b900-675312b784d8\",\"status\":\"pending\",\"type\":\"deployment.update\",\"created_at\":\"2020-06-16T18:20:35Z\"}`,\n\t\t\thttp.StatusAccepted,\n\t\t)\n\n\t\tdefer srv.Close()\n\n\t\tdeploymentRequest := model.DeploymentRequest{\n\t\t\tCluster:     \"production\",\n\t\t\tNamespace:   \"default\",\n\t\t\tApplication: \"toad\",\n\t\t\tVersion:     \"1.0.0\",\n\t\t\tStrategy:    \"recreate\",\n\t\t}\n\n\t\thttpClient.SetAPIURL(srv.URL)\n\t\tresult, err := httpClient.CreateDeployment(context.TODO(), deploymentRequest)\n\n\t\tpkg.Expect(t, err, nil)\n\t\tpkg.Expect(t, 1, result.ID)\n\t\tpkg.Expect(t, \"4f540ab1-2c29-47e6-b900-675312b784d8\", result.UUID)\n\t\tpkg.Expect(t, \"pending\", result.Status)\n\t\tpkg.Expect(t, \"deployment.update\", result.Type)\n\t})\n}\n"
  },
  {
    "path": "sdk/job.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage sdk\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/clivern/beetle/core/model\"\n)\n\n// GetJobs Get Jobs List\nfunc (c *Client) GetJobs(ctx context.Context) (model.Jobs, error) {\n\tvar result model.Jobs\n\n\tresponse, err := c.HTTPClient.Get(\n\t\tctx,\n\t\tfmt.Sprintf(\"%s/api/v1/job\", c.APIURL),\n\t\tmap[string]string{},\n\t\tmap[string]string{\"X-API-KEY\": c.APIKey},\n\t)\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tstatusCode := c.HTTPClient.GetStatusCode(response)\n\n\tif statusCode != http.StatusOK {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid status code %d\", statusCode))\n\t}\n\n\tbody, err := c.HTTPClient.ToString(response)\n\n\tif err != nil {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid response: %s\", err.Error()))\n\t}\n\n\tok, err := result.LoadFromJSON([]byte(body))\n\n\tif err != nil {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid response: %s\", err.Error()))\n\t}\n\n\tif !ok {\n\t\treturn result, fmt.Errorf(\"Invalid response\")\n\t}\n\n\treturn result, nil\n}\n\n// GetJob Get Job\nfunc (c *Client) GetJob(ctx context.Context, uuid string) (model.Job, error) {\n\tvar result model.Job\n\n\tresponse, err := c.HTTPClient.Get(\n\t\tctx,\n\t\tfmt.Sprintf(\"%s/api/v1/job/%s\", c.APIURL, uuid),\n\t\tmap[string]string{},\n\t\tmap[string]string{\"X-API-KEY\": c.APIKey},\n\t)\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tstatusCode := c.HTTPClient.GetStatusCode(response)\n\n\tif statusCode != http.StatusOK {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid status code %d\", statusCode))\n\t}\n\n\tbody, err := c.HTTPClient.ToString(response)\n\n\tif err != nil {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid response: %s\", err.Error()))\n\t}\n\n\tok, err := result.LoadFromJSON([]byte(body))\n\n\tif err != nil {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid response: %s\", err.Error()))\n\t}\n\n\tif !ok {\n\t\treturn result, fmt.Errorf(\"Invalid response\")\n\t}\n\n\treturn result, nil\n}\n\n// DeleteJob Delete Job\nfunc (c *Client) DeleteJob(ctx context.Context, uuid string) (bool, error) {\n\tresponse, err := c.HTTPClient.Delete(\n\t\tctx,\n\t\tfmt.Sprintf(\"%s/api/v1/job/%s\", c.APIURL, uuid),\n\t\tmap[string]string{},\n\t\tmap[string]string{\"X-API-KEY\": c.APIKey},\n\t)\n\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tstatusCode := c.HTTPClient.GetStatusCode(response)\n\n\tif statusCode != http.StatusNoContent {\n\t\treturn false, fmt.Errorf(fmt.Sprintf(\"Invalid status code %d\", statusCode))\n\t}\n\n\treturn true, nil\n}\n"
  },
  {
    "path": "sdk/job_test.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage sdk\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/clivern/beetle/core/module\"\n\t\"github.com/clivern/beetle/pkg\"\n\n\t\"github.com/drone/envsubst\"\n\t\"github.com/spf13/viper\"\n)\n\n// TestJobCRUD test cases\nfunc TestJobCRUD(t *testing.T) {\n\ttestingConfig := \"config.testing.yml\"\n\n\thttpClient := Client{}\n\thttpClient.SetHTTPClient(module.NewHTTPClient(20))\n\thttpClient.SetAPIKey(\"\")\n\n\t// LoadConfigFile\n\tt.Run(\"LoadConfigFile\", func(t *testing.T) {\n\t\tfs := module.FileSystem{}\n\n\t\tdir, _ := os.Getwd()\n\t\tconfigFile := fmt.Sprintf(\"%s/%s\", dir, testingConfig)\n\n\t\tfor {\n\t\t\tif fs.FileExists(configFile) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tdir = filepath.Dir(dir)\n\t\t\tconfigFile = fmt.Sprintf(\"%s/%s\", dir, testingConfig)\n\t\t}\n\n\t\tt.Logf(\"Load Config File %s\", configFile)\n\n\t\tconfigUnparsed, _ := ioutil.ReadFile(configFile)\n\t\tconfigParsed, _ := envsubst.EvalEnv(string(configUnparsed))\n\t\tviper.SetConfigType(\"yaml\")\n\t\tviper.ReadConfig(bytes.NewBuffer([]byte(configParsed)))\n\t})\n\n\t// TestGetJobs\n\tt.Run(\"TestGetJobs\", func(t *testing.T) {\n\t\tsrv := pkg.ServerMock(\n\t\t\t\"/api/v1/job\",\n\t\t\t`{\"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\"}]}`,\n\t\t\thttp.StatusOK,\n\t\t)\n\n\t\tdefer srv.Close()\n\n\t\thttpClient.SetAPIURL(srv.URL)\n\t\tresult, err := httpClient.GetJobs(context.TODO())\n\n\t\tpkg.Expect(t, err, nil)\n\t\tpkg.Expect(t, result.Jobs[0].ID, 1)\n\t\tpkg.Expect(t, result.Jobs[0].UUID, \"4f540ab1-2c29-47e6-b900-675312b784d8\")\n\t\tpkg.Expect(t, result.Jobs[0].Status, \"pending\")\n\t\tpkg.Expect(t, result.Jobs[0].Type, \"deployment.update\")\n\t})\n\n\t// TestGetJob\n\tt.Run(\"TestGetJob\", func(t *testing.T) {\n\t\tsrv := pkg.ServerMock(\n\t\t\t\"/api/v1/job/4f540ab1-2c29-47e6-b900-675312b784d8\",\n\t\t\t`{\"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\"}`,\n\t\t\thttp.StatusOK,\n\t\t)\n\n\t\tdefer srv.Close()\n\n\t\thttpClient.SetAPIURL(srv.URL)\n\t\tresult, err := httpClient.GetJob(context.TODO(), \"4f540ab1-2c29-47e6-b900-675312b784d8\")\n\n\t\tpkg.Expect(t, err, nil)\n\t\tpkg.Expect(t, result.ID, 1)\n\t\tpkg.Expect(t, result.UUID, \"4f540ab1-2c29-47e6-b900-675312b784d8\")\n\t\tpkg.Expect(t, result.Status, \"pending\")\n\t\tpkg.Expect(t, result.Type, \"deployment.update\")\n\t})\n\n\t// TestDeleteJob\n\tt.Run(\"TestDeleteJob\", func(t *testing.T) {\n\t\tsrv := pkg.ServerMock(\n\t\t\t\"/api/v1/job/4f540ab1-2c29-47e6-b900-675312b784d8\",\n\t\t\t``,\n\t\t\thttp.StatusNoContent,\n\t\t)\n\n\t\tdefer srv.Close()\n\n\t\thttpClient.SetAPIURL(srv.URL)\n\t\tresult, err := httpClient.DeleteJob(context.TODO(), \"4f540ab1-2c29-47e6-b900-675312b784d8\")\n\n\t\tpkg.Expect(t, err, nil)\n\t\tpkg.Expect(t, result, true)\n\t})\n}\n"
  },
  {
    "path": "sdk/namespace.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage sdk\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/clivern/beetle/core/model\"\n)\n\n// GetNamespaces Get Namespaces List\nfunc (c *Client) GetNamespaces(ctx context.Context, cluster string) (model.Namespaces, error) {\n\tvar result model.Namespaces\n\n\tresponse, err := c.HTTPClient.Get(\n\t\tctx,\n\t\tfmt.Sprintf(\"%s/api/v1/cluster/%s/namespace\", c.APIURL, cluster),\n\t\tmap[string]string{},\n\t\tmap[string]string{\"X-API-KEY\": c.APIKey},\n\t)\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tstatusCode := c.HTTPClient.GetStatusCode(response)\n\n\tif statusCode != http.StatusOK {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid status code %d\", statusCode))\n\t}\n\n\tbody, err := c.HTTPClient.ToString(response)\n\n\tif err != nil {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid response: %s\", err.Error()))\n\t}\n\n\tok, err := result.LoadFromJSON([]byte(body))\n\n\tif err != nil {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid response: %s\", err.Error()))\n\t}\n\n\tif !ok {\n\t\treturn result, fmt.Errorf(\"Invalid response\")\n\t}\n\n\treturn result, nil\n}\n\n// GetNamespace Get Namespace\nfunc (c *Client) GetNamespace(ctx context.Context, cluster, namespace string) (model.Namespace, error) {\n\tvar result model.Namespace\n\n\tresponse, err := c.HTTPClient.Get(\n\t\tctx,\n\t\tfmt.Sprintf(\"%s/api/v1/cluster/%s/namespace/%s\", c.APIURL, cluster, namespace),\n\t\tmap[string]string{},\n\t\tmap[string]string{\"X-API-KEY\": c.APIKey},\n\t)\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tstatusCode := c.HTTPClient.GetStatusCode(response)\n\n\tif statusCode != http.StatusOK {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid status code %d\", statusCode))\n\t}\n\n\tbody, err := c.HTTPClient.ToString(response)\n\n\tif err != nil {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid response: %s\", err.Error()))\n\t}\n\n\tok, err := result.LoadFromJSON([]byte(body))\n\n\tif err != nil {\n\t\treturn result, fmt.Errorf(fmt.Sprintf(\"Invalid response: %s\", err.Error()))\n\t}\n\n\tif !ok {\n\t\treturn result, fmt.Errorf(\"Invalid response\")\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "sdk/namespace_test.go",
    "content": "// Copyright 2020 Clivern. All rights reserved.\n// Use of this source code is governed by the MIT\n// license that can be found in the LICENSE file.\n\npackage sdk\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/clivern/beetle/core/model\"\n\t\"github.com/clivern/beetle/core/module\"\n\t\"github.com/clivern/beetle/pkg\"\n\n\t\"github.com/drone/envsubst\"\n\t\"github.com/spf13/viper\"\n)\n\n// TestNamespaceCRUD test cases\nfunc TestNamespaceCRUD(t *testing.T) {\n\ttestingConfig := \"config.testing.yml\"\n\n\thttpClient := Client{}\n\thttpClient.SetHTTPClient(module.NewHTTPClient(20))\n\thttpClient.SetAPIKey(\"\")\n\n\t// LoadConfigFile\n\tt.Run(\"LoadConfigFile\", func(t *testing.T) {\n\t\tfs := module.FileSystem{}\n\n\t\tdir, _ := os.Getwd()\n\t\tconfigFile := fmt.Sprintf(\"%s/%s\", dir, testingConfig)\n\n\t\tfor {\n\t\t\tif fs.FileExists(configFile) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tdir = filepath.Dir(dir)\n\t\t\tconfigFile = fmt.Sprintf(\"%s/%s\", dir, testingConfig)\n\t\t}\n\n\t\tt.Logf(\"Load Config File %s\", configFile)\n\n\t\tconfigUnparsed, _ := ioutil.ReadFile(configFile)\n\t\tconfigParsed, _ := envsubst.EvalEnv(string(configUnparsed))\n\t\tviper.SetConfigType(\"yaml\")\n\t\tviper.ReadConfig(bytes.NewBuffer([]byte(configParsed)))\n\t})\n\n\t// TestGetNamespaces\n\tt.Run(\"TestGetNamespaces\", func(t *testing.T) {\n\t\tsrv := pkg.ServerMock(\n\t\t\t\"/api/v1/cluster/production/namespace\",\n\t\t\t`{\"namespaces\": [{\"name\": \"default\",\"uid\": \"f03ea2f1-bc1c-4563-b9c7-4413dffc18db\",\"status\": \"active\"},{\"name\": \"kube-node-lease\",\"uid\": \"398c907f-d888-455d-871d-145752f9ca73\",\"status\": \"active\"}]}`,\n\t\t\thttp.StatusOK,\n\t\t)\n\n\t\tdefer srv.Close()\n\n\t\thttpClient.SetAPIURL(srv.URL)\n\t\tresult, err := httpClient.GetNamespaces(context.TODO(), \"production\")\n\n\t\tpkg.Expect(t, nil, err)\n\t\tpkg.Expect(t, result, model.Namespaces{\n\t\t\tNamespaces: []model.Namespace{\n\t\t\t\tmodel.Namespace{Name: \"default\", UID: \"f03ea2f1-bc1c-4563-b9c7-4413dffc18db\", Status: \"active\"},\n\t\t\t\tmodel.Namespace{Name: \"kube-node-lease\", UID: \"398c907f-d888-455d-871d-145752f9ca73\", Status: \"active\"},\n\t\t\t},\n\t\t})\n\t})\n\n\t// TestGetNamespace\n\tt.Run(\"TestGetNamespace\", func(t *testing.T) {\n\t\tsrv := pkg.ServerMock(\n\t\t\t\"/api/v1/cluster/production/namespace/default\",\n\t\t\t`{\"name\":\"default\",\"status\":\"active\",\"uid\":\"f03ea2f1-bc1c-4563-b9c7-4413dffc18db\"}`,\n\t\t\thttp.StatusOK,\n\t\t)\n\n\t\tdefer srv.Close()\n\n\t\thttpClient.SetAPIURL(srv.URL)\n\t\tresult, err := httpClient.GetNamespace(context.TODO(), \"production\", \"default\")\n\n\t\tpkg.Expect(t, nil, err)\n\t\tpkg.Expect(t, result, model.Namespace{Name: \"default\", UID: \"f03ea2f1-bc1c-4563-b9c7-4413dffc18db\", Status: \"active\"})\n\t})\n}\n"
  },
  {
    "path": "swagger.yaml",
    "content": "swagger: '2.0'\ninfo:\n  description: |\n    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.\n  version: 1.0.3\n  title: Beetle\n  contact:\n    email: hello@clivern.com\n  license:\n    name: MIT\n    url: 'https://github.com/Clivern/Beetle/blob/main/LICENSE'\nhost: beetle.yourcompany.com\nbasePath: /\nschemes:\n  - https\n  - http\npaths:\n  /_health:\n    get:\n      tags:\n        - Healthcheck\n      summary: Get system health status\n      produces:\n        - application/json\n      parameters: []\n      responses:\n        '200':\n          description: system healthy\n          schema:\n            $ref: '#/definitions/healthResponse'\n        '500':\n          description: system is down\n          schema:\n            $ref: '#/definitions/healthResponse'\n  /_ready:\n    get:\n      tags:\n        - Readiness\n      summary: Get system readiness\n      produces:\n        - application/json\n      parameters: []\n      responses:\n        '200':\n          description: system ready to accept traffic\n          schema:\n            $ref: '#/definitions/healthResponse'\n        '500':\n          description: system not ready to accept traffic\n          schema:\n            $ref: '#/definitions/healthResponse'\n  /metrics:\n    get:\n      tags:\n        - Metrics\n      summary: Get metrics for prometheus\n      produces:\n        - text/plain\n      parameters: []\n      responses:\n        '200':\n          description: system metrics\n        '500':\n          description: Internal server error\n  /api/v1/cluster:\n    get:\n      tags:\n        - Cluster\n      summary: Get clusters list\n      description: ''\n      operationId: getClusters\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: successful operation\n          schema:\n            $ref: '#/definitions/Clusters'\n        '400':\n          description: Invalid request\n        '404':\n          description: Resource not found\n        '500':\n          description: Internal server error\n      security:\n        - api_key: []\n  '/api/v1/cluster/{cn}':\n    get:\n      tags:\n        - Cluster\n      summary: Get cluster by name\n      description: ''\n      operationId: getClusterByName\n      produces:\n        - application/json\n      parameters:\n        - in: path\n          name: cn\n          description: The name of the cluster\n          required: true\n          type: string\n      responses:\n        '200':\n          description: successful operation\n          schema:\n            $ref: '#/definitions/Cluster'\n        '400':\n          description: Invalid request\n        '404':\n          description: Resource not found\n        '500':\n          description: Internal server error\n      security:\n        - api_key: []\n  '/api/v1/cluster/{cn}/namespace':\n    get:\n      tags:\n        - Namespace\n      summary: Get namespaces list\n      description: ''\n      operationId: getNamespaces\n      produces:\n        - application/json\n      parameters:\n        - in: path\n          name: cn\n          description: The name of the cluster\n          required: true\n          type: string\n      responses:\n        '200':\n          description: successful operation\n          schema:\n            $ref: '#/definitions/Namespaces'\n        '400':\n          description: Invalid request\n        '404':\n          description: Resource not found\n        '500':\n          description: Internal server error\n      security:\n        - api_key: []\n  '/api/v1/cluster/{cn}/namespace/{ns}':\n    get:\n      tags:\n        - Namespace\n      summary: Get cluster namespace by name\n      description: ''\n      operationId: getClusterNamespaceByName\n      produces:\n        - application/json\n      parameters:\n        - in: path\n          name: cn\n          description: The name of the cluster\n          required: true\n          type: string\n        - in: path\n          name: ns\n          description: The name of the cluster namespace\n          required: true\n          type: string\n      responses:\n        '200':\n          description: successful operation\n          schema:\n            $ref: '#/definitions/Namespace'\n        '400':\n          description: Invalid request\n        '404':\n          description: Resource not found\n        '500':\n          description: Internal server error\n      security:\n        - api_key: []\n  '/api/v1/cluster/{cn}/namespace/{ns}/app':\n    get:\n      tags:\n        - Application\n      summary: Get applications list\n      description: ''\n      operationId: getApplications\n      produces:\n        - application/json\n      parameters:\n        - in: path\n          name: cn\n          description: The name of the cluster\n          required: true\n          type: string\n        - in: path\n          name: ns\n          description: The name of the cluster namespace\n          required: true\n          type: string\n      responses:\n        '200':\n          description: successful operation\n          schema:\n            $ref: '#/definitions/Applications'\n        '400':\n          description: Invalid request\n        '404':\n          description: Resource not found\n        '500':\n          description: Internal server error\n      security:\n        - api_key: []\n  '/api/v1/cluster/{cn}/namespace/{ns}/app/{id}':\n    get:\n      tags:\n        - Application\n      summary: Get application by id\n      description: ''\n      operationId: getApplicationById\n      produces:\n        - application/json\n      parameters:\n        - in: path\n          name: cn\n          description: The name of the cluster\n          required: true\n          type: string\n        - in: path\n          name: ns\n          description: The name of the cluster namespace\n          required: true\n          type: string\n        - in: path\n          name: id\n          description: The application id\n          required: true\n          type: string\n      responses:\n        '200':\n          description: successful operation\n          schema:\n            $ref: '#/definitions/Application'\n        '400':\n          description: Invalid request\n        '404':\n          description: Resource not found\n        '500':\n          description: Internal server error\n      security:\n        - api_key: []\n    post:\n      tags:\n        - Application\n      summary: Create a deployment request\n      description: ''\n      operationId: createDeploymentRequest\n      produces:\n        - application/json\n      parameters:\n        - in: path\n          name: cn\n          description: The name of the cluster\n          required: true\n          type: string\n        - in: path\n          name: ns\n          description: The name of the cluster namespace\n          required: true\n          type: string\n        - in: path\n          name: id\n          description: The application id\n          required: true\n          type: string\n        - in: body\n          name: body\n          description: The deployment request\n          required: true\n          schema:\n            $ref: '#/definitions/DeploymentRequest'\n      responses:\n        '202':\n          description: successful operation\n          schema:\n            $ref: '#/definitions/Job'\n        '400':\n          description: Invalid request\n        '404':\n          description: Resource not found\n        '500':\n          description: Internal server error\n      security:\n        - api_key: []\n  /api/v1/job:\n    get:\n      tags:\n        - Job\n      summary: Get jobs list\n      description: ''\n      operationId: getJobs\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: successful operation\n          schema:\n            $ref: '#/definitions/Jobs'\n        '400':\n          description: Invalid request\n        '500':\n          description: Internal server error\n      security:\n        - api_key: []\n  '/api/v1/job/{uuid}':\n    get:\n      tags:\n        - Job\n      summary: Get a job by UUID\n      description: ''\n      operationId: getJobByUUID\n      produces:\n        - application/json\n      parameters:\n        - in: path\n          name: uuid\n          description: The UUID of the job\n          required: true\n          type: string\n      responses:\n        '200':\n          description: successful operation\n          schema:\n            $ref: '#/definitions/Job'\n        '400':\n          description: Invalid request\n        '404':\n          description: Job not found\n        '500':\n          description: Internal server error\n      security:\n        - api_key: []\n    delete:\n      tags:\n        - Job\n      summary: Delete a job by UUID\n      description: ''\n      operationId: deleteJobByUUID\n      produces:\n        - application/json\n      parameters:\n        - in: path\n          name: uuid\n          description: The UUID of the job\n          required: true\n          type: string\n      responses:\n        '204':\n          description: successful operation\n        '400':\n          description: Invalid request\n        '404':\n          description: Job not found\n        '500':\n          description: Internal server error\n      security:\n        - api_key: []\nsecurityDefinitions:\n  api_key:\n    type: apiKey\n    name: X-API-KEY\n    in: header\ndefinitions:\n  healthResponse:\n    type: object\n    properties:\n      status:\n        type: string\n  Cluster:\n    type: object\n    properties:\n      name:\n        type: string\n      health:\n        type: boolean\n  Clusters:\n    type: object\n    properties:\n      clusters:\n        type: array\n        items:\n          $ref: '#/definitions/Cluster'\n  Namespace:\n    type: object\n    properties:\n      name:\n        type: string\n      uid:\n        type: string\n      status:\n        type: string\n  Namespaces:\n    type: object\n    properties:\n      namespaces:\n        type: array\n        items:\n          $ref: '#/definitions/Namespace'\n  Job:\n    type: object\n    properties:\n      id:\n        type: integer\n        format: int64\n      uuid:\n        type: string\n      payload:\n        type: string\n      status:\n        type: string\n      type:\n        type: string\n      result:\n        type: string\n      retry:\n        type: integer\n        format: int64\n      parent:\n        type: integer\n        format: int64\n      run_at:\n        type: string\n      created_at:\n        type: string\n      updated_at:\n        type: string\n  Jobs:\n    type: object\n    properties:\n      namespaces:\n        type: array\n        items:\n          $ref: '#/definitions/Job'\n  Applications:\n    type: object\n    properties:\n      namespaces:\n        type: array\n        items:\n          $ref: '#/definitions/Application'\n  Application:\n    type: object\n    properties:\n      id:\n        type: string\n      name:\n        type: string\n      format:\n        type: string\n      containers:\n        type: array\n        items:\n          $ref: '#/definitions/Container'\n  Container:\n    type: object\n    properties:\n      name:\n        type: string\n      image:\n        type: string\n      version:\n        type: string\n      deployment:\n        $ref: '#/definitions/Deployment'\n  Deployment:\n    type: object\n    properties:\n      name:\n        type: string\n      uid:\n        type: string\n  DeploymentRequest:\n    type: object\n    properties:\n      version:\n        type: string\n      strategy:\n        type: string\nexternalDocs:\n  description: Find out more about beetle\n  url: 'https://github.com/Clivern/Beetle'\n"
  }
]