From ba7fd61e2fbeb64922459b93ef88280bb45a9425 Mon Sep 17 00:00:00 2001 From: Christophe Collot Date: Fri, 15 Dec 2023 11:40:44 +0100 Subject: [PATCH] initial commit --- .github/workflows/build.yaml | 68 + .github/workflows/lint.yaml | 19 + .github/workflows/test.yaml | 16 + .gitignore | 5 + .golangci.yml | 92 + .goreleaser.yaml | 141 + Dockerfile | 21 + Makefile | 10 + README.md | 7 + cmd/daemon.go | 89 + cmd/root.go | 34 + .../_testdata/config-fail-wrong-interval.yml | 29 + config/_testdata/config-success.yml | 29 + config/config.go | 55 + config/config.yml | 87 + config/config_test.go | 30 + ...2-10-25-kubernetes-upgrade-kpi-diagram.png | Bin 0 -> 144367 bytes go.mod | 173 + go.sum | 641 ++ internal/app/_testdata/fakeSampleKey | 9 + internal/app/_testdata/index-traefik.yaml | 5624 +++++++++++++++++ internal/app/_testdata/index.yaml | 19 + internal/app/_testdata/test_chart/Chart.yaml | 12 + internal/app/_testdata/test_chart2/Chart.yaml | 9 + internal/app/_testdata/test_chart3/Chart.yaml | 10 + internal/app/_testdata/test_chart4/Chart.yaml | 9 + internal/app/app.go | 338 + internal/app/calculators/candidate_count.go | 20 + .../app/calculators/candidate_count_test.go | 42 + internal/app/calculators/date.go | 34 + internal/app/calculators/date_test.go | 73 + internal/app/calculators/default.go | 147 + internal/app/calculators/default_test.go | 58 + internal/app/calculators/meta.go | 39 + internal/app/calculators/meta_test.go | 49 + internal/app/calculators/skip.go | 12 + internal/app/core/software/contracts.go | 10 + internal/app/core/software/software.go | 52 + internal/app/core/software/version.go | 33 + internal/app/filters/date.go | 20 + internal/app/filters/date_test.go | 45 + internal/app/filters/filter.go | 32 + internal/app/filters/filter_test.go | 55 + internal/app/filters/semver.go | 40 + internal/app/filters/semver_test.go | 127 + internal/app/semver/semver.go | 25 + internal/app/semver/semver_test.go | 63 + internal/app/sources/argohelm/config.go | 21 + .../app/sources/argohelm/git_credentials.go | 65 + .../sources/argohelm/git_credentials_test.go | 88 + internal/app/sources/argohelm/source.go | 220 + internal/app/sources/argohelm/source_test.go | 70 + internal/app/sources/aws/config.go | 17 + internal/app/sources/aws/eks/config.go | 9 + internal/app/sources/aws/eks/source.go | 178 + internal/app/sources/aws/eks/source_test.go | 136 + .../app/sources/aws/elasticache/config.go | 9 + .../app/sources/aws/elasticache/provider.go | 49 + .../sources/aws/elasticache/provider_test.go | 108 + .../app/sources/aws/elasticache/source.go | 112 + .../sources/aws/elasticache/source_test.go | 43 + internal/app/sources/aws/lambda/config.go | 12 + internal/app/sources/aws/lambda/source.go | 121 + .../app/sources/aws/lambda/source_test.go | 118 + internal/app/sources/aws/msk/config.go | 9 + internal/app/sources/aws/msk/source.go | 75 + internal/app/sources/aws/msk/source_test.go | 49 + internal/app/sources/aws/rds/config.go | 10 + internal/app/sources/aws/rds/source.go | 123 + internal/app/sources/aws/rds/source_test.go | 71 + internal/app/sources/deployments/config.go | 14 + internal/app/sources/deployments/source.go | 179 + .../app/sources/deployments/source_test.go | 67 + internal/app/sources/filesystemhelm/config.go | 11 + internal/app/sources/filesystemhelm/source.go | 75 + .../app/sources/filesystemhelm/source_test.go | 48 + internal/app/sources/helm/versions/chart.go | 21 + .../app/sources/helm/versions/chart_test.go | 24 + .../app/sources/helm/versions/repo_backend.go | 110 + .../helm/versions/repo_backend_helm.go | 45 + .../sources/helm/versions/repo_backend_s3.go | 87 + .../helm/versions/repo_backend_s3_test.go | 102 + .../helm/versions/repo_backend_test.go | 26 + .../app/sources/helm/versions/versions.go | 175 + internal/app/sources/utils/gitutils/git.go | 44 + internal/app/sources/utils/utils.go | 5 + internal/build/build.go | 13 + internal/infra/aws/config.go | 7 + internal/infra/aws/eks.go | 15 + internal/infra/aws/eks_mock.go | 37 + internal/infra/aws/elasticache.go | 12 + internal/infra/aws/elasticache_mock.go | 99 + internal/infra/aws/lambda.go | 100 + internal/infra/aws/lambda_mock.go | 17 + internal/infra/aws/lambda_test.go | 79 + internal/infra/aws/mock.go | 21 + internal/infra/aws/msk.go | 12 + internal/infra/aws/msk_mock.go | 22 + internal/infra/aws/rds.go | 12 + internal/infra/aws/rds_mock.go | 22 + internal/infra/aws/s3.go | 11 + internal/infra/aws/s3_mock.go | 13 + internal/infra/http/server.go | 130 + internal/infra/kubernetes/argocd.go | 137 + internal/infra/kubernetes/argocd_test.go | 208 + internal/infra/kubernetes/client.go | 69 + internal/infra/kubernetes/deployment.go | 20 + internal/infra/kubernetes/filters.go | 71 + internal/infra/kubernetes/filters_test.go | 138 + internal/infra/kubernetes/mock.go | 28 + internal/infra/kubernetes/secret.go | 16 + internal/infra/registry/client.go | 70 + main.go | 13 + 113 files changed, 12690 insertions(+) create mode 100644 .github/workflows/build.yaml create mode 100644 .github/workflows/lint.yaml create mode 100644 .github/workflows/test.yaml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 .goreleaser.yaml create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 cmd/daemon.go create mode 100644 cmd/root.go create mode 100644 config/_testdata/config-fail-wrong-interval.yml create mode 100644 config/_testdata/config-success.yml create mode 100644 config/config.go create mode 100644 config/config.yml create mode 100644 config/config_test.go create mode 100644 doc/2022-10-25-kubernetes-upgrade-kpi-diagram.png create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/app/_testdata/fakeSampleKey create mode 100644 internal/app/_testdata/index-traefik.yaml create mode 100644 internal/app/_testdata/index.yaml create mode 100644 internal/app/_testdata/test_chart/Chart.yaml create mode 100644 internal/app/_testdata/test_chart2/Chart.yaml create mode 100644 internal/app/_testdata/test_chart3/Chart.yaml create mode 100644 internal/app/_testdata/test_chart4/Chart.yaml create mode 100644 internal/app/app.go create mode 100644 internal/app/calculators/candidate_count.go create mode 100644 internal/app/calculators/candidate_count_test.go create mode 100644 internal/app/calculators/date.go create mode 100644 internal/app/calculators/date_test.go create mode 100644 internal/app/calculators/default.go create mode 100644 internal/app/calculators/default_test.go create mode 100644 internal/app/calculators/meta.go create mode 100644 internal/app/calculators/meta_test.go create mode 100644 internal/app/calculators/skip.go create mode 100644 internal/app/core/software/contracts.go create mode 100644 internal/app/core/software/software.go create mode 100644 internal/app/core/software/version.go create mode 100644 internal/app/filters/date.go create mode 100644 internal/app/filters/date_test.go create mode 100644 internal/app/filters/filter.go create mode 100644 internal/app/filters/filter_test.go create mode 100644 internal/app/filters/semver.go create mode 100644 internal/app/filters/semver_test.go create mode 100644 internal/app/semver/semver.go create mode 100644 internal/app/semver/semver_test.go create mode 100644 internal/app/sources/argohelm/config.go create mode 100644 internal/app/sources/argohelm/git_credentials.go create mode 100644 internal/app/sources/argohelm/git_credentials_test.go create mode 100644 internal/app/sources/argohelm/source.go create mode 100644 internal/app/sources/argohelm/source_test.go create mode 100644 internal/app/sources/aws/config.go create mode 100644 internal/app/sources/aws/eks/config.go create mode 100644 internal/app/sources/aws/eks/source.go create mode 100644 internal/app/sources/aws/eks/source_test.go create mode 100644 internal/app/sources/aws/elasticache/config.go create mode 100644 internal/app/sources/aws/elasticache/provider.go create mode 100644 internal/app/sources/aws/elasticache/provider_test.go create mode 100644 internal/app/sources/aws/elasticache/source.go create mode 100644 internal/app/sources/aws/elasticache/source_test.go create mode 100644 internal/app/sources/aws/lambda/config.go create mode 100644 internal/app/sources/aws/lambda/source.go create mode 100644 internal/app/sources/aws/lambda/source_test.go create mode 100644 internal/app/sources/aws/msk/config.go create mode 100644 internal/app/sources/aws/msk/source.go create mode 100644 internal/app/sources/aws/msk/source_test.go create mode 100644 internal/app/sources/aws/rds/config.go create mode 100644 internal/app/sources/aws/rds/source.go create mode 100644 internal/app/sources/aws/rds/source_test.go create mode 100644 internal/app/sources/deployments/config.go create mode 100644 internal/app/sources/deployments/source.go create mode 100644 internal/app/sources/deployments/source_test.go create mode 100644 internal/app/sources/filesystemhelm/config.go create mode 100644 internal/app/sources/filesystemhelm/source.go create mode 100644 internal/app/sources/filesystemhelm/source_test.go create mode 100644 internal/app/sources/helm/versions/chart.go create mode 100644 internal/app/sources/helm/versions/chart_test.go create mode 100644 internal/app/sources/helm/versions/repo_backend.go create mode 100644 internal/app/sources/helm/versions/repo_backend_helm.go create mode 100644 internal/app/sources/helm/versions/repo_backend_s3.go create mode 100644 internal/app/sources/helm/versions/repo_backend_s3_test.go create mode 100644 internal/app/sources/helm/versions/repo_backend_test.go create mode 100644 internal/app/sources/helm/versions/versions.go create mode 100644 internal/app/sources/utils/gitutils/git.go create mode 100644 internal/app/sources/utils/utils.go create mode 100644 internal/build/build.go create mode 100644 internal/infra/aws/config.go create mode 100644 internal/infra/aws/eks.go create mode 100644 internal/infra/aws/eks_mock.go create mode 100644 internal/infra/aws/elasticache.go create mode 100644 internal/infra/aws/elasticache_mock.go create mode 100644 internal/infra/aws/lambda.go create mode 100644 internal/infra/aws/lambda_mock.go create mode 100644 internal/infra/aws/lambda_test.go create mode 100644 internal/infra/aws/mock.go create mode 100644 internal/infra/aws/msk.go create mode 100644 internal/infra/aws/msk_mock.go create mode 100644 internal/infra/aws/rds.go create mode 100644 internal/infra/aws/rds_mock.go create mode 100644 internal/infra/aws/s3.go create mode 100644 internal/infra/aws/s3_mock.go create mode 100644 internal/infra/http/server.go create mode 100644 internal/infra/kubernetes/argocd.go create mode 100644 internal/infra/kubernetes/argocd_test.go create mode 100644 internal/infra/kubernetes/client.go create mode 100644 internal/infra/kubernetes/deployment.go create mode 100644 internal/infra/kubernetes/filters.go create mode 100644 internal/infra/kubernetes/filters_test.go create mode 100644 internal/infra/kubernetes/mock.go create mode 100644 internal/infra/kubernetes/secret.go create mode 100644 internal/infra/registry/client.go create mode 100644 main.go diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..f7c0c8f --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,68 @@ +name: build +run-name: building and publishing new release +on: + push: + # run only against tags + tags: + - "*" +permissions: + contents: write # allows the action to create a Github release + id-token: write # This is required for requesting the AWS JWT + +jobs: + build-publish: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: us-east-1 # ECR Public can only be logged into from the us-east-1 region + role-to-assume: arn:aws:iam::202662887508:role/ecr-upgrade-manager + role-session-name: githubActions + + - name: Login to Amazon ECR + id: login-ecr-public + uses: aws-actions/amazon-ecr-login@v2 + with: + registry-type: public + mask-password: 'true' + + - run: git fetch --force --tags + + - uses: actions/setup-go@v4 + with: + go-version: 1.20 + + - name: Set up QEMU for ARM64 build + uses: docker/setup-qemu-action@v3 + + - uses: goreleaser/goreleaser-action@v5 + with: + distribution: goreleaser + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure AWS credentials for helm chart + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: us-east-1 # ECR Public can only be logged into from the us-east-1 region + role-to-assume: arn:aws:iam::202662887508:role/ecr-upgrade-manager-chart + role-session-name: githubActions + + - name: Login to Amazon ECR for helm chart + uses: aws-actions/amazon-ecr-login@v2 + with: + registry-type: public + mask-password: 'true' + + - name: Helm release + run: | + RELEASE_VERSION=$(jq -r .tag dist/metadata.json) + ./scripts/helm-release.sh upgrade-manager-chart chart ${RELEASE_VERSION} qonto diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..a3eed00 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,19 @@ +on: + push: + branches : [main] + pull_request: + branches: [main] + +jobs: + lint: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 + with: + go-version: 1.20 + - uses: golangci/golangci-lint-action@v3 + with: + version: v1.55.2 + args: --timeout=5m + skip-cache: false \ No newline at end of file diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..fb01933 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,16 @@ +on: + push: + branches : [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 + with: + go-version: 1.20 + - name: Run testing + run: go test -race -v ./... \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b97e010 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +upgrade-manager +tmp* +/config.yaml +*.tgz +.*rendered.* \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..8258deb --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,92 @@ +run: + concurrency: 4 + deadline: 1m + issues-exit-code: 1 + tests: true + +output: + format: colored-line-number + print-issued-lines: true + print-linter-name: true + +linters: + enable-all: false + disable-all: false + enable: + - deadcode + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - structcheck + - typecheck + - unused + - varcheck + - asciicheck + - bodyclose + - depguard + - dogsled + - durationcheck + - errorlint + - exhaustive + - exportloopref + - forcetypeassert + - gochecknoinits + - goconst + - gocritic + - gocyclo + - gofmt + - goimports + - gomoddirectives + - gomodguard + - goprintffuncname + - gosec + - ifshort + - importas + - makezero + - misspell + - nakedret + - nestif + - nilerr + - noctx + - predeclared + - revive + - rowserrcheck + - sqlclosecheck + - thelper + - tparallel + - unconvert + - unparam + - wastedassign + - whitespace + - gci # File is not `gci`-ed with --skip-generated -s standard,default (gci) +linters-settings: + gocyclo: + min-complexity: 35 + + revive: + rules: + - name: exported + disabled: true + +issues: + exclude-use-default: false + max-per-linter: 1024 + max-same: 1024 + + exclude-rules: + - text: "SA1029" + linters: + - staticcheck + - text: "G304" + linters: + - gosec + # Exclude some linters from running on test files + - path: _test\.go + linters: + # bodyclose reports some false-positives when using a test request recorder + - bodyclose + # It's overkill to use `NewRequestWithContext` in tests + - noctx + - goerr113 diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..0d020fb --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,141 @@ +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +env: + - BUILD_INFO_PACKAGE_PATH=github.com/qonto/upgrade-manager/internal/build + - DOCKER_REGISTRY=public.ecr.aws/qonto + - DOCKER_IMAGE_NAME=upgrade-manager + +builds: + - env: + - CGO_ENABLED=0 + ldflags: + - '-s' + - '-w' + - '-X "{{ .Env.BUILD_INFO_PACKAGE_PATH }}.Version={{.Version}}"' + - '-X "{{ .Env.BUILD_INFO_PACKAGE_PATH }}.Commit={{.Commit}}"' + - '-X "{{ .Env.BUILD_INFO_PACKAGE_PATH }}.Date={{.Date}}"' + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + +archives: + - format: tar.gz + # this name template makes the OS and Arch compatible with the results of uname. + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + # use zip for windows archives + format_overrides: + - goos: windows + format: zip + +checksum: + name_template: 'checksums.txt' + +snapshot: + name_template: "{{ incpatch .Version }}-next" + +changelog: + sort: asc + use: github + filters: + exclude: + - "^test:" + - "^chore" + - "merge conflict" + - Merge pull request + - Merge remote-tracking branch + - Merge branch + - go mod tidy + groups: + - title: Dependency updates + regexp: '^.*?(feat|fix)\(deps\)!?:.+$' + order: 300 + - title: "New Features" + regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$' + order: 100 + - title: "Security updates" + regexp: '^.*?sec(\([[:word:]]+\))??!?:.+$' + order: 150 + - title: "Bug fixes" + regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$' + order: 200 + - title: "Documentation updates" + regexp: ^.*?doc(\([[:word:]]+\))??!?:.+$ + order: 400 + - title: "Build process updates" + regexp: ^.*?build(\([[:word:]]+\))??!?:.+$ + order: 400 + - title: Other work + order: 9999 + +dockers: +- image_templates: + - "{{ .Env.DOCKER_REGISTRY }}/{{ .Env.DOCKER_IMAGE_NAME }}:{{ .Tag }}-amd64" + - "{{ .Env.DOCKER_REGISTRY }}/{{ .Env.DOCKER_IMAGE_NAME }}:v{{ .Major }}-amd64" + - "{{ .Env.DOCKER_REGISTRY }}/{{ .Env.DOCKER_IMAGE_NAME }}:v{{ .Major }}.{{ .Minor }}-amd64" + - "{{ .Env.DOCKER_REGISTRY }}/{{ .Env.DOCKER_IMAGE_NAME }}:latest-amd64" + dockerfile: Dockerfile + build_flag_templates: + - --label=org.opencontainers.image.title={{ .ProjectName }} + - --label=org.opencontainers.image.description={{ .ProjectName }} + - --label=org.opencontainers.image.url=https://github.com/qonto/upgrade-manager + - --label=org.opencontainers.image.source=https://github.com/qonto/upgrade-manager + - --label=org.opencontainers.image.version={{ .Version }} + - --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }} + - --label=org.opencontainers.image.revision={{ .FullCommit }} + - --label=org.opencontainers.image.licenses=MIT + - "--pull" + - "--platform=linux/amd64" + use: buildx +- image_templates: + - "{{ .Env.DOCKER_REGISTRY }}/{{ .Env.DOCKER_IMAGE_NAME }}:{{ .Tag }}-arm64" + - "{{ .Env.DOCKER_REGISTRY }}/{{ .Env.DOCKER_IMAGE_NAME }}:v{{ .Major }}-arm64" + - "{{ .Env.DOCKER_REGISTRY }}/{{ .Env.DOCKER_IMAGE_NAME }}:v{{ .Major }}.{{ .Minor }}-arm64" + - "{{ .Env.DOCKER_REGISTRY }}/{{ .Env.DOCKER_IMAGE_NAME }}:latest-arm64" + dockerfile: Dockerfile + build_flag_templates: + - --label=org.opencontainers.image.title={{ .ProjectName }} + - --label=org.opencontainers.image.description={{ .ProjectName }} + - --label=org.opencontainers.image.url=https://github.com/qonto/upgrade-manager + - --label=org.opencontainers.image.source=https://github.com/qonto/upgrade-manager + - --label=org.opencontainers.image.version={{ .Version }} + - --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }} + - --label=org.opencontainers.image.revision={{ .FullCommit }} + - --label=org.opencontainers.image.licenses=MIT + - "--pull" + - "--platform=linux/arm64" + use: buildx + goarch: arm64 + +docker_manifests: +- name_template: '{{ .Env.DOCKER_REGISTRY }}/{{ .Env.DOCKER_IMAGE_NAME }}:{{ .Tag }}' + image_templates: + - '{{ .Env.DOCKER_REGISTRY }}/{{ .Env.DOCKER_IMAGE_NAME }}:{{ .Tag }}-amd64' + - '{{ .Env.DOCKER_REGISTRY }}/{{ .Env.DOCKER_IMAGE_NAME }}:{{ .Tag }}-arm64' +- name_template: '{{ .Env.DOCKER_REGISTRY }}/{{ .Env.DOCKER_IMAGE_NAME }}:v{{ .Major }}' + image_templates: + - '{{ .Env.DOCKER_REGISTRY }}/{{ .Env.DOCKER_IMAGE_NAME }}:v{{ .Major }}-amd64' + - '{{ .Env.DOCKER_REGISTRY }}/{{ .Env.DOCKER_IMAGE_NAME }}:v{{ .Major }}-arm64' +- name_template: '{{ .Env.DOCKER_REGISTRY }}/{{ .Env.DOCKER_IMAGE_NAME }}:v{{ .Major }}.{{ .Minor }}' + image_templates: + - '{{ .Env.DOCKER_REGISTRY }}/{{ .Env.DOCKER_IMAGE_NAME }}:v{{ .Major }}.{{ .Minor }}-amd64' + - '{{ .Env.DOCKER_REGISTRY }}/{{ .Env.DOCKER_IMAGE_NAME }}:v{{ .Major }}.{{ .Minor }}-arm64' +- name_template: '{{ .Env.DOCKER_REGISTRY }}/{{ .Env.DOCKER_IMAGE_NAME }}:latest' + image_templates: + - '{{ .Env.DOCKER_REGISTRY }}/{{ .Env.DOCKER_IMAGE_NAME }}:latest-amd64' + - '{{ .Env.DOCKER_REGISTRY }}/{{ .Env.DOCKER_IMAGE_NAME }}:latest-arm64' + +release: + github: + owner: qonto + name: upgrade-manager + name_template: "v{{.Version}}" + footer: | + **Full Changelog**: https://github.com/qonto/upgrade-manager/compare/{{ .PreviousTag }}...{{ .Tag }} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2608ccc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM alpine:3.19 + +ARG HOME=/app + +RUN apk add --update --no-cache ca-certificates + +RUN addgroup -g 1616 -S upgrademanager \ + && adduser --home ${HOME} -u 1616 -S upgrademanager -G upgrademanager \ + && mkdir -p /app \ + && chown upgrademanager: -R /app + +USER 1616 + +WORKDIR ${HOME} + +COPY upgrade-manager /app/ + +EXPOSE 10000 + +ENTRYPOINT ["/app/upgrade-manager"] +CMD ["start"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c3e565e --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +test: + go test -v -cover -race ./... + +start: + go run main.go start --debug + +lint: + golangci-lint run + +.PHONY: test start lint \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..311d082 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Description + +An autodiscovery tool to help you know what and when to update. +Features: + + automatically discover current software version + + automatically discover newer versions for the softwares + + calculate the obsolescence score SLI diff --git a/cmd/daemon.go b/cmd/daemon.go new file mode 100644 index 0000000..885dee3 --- /dev/null +++ b/cmd/daemon.go @@ -0,0 +1,89 @@ +package cmd + +import ( + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" + "github.com/qonto/upgrade-manager/config" + "github.com/qonto/upgrade-manager/internal/build" + "github.com/qonto/upgrade-manager/internal/app" + "github.com/qonto/upgrade-manager/internal/infra/http" + "github.com/qonto/upgrade-manager/internal/infra/kubernetes" + "go.uber.org/zap" +) + +func Run() error { + registry := prometheus.NewRegistry() + err := registry.Register(collectors.NewGoCollector()) + if err != nil { + return err + } + zapConfig := zap.NewProductionConfig() + if debug { + zapConfig.Level.SetLevel(zap.DebugLevel) + } + + logger, err := zapConfig.Build() + if err != nil { + return err + } + defer func() { + err = logger.Sync() + }() + + logger.Info(build.VersionMessage()) + signals := make(chan os.Signal, 1) + errChan := make(chan error) + if err != nil { + return err + } + signal.Notify( + signals, + syscall.SIGINT, + syscall.SIGTERM) + config, err := config.Load(configFilePath) + if err != nil { + return err + } + + server, err := http.New(registry, logger, config.HTTP) + if err != nil { + return err + } + + err = server.Start() + if err != nil { + return err + } + + client, err := kubernetes.NewClient(logger) + if err != nil { + return err + } + + a, err := app.New(logger, registry, client, config) + if err != nil { + return err + } + + a.Start() + + go func() { + for sig := range signals { + switch sig { + case syscall.SIGINT, syscall.SIGTERM: + logger.Info(fmt.Sprintf("Received signal %s, shutdown", sig)) + signal.Stop(signals) + a.Stop() + err := server.Stop() + errChan <- err + } + } + }() + exitErr := <-errChan + return exitErr +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..4806055 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,34 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +var ( + debug bool + configFilePath string +) + +func InitAndRunCommand() error { + rootCmd := &cobra.Command{ + Use: "root", + Short: "Run the main process", + } + startCmd := &cobra.Command{ + Use: "start", + Short: "Run the main process", + Run: func(cmd *cobra.Command, args []string) { + if err := Run(); err != nil { + fmt.Println(err) + os.Exit(3) + } + }, + } + rootCmd.AddCommand(startCmd) + startCmd.PersistentFlags().BoolVar(&debug, "debug", false, "set log-level to debug") + startCmd.PersistentFlags().StringVar(&configFilePath, "config-file", "config/config.yml", "set log-level to debug") + return rootCmd.Execute() +} diff --git a/config/_testdata/config-fail-wrong-interval.yml b/config/_testdata/config-fail-wrong-interval.yml new file mode 100644 index 0000000..239f994 --- /dev/null +++ b/config/_testdata/config-fail-wrong-interval.yml @@ -0,0 +1,29 @@ +global: + interval: wrong + aws: + region: eu-west-3 +sources: + argocdHelm: + - enabled: true + namespace: argocd + filters: + stabilityDays: 21 # minimum age for new releases to be considered stable (and add them as version candidates) + skipPrerelease: true # skip pre-release versions (ex: 1.2.3-beta) + skipFirstMajorVersion: true # skip major 0 versions (ex: 1.0.0, 2.0.0, 3.0.0 ...) + filesystemHelm: + - enabled: true + paths: + - "./internal/app/_testdata/test_chart/Chart.yaml" + - "./internal/app/_testdata/test_chart2/Chart.yaml" + - "./internal/app/_testdata/test_chart3/Chart.yaml" + - "./internal/app/_testdata/test_chart4/Chart.yaml" + filters: + stabilityDays: 21 + skipPrerelease: true + skipFirstMajorVersion: true +http: + host: 0.0.0.0 + port: 10000 + write-timeout: 10 + read-timeout: 10 + read-header-timeout: 10 diff --git a/config/_testdata/config-success.yml b/config/_testdata/config-success.yml new file mode 100644 index 0000000..8bdc1aa --- /dev/null +++ b/config/_testdata/config-success.yml @@ -0,0 +1,29 @@ +global: + interval: 2m + aws: + region: eu-west-3 +sources: + argocdHelm: + - enabled: true + namespace: argocd + filters: + stabilityDays: 21 # minimum age for new releases to be considered stable (and add them as version candidates) + skipPrerelease: true # skip pre-release versions (ex: 1.2.3-beta) + skipFirstMajorVersion: true # skip major 0 versions (ex: 1.0.0, 2.0.0, 3.0.0 ...) + filesystemHelm: + - enabled: true + paths: + - "./internal/app/_testdata/test_chart/Chart.yaml" + - "./internal/app/_testdata/test_chart2/Chart.yaml" + - "./internal/app/_testdata/test_chart3/Chart.yaml" + - "./internal/app/_testdata/test_chart4/Chart.yaml" + filters: + stabilityDays: 21 + skipPrerelease: true + skipFirstMajorVersion: true +http: + host: 0.0.0.0 + port: 10000 + write-timeout: 10 + read-timeout: 10 + read-header-timeout: 10 diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..831f391 --- /dev/null +++ b/config/config.go @@ -0,0 +1,55 @@ +package config + +import ( + "os" + "time" + + "github.com/go-playground/validator/v10" + "github.com/qonto/upgrade-manager/internal/app/sources/argohelm" + awsSource "github.com/qonto/upgrade-manager/internal/app/sources/aws" + "github.com/qonto/upgrade-manager/internal/app/sources/deployments" + "github.com/qonto/upgrade-manager/internal/app/sources/filesystemhelm" + awsinfra "github.com/qonto/upgrade-manager/internal/infra/aws" + "github.com/qonto/upgrade-manager/internal/infra/http" + "gopkg.in/yaml.v3" +) + +type Config struct { + Global GlobalConfig `yaml:"global" validate:"required"` + Sources Sources `yaml:"sources" validate:"required"` + HTTP http.Config `yaml:"http" validate:"required"` +} +type GlobalConfig struct { + Interval string `yaml:"interval" validate:"required"` + AwsConfig awsinfra.Config `yaml:"aws" validate:"required"` +} + +type Sources struct { + Deployments []deployments.Config `yaml:"deployments"` + ArgocdHelm []argohelm.Config `yaml:"argocdHelm"` + FsHelm []filesystemhelm.Config `yaml:"filesystemHelm"` + Aws awsSource.Config `yaml:"aws"` +} + +// Load and unmarshal config file +func Load(configFilePath string) (Config, error) { + var cfg Config + f, err := os.ReadFile(configFilePath) + if err != nil { + return cfg, err + } + err = yaml.Unmarshal(f, &cfg) + if err != nil { + return cfg, err + } + _, err = time.ParseDuration(cfg.Global.Interval) + if err != nil { + return cfg, err + } + validate := validator.New() + err = validate.Struct(cfg) + if err != nil { + return cfg, err + } + return cfg, nil +} diff --git a/config/config.yml b/config/config.yml new file mode 100644 index 0000000..4b37510 --- /dev/null +++ b/config/config.yml @@ -0,0 +1,87 @@ +global: + interval: 10m + aws: + region: us-east-1 +sources: + deployments: + - label-selector: + upgrade-manager.qonto.com/enabled: "true" + filters: + semver-versions: + remove-pre-release: true + remove-first-major-version: true + + registries: + : + enable-date-retrieval: true + auth: + aws: true + argocdHelm: + - enabled: true + argocd-namespace: argocd # namespace where the argocd application object is deployed + git-credentials-secrets-namespace: argocd # namespace where secrets containing git credentials are deployed + git-credentials-secrets-pattern: ".*-repo-.*" # regex to filter which secrets to fetch + filters: + semver-versions: + remove-pre-release: true + remove-first-major-version: true + recent-versions: + days: 21 # number of days since the version was released + destination-namespace: # namespace where the app resources will be deployed + include: [] + # ["kyverno"] # regular expression, if include is empty, it includes all namespaces + exclude: [] + # ["default", "temporary-*", "feature-*", "kube-system"] # regular expression if exclude is empty, it does not exclude any namespace + filesystemHelm: + - enabled: true + paths: + - "./internal/app/_testdata/test_chart/Chart.yaml" + - "./internal/app/_testdata/test_chart2/Chart.yaml" + - "./internal/app/_testdata/test_chart3/Chart.yaml" + - "./internal/app/_testdata/test_chart4/Chart.yaml" + filters: + semver-versions: + remove-pre-release: true + remove-first-major-version: true + recent-versions: + days: 21 + aws: + eks: + enabled: true + request-timeout: 15s + rds: + enabled: true + request-timeout: 15s + aggregation-level: cluster + msk: + enabled: true + request-timeout: 15s + elasticache: + enabled: true + request-timeout: 15s + lambda: + enabled: true + request-timeout: 15s + deprecated-runtimes-score: 100 # defaults to 100 + deprecated-runtimes: + - "nodejs" + - "nodejs4.3" + - "nodejs4.3-edge" + - "nodejs6.10" + - "nodejs8.10" + - "nodejs10.x" + - "nodejs12.x" + - "java8" + - "java8.al2" + - "python2.7" + - "python3.6" + - "dotnetcore1.0" + - "dotnetcore2.0" + - "dotnet6" + - "ruby2.5" +http: + host: 0.0.0.0 + port: 10000 + write-timeout: 10 + read-timeout: 10 + read-header-timeout: 10 diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..9b4f7cb --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,30 @@ +package config + +import ( + "testing" +) + +func TestLoadConfig(t *testing.T) { + testCases := []struct { + configFilePath string + expectedSuccess bool + }{ + { + configFilePath: "./_testdata/config-success.yml", + expectedSuccess: true, + }, + { + configFilePath: "./_testdata/config-fail-no-sources.yml", + expectedSuccess: false, + }, + } + for i, tc := range testCases { + _, err := Load(tc.configFilePath) + if err != nil && tc.expectedSuccess { + t.Errorf("Case %d, expected success but got error %s", i+1, err) + } + if err == nil && !tc.expectedSuccess { + t.Errorf("Case %d, expected error but got success", i+1) + } + } +} diff --git a/doc/2022-10-25-kubernetes-upgrade-kpi-diagram.png b/doc/2022-10-25-kubernetes-upgrade-kpi-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..342cdf7dababe8d6ce1ab7cccc0cd14c8fedef4a GIT binary patch literal 144367 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

U&^-%C{Bbd*E#S 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

ky$!^RoJaDt~?$TAsilNB*J2| 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 literal 0 HcmV?d00001 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..75ce591 --- /dev/null +++ b/go.mod @@ -0,0 +1,173 @@ +module github.com/qonto/upgrade-manager + +go 1.20 + +require ( + github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.20.0 + github.com/aws/aws-sdk-go-v2/config v1.18.25 + github.com/aws/aws-sdk-go-v2/service/elasticache v1.27.0 + github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1 + github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20230519004202-7f2db5bd753e + github.com/gin-gonic/gin v1.9.1 + github.com/go-git/go-git/v5 v5.7.0 + github.com/go-playground/validator/v10 v10.14.1 + github.com/google/go-containerregistry v0.12.1 + github.com/hashicorp/go-version v1.6.0 + github.com/prometheus/client_golang v1.15.1 + github.com/spf13/cobra v1.7.0 + github.com/stretchr/testify v1.8.3 + go.uber.org/zap v1.24.0 + gopkg.in/yaml.v3 v3.0.1 + helm.sh/helm/v3 v3.10.1 + k8s.io/api v0.25.3 + k8s.io/apimachinery v0.27.2 + k8s.io/client-go v0.25.3 +) + +require ( + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230528122434-6f98819771a1 // indirect + github.com/acomagu/bufpipe v1.0.4 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.13.24 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.37 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.31 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 // indirect + github.com/aws/aws-sdk-go-v2/service/ecr v1.18.11 // indirect + github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.16.2 // indirect + github.com/aws/aws-sdk-go-v2/service/eks v1.27.0 + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 // indirect + github.com/aws/aws-sdk-go-v2/service/rds v1.40.3 + github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.19.0 // indirect + github.com/aws/smithy-go v1.14.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/containerd/containerd v1.6.6 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/docker/cli v20.10.20+incompatible // indirect + github.com/docker/distribution v2.8.1+incompatible // indirect + github.com/docker/docker v20.10.20+incompatible // indirect + github.com/docker/docker-credential-helpers v0.7.0 // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-metrics v0.0.1 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/emicklei/go-restful/v3 v3.8.0 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/evanphx/json-patch v5.6.0+incompatible // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-errors/errors v1.0.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.4.1 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.1 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/btree v1.0.1 // indirect + github.com/google/gnostic v0.5.7-v3refs // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect + github.com/imdario/mergo v0.3.16 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/compress v1.15.11 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/moby/locker v1.0.1 // indirect + github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0-rc2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect + github.com/sergi/go-diff v1.3.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.0 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/xlab/treeprint v1.1.0 // indirect + go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/mod v0.10.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/oauth2 v0.5.0 // indirect + golang.org/x/sync v0.2.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/term v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect + golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect + golang.org/x/tools v0.9.3 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 // indirect + google.golang.org/grpc v1.47.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + k8s.io/cli-runtime v0.25.2 // indirect + k8s.io/klog/v2 v2.90.1 // indirect + k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect + k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect + oras.land/oras-go v1.2.0 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/kustomize/api v0.12.1 // indirect + sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) + +require ( + github.com/aws/aws-sdk-go-v2/service/kafka v1.22.1 + github.com/aws/aws-sdk-go-v2/service/lambda v1.34.1 + gopkg.in/yaml.v2 v2.4.0 +) + +require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/cloudflare/circl v1.3.3 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/klauspost/cpuid/v2 v2.2.5 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/skeema/knownhosts v1.1.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + golang.org/x/arch v0.3.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3059cbb --- /dev/null +++ b/go.sum @@ -0,0 +1,641 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/hcsshim v0.9.3 h1:k371PzBuRrz2b+ebGuI2nVgVhgsVX60jMfSw80NECxo= +github.com/ProtonMail/go-crypto v0.0.0-20230528122434-6f98819771a1 h1:JMDGhoQvXNTqH6Y3MC0IUw6tcZvaUdujNqzK2HYWZc8= +github.com/ProtonMail/go-crypto v0.0.0-20230528122434-6f98819771a1/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= +github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= +github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/aws/aws-sdk-go-v2 v1.17.3/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= +github.com/aws/aws-sdk-go-v2 v1.17.4/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= +github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= +github.com/aws/aws-sdk-go-v2 v1.20.0 h1:INUDpYLt4oiPOJl0XwZDK2OVAVf0Rzo+MGVTv9f+gy8= +github.com/aws/aws-sdk-go-v2 v1.20.0/go.mod h1:uWOr0m0jDsiWw8nnXiqZ+YG6LdvAlGYDLLf2NmHZoy4= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno= +github.com/aws/aws-sdk-go-v2/config v1.18.25 h1:JuYyZcnMPBiFqn87L2cRppo+rNwgah6YwD3VuyvaW6Q= +github.com/aws/aws-sdk-go-v2/config v1.18.25/go.mod h1:dZnYpD5wTW/dQF0rRNLVypB396zWCcPiBIvdvSWHEg4= +github.com/aws/aws-sdk-go-v2/credentials v1.13.24 h1:PjiYyls3QdCrzqUN35jMWtUK1vqVZ+zLfdOa/UPFDp0= +github.com/aws/aws-sdk-go-v2/credentials v1.13.24/go.mod h1:jYPYi99wUOPIFi0rhiOvXeSEReVOzBqFNOX5bXYoG2o= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 h1:jJPgroehGvjrde3XufFIJUZVK5A2L9a3KwSFgKy9n8w= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3/go.mod h1:4Q0UFP0YJf0NrsEuEYHpM9fTSEVnD16Z3uyEF7J9JGM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27/go.mod h1:a1/UpzeyBBerajpnP5nGZa9mGzsBn5cOKxm6NWQsvoI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28/go.mod h1:3lwChorpIM/BhImY/hy+Z6jekmN92cXGPI1QJasVPYY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.37 h1:zr/gxAZkMcvP71ZhQOcvdm8ReLjFgIXnIn0fw5AM7mo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.37/go.mod h1:Pdn4j43v49Kk6+82spO3Tu5gSeQXRsxo56ePPQAvFiA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21/go.mod h1:+Gxn8jYn5k9ebfHEqlhrMirFjSW0v0C9fI+KN5vk2kE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.22/go.mod h1:EqK7gVrIGAHyZItrD1D8B0ilgwMD1GiWAmbU4u/JHNk= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.31 h1:0HCMIkAkVY9KMgueD8tf4bRTUanzEYvhw7KkPXIMpO0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.31/go.mod h1:fTJDMe8LOFYtqiFFFeHA+SVMAwqLhoq0kcInYoLa9Js= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 h1:gGLG7yKaXG02/jBlg210R7VgQIotiQntNhsCFejawx8= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34/go.mod h1:Etz2dj6UHYuw+Xw830KfzCfWGMzqvUTCjUj5b76GVDc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 h1:AzwRi5OKKwo4QNqPf7TjeO+tK8AyOK3GVSwmRPo7/Cs= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25/go.mod h1:SUbB4wcbSEyCvqBxv/O/IBf93RbEze7U7OnoTlpPB+g= +github.com/aws/aws-sdk-go-v2/service/ecr v1.18.11 h1:wlTgmb/sCmVRJrN5De3CiHj4v/bTCgL5+qpdEd0CPtw= +github.com/aws/aws-sdk-go-v2/service/ecr v1.18.11/go.mod h1:Ce1q2jlNm8BVpjLaOnwnm5v2RClAbK6txwPljFzyW6c= +github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.16.2 h1:yflJrGmi1pXtP9lOpOeaNZyc0vXnJTuP2sor3nJcGGo= +github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.16.2/go.mod h1:uHtRE7aqXNmpeYL+7Ec7LacH5zC9+w2T5MBOeEKDdu0= +github.com/aws/aws-sdk-go-v2/service/eks v1.27.0 h1:ZXtMY5AgBS6YBtvrlKHSCLuIm5jtLKb/QaUhXH+vCsk= +github.com/aws/aws-sdk-go-v2/service/eks v1.27.0/go.mod h1:H/748RFDDxPmaxe03lhX0ufIQHIO2ctqjTfxuX4N7Vg= +github.com/aws/aws-sdk-go-v2/service/elasticache v1.27.0 h1:m0ZU6thCelJgtyNMnXYp39y/SY9qE4clxhs84sQM3jA= +github.com/aws/aws-sdk-go-v2/service/elasticache v1.27.0/go.mod h1:uSAGbZNsmIG2EXhWd7+6iKtwF15wWNtR5HLuRamlY5Q= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 h1:vGWm5vTpMr39tEZfQeDiDAMgk+5qsnvRny3FjLpnH5w= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28/go.mod h1:spfrICMD6wCAhjhzHuy6DOZZ+LAIY10UxhUmLzpJTTs= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.22/go.mod h1:xt0Au8yPIwYXf/GYPy/vl4K3CgwhfQMYbrH7DlUUIws= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 h1:0iKliEXAcCa2qVtRs7Ot5hItA2MsufrphbRFlz1Owxo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EOwBD4J4S5qYszS5/3DpkejfuK+Z5/1uzICfPaZLtqw= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 h1:NbWkRxEEIRSCqxhsHQuMiTH7yo+JZW1gp8v3elSVMTQ= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2/go.mod h1:4tfW5l4IAB32VWCDEBxCRtR9T4BWy4I4kr1spr8NgZM= +github.com/aws/aws-sdk-go-v2/service/kafka v1.22.1 h1:j6MlKSZyKzjs63bSdhLwrARLQ63eLGCJY7uQ/0PZf9A= +github.com/aws/aws-sdk-go-v2/service/kafka v1.22.1/go.mod h1:1i9P8wJupoac/btTK4ogmdWlGlTuiClDh3rvDF6lhE0= +github.com/aws/aws-sdk-go-v2/service/lambda v1.34.1 h1:1Q4cSbM9p1aLhs4GKuvyyj46YwJ/E0/2kubFViF4NtA= +github.com/aws/aws-sdk-go-v2/service/lambda v1.34.1/go.mod h1:i23nHcGEyswthctBfhEO1agGpM5Uyh83aSmSB6DmdCk= +github.com/aws/aws-sdk-go-v2/service/rds v1.40.3 h1:i8vKNkja4aJ6w9DR2a8T+HILObc6sCNztzlWAVClAPM= +github.com/aws/aws-sdk-go-v2/service/rds v1.40.3/go.mod h1:UFRMdSp7ok62LLFvZjnbAxPf+jfYwsjPEIYiqQYavJE= +github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1 h1:O+9nAy9Bb6bJFTpeNFtd9UfHbgxO1o4ZDAM9rQp5NsY= +github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1/go.mod h1:J9kLNzEiHSeGMyN7238EjJmBpCniVzFda75Gxl/NqB8= +github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 h1:UBQjaMTCKwyUYwiVnUt6toEJwGXsLBI6al083tpjJzY= +github.com/aws/aws-sdk-go-v2/service/sso v1.12.10/go.mod h1:ouy2P4z6sJN70fR3ka3wD3Ro3KezSxU6eKGQI2+2fjI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 h1:PkHIIJs8qvq0e5QybnZoG1K/9QTrLr9OsqCIo59jOBA= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10/go.mod h1:AFvkxc8xfBe8XA+5St5XIHHrQQtkxqrRincx4hmMHOk= +github.com/aws/aws-sdk-go-v2/service/sts v1.19.0 h1:2DQLAKDteoEDI8zpCzqBMaZlJuoE9iTYD0gFmXVax9E= +github.com/aws/aws-sdk-go-v2/service/sts v1.19.0/go.mod h1:BgQOMsg8av8jset59jelyPW7NoZcZXLVpDsXunGDrk8= +github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/aws/smithy-go v1.14.0 h1:+X90sB94fizKjDmwb4vyl2cTTPXTE5E2G/1mjByb0io= +github.com/aws/smithy-go v1.14.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20230519004202-7f2db5bd753e h1:hli0IOU73/tNWARHav2a41uMg7arHx0Qbhgcm4bDKXI= +github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20230519004202-7f2db5bd753e/go.mod h1:cheRroDS4qmOzi+Ue/oMHG4AV6n9F52W5QFdEKU59a0= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/containerd/cgroups v1.0.3 h1:ADZftAkglvCiD44c77s5YmMqaP2pzVCFZvBmAlBdAP4= +github.com/containerd/containerd v1.6.6 h1:xJNPhbrmz8xAMDNoVjHy9YHtWwEQNS+CDkcIRh7t8Y0= +github.com/containerd/containerd v1.6.6/go.mod h1:ZoP1geJldzCVY3Tonoz7b1IXk8rIX0Nltt5QE4OMNk0= +github.com/containerd/stargz-snapshotter/estargz v0.12.1 h1:+7nYmHJb0tEkcRaAW+MHqoKaJYZmkikupxCqVtmPuY0= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/distribution/v3 v3.0.0-20220526142353-ffbd94cbe269 h1:hbCT8ZPPMqefiAWD2ZKjn7ypokIGViTvBBg/ExLSdCk= +github.com/docker/cli v20.10.20+incompatible h1:lWQbHSHUFs7KraSN2jOJK7zbMS2jNCHI4mt4xUFUVQ4= +github.com/docker/cli v20.10.20+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= +github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v20.10.20+incompatible h1:kH9tx6XO+359d+iAkumyKDc5Q1kOwPuAUaeri48nD6E= +github.com/docker/docker v20.10.20+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0= +github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw= +github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= +github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8= +github.com/go-git/go-git/v5 v5.7.0 h1:t9AudWVLmqzlo+4bqdf7GY+46SUuRsx59SboFxkq2aE= +github.com/go-git/go-git/v5 v5.7.0/go.mod h1:coJHKEOk5kUClpsNlXrUvPrDxY3w3gjHvhcZd8Fodw8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8= +github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k= +github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-containerregistry v0.12.1 h1:W1mzdNUTx4Zla4JaixCRLhORcR7G6KxE5hHl5fkPsp8= +github.com/google/go-containerregistry v0.12.1/go.mod h1:sdIK+oHQO7B93xI8UweYdl887YhuIwg9vz8BSLH3+8k= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= +github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= +github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/sys/mountinfo v0.5.0 h1:2Ks8/r6lopsxWi9m58nlwjaeSzUX9iiL1vj5qB/9ObI= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/onsi/ginkgo/v2 v2.9.1 h1:zie5Ly042PD3bsCvsSOPvRnFwyo3rKe64TJlD6nu0mk= +github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= +github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= +github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skeema/knownhosts v1.1.1 h1:MTk78x9FPgDFVFkDLTrsnnfCJl7g1C/nnKvePgrIngE= +github.com/skeema/knownhosts v1.1.1/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk= +github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 h1:+FNtrFTmVw0YZGpBGX56XDee331t6JAXeK2bcyhLOOc= +go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= +golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM= +golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 h1:hrbNEivu7Zn1pxvHk6MBrq9iE22woVILTHqexqBxe6I= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +helm.sh/helm/v3 v3.10.1 h1:uTnNlYx8QcTSNA4ZJ50Llwife4CSohUY4ehumyVf2QE= +helm.sh/helm/v3 v3.10.1/go.mod h1:CXOcs02AYvrlPMWARNYNRgf2rNP7gLJQsi/Ubd4EDrI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.25.3 h1:Q1v5UFfYe87vi5H7NU0p4RXC26PPMT8KOpr1TLQbCMQ= +k8s.io/api v0.25.3/go.mod h1:o42gKscFrEVjHdQnyRenACrMtbuJsVdP+WVjqejfzmI= +k8s.io/apimachinery v0.27.2 h1:vBjGaKKieaIreI+oQwELalVG4d8f3YAMNpWLzDXkxeg= +k8s.io/apimachinery v0.27.2/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E= +k8s.io/cli-runtime v0.25.2 h1:XOx+SKRjBpYMLY/J292BHTkmyDffl/qOx3YSuFZkTuc= +k8s.io/cli-runtime v0.25.2/go.mod h1:OQx3+/0st6x5YpkkJQlEWLC73V0wHsOFMC1/roxV8Oc= +k8s.io/client-go v0.25.3 h1:oB4Dyl8d6UbfDHD8Bv8evKylzs3BXzzufLiO27xuPs0= +k8s.io/client-go v0.25.3/go.mod h1:t39LPczAIMwycjcXkVc+CB+PZV69jQuNx4um5ORDjQA= +k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= +k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg= +k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg= +k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY= +k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +oras.land/oras-go v1.2.0 h1:yoKosVIbsPoFMqAIFHTnrmOuafHal+J/r+I5bdbVWu4= +oras.land/oras-go v1.2.0/go.mod h1:pFNs7oHp2dYsYMSS82HaX5l4mpnGO7hbpPN6EWH2ltc= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/kustomize/api v0.12.1 h1:7YM7gW3kYBwtKvoY216ZzY+8hM+lV53LUayghNRJ0vM= +sigs.k8s.io/kustomize/api v0.12.1/go.mod h1:y3JUhimkZkR6sbLNwfJHxvo1TCLwuwm14sCYnkH6S1s= +sigs.k8s.io/kustomize/kyaml v0.13.9 h1:Qz53EAaFFANyNgyOEJbT/yoIHygK40/ZcvU3rgry2Tk= +sigs.k8s.io/kustomize/kyaml v0.13.9/go.mod h1:QsRbD0/KcU+wdk0/L0fIp2KLnohkVzs6fQ85/nOXac4= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/internal/app/_testdata/fakeSampleKey b/internal/app/_testdata/fakeSampleKey new file mode 100644 index 0000000..8c8d1ef --- /dev/null +++ b/internal/app/_testdata/fakeSampleKey @@ -0,0 +1,9 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS +1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQRGQf4vF/j68VOnkVuPApNjl3rNxTZn +tv5BPsmDYzp7mnrjugzLNJrqm5LmySjGZbjZTO91OuJ15XzBKsfz58h0AAAAuNqwJKHasC +ShAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEZB/i8X+PrxU6eR +W48Ck2OXes3FNme2/kE+yYNjOnuaeuO6DMs0muqbkubJKMZluNlM73U64nXlfMEqx/PnyH +QAAAAhAJBs79DhWi2H4gBralmmj5X/t24YwnFHgiE3SpBAzyltAAAAG2NocmlzdG9waGVj +b2xsb3RASjE2UDJYN0pSNwECAwQ= +-----END OPENSSH PRIVATE KEY----- diff --git a/internal/app/_testdata/index-traefik.yaml b/internal/app/_testdata/index-traefik.yaml new file mode 100644 index 0000000..5380d25 --- /dev/null +++ b/internal/app/_testdata/index-traefik.yaml @@ -0,0 +1,5624 @@ +apiVersion: v1 +entries: + traefik: + - annotations: + artifacthub.io/changes: "- \U0001F3A8 Don't require exposed Ports when enabling + Hub\n" + apiVersion: v2 + appVersion: 2.9.4 + created: "2022-11-03T10:23:17.796696491Z" + description: A Traefik based Kubernetes ingress controller + digest: 9aa86fb7f08895353d6a208578b6f7b03a5685096781cf12e2190c669f3b020c + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + - email: michel.loiseleur@traefik.io + name: mloiseleur + - email: charlie.haley@traefik.io + name: charlie-haley + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-19.0.3.tgz + version: 19.0.3 + - annotations: + artifacthub.io/changes: "- \U0001F4AC Support volume secrets with '.' in name\n" + apiVersion: v2 + appVersion: 2.9.4 + created: "2022-11-03T10:23:17.792926226Z" + description: A Traefik based Kubernetes ingress controller + digest: a2fd364dd542aa2138aa07cf1a8c7e5f5c6c6629b7ebba4cfeb0d480fdafddc8 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + - email: michel.loiseleur@traefik.io + name: mloiseleur + - email: charlie.haley@traefik.io + name: charlie-haley + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-19.0.2.tgz + version: 19.0.2 + - annotations: + artifacthub.io/changes: "- \U0001F41B Fix IngressClass install on EKS\n" + apiVersion: v2 + appVersion: 2.9.4 + created: "2022-11-03T10:23:17.788937157Z" + description: A Traefik based Kubernetes ingress controller + digest: 0a6480df7ef2663d0f4abddf5318d63e27063c832157c9ed9634af76c276471c + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + - email: michel.loiseleur@traefik.io + name: mloiseleur + - email: charlie.haley@traefik.io + name: charlie-haley + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-19.0.1.tgz + version: 19.0.1 + - annotations: + artifacthub.io/changes: | + - ✨ Provides Default IngressClass for Traefik by default + apiVersion: v2 + appVersion: 2.9.4 + created: "2022-11-03T10:23:17.784929388Z" + description: A Traefik based Kubernetes ingress controller + digest: f7e26519845320f978c03d6ba70a6ce295b12b12607cc5fc30a4d74896cfb787 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + - email: michel.loiseleur@traefik.io + name: mloiseleur + - email: charlie.haley@traefik.io + name: charlie-haley + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-19.0.0.tgz + version: 19.0.0 + - annotations: + artifacthub.io/changes: | + - ⬆️ Update Traefik appVersion to 2.9.4 + apiVersion: v2 + appVersion: 2.9.4 + created: "2022-11-03T10:23:17.779950903Z" + description: A Traefik based Kubernetes ingress controller + digest: 1aa33e6c51104f141e50aa24eefc9bf12f4e8af179a7058a6267b544cd56459a + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + - email: michel.loiseleur@traefik.io + name: mloiseleur + - email: charlie.haley@traefik.io + name: charlie-haley + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-18.3.0.tgz + version: 18.3.0 + - annotations: + artifacthub.io/changes: "- \U0001F6A9 Add an optional \"internal\" service\n- + \U0001F4DD Improve documentation on `single` service\n" + apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.776130337Z" + description: A Traefik based Kubernetes ingress controller + digest: 6cd406c294f8fd73a09221bb3629a6a9dedd58497daf8c1a425425fa9133ec67 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + - email: michel.loiseleur@traefik.io + name: mloiseleur + - email: charlie.haley@traefik.io + name: charlie-haley + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-18.2.0.tgz + version: 18.2.0 + - annotations: + artifacthub.io/changes: "- \U0001F680 Add support for Traefik Hub\n" + apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.772301871Z" + description: A Traefik based Kubernetes ingress controller + digest: 6c836280408a3476f2792f6a6f31dfa5cd1b5a8c523d8fd59d135f7f9cf3fc3b + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + - email: michel.loiseleur@traefik.io + name: mloiseleur + - email: charlie.haley@traefik.io + name: charlie-haley + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-18.1.0.tgz + version: 18.1.0 + - annotations: + artifacthub.io/changes: | + - Provides single service using `MixedProtocolLBService`, by default. + - Dual service is still possible by setting `service.single=false` + - Refactor and improve http3 deployments + apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.768199901Z" + description: A Traefik based Kubernetes ingress controller + digest: 25131b0306b9725937b0d37ceb35831628545fd9e7a41c6664c56b32a3702246 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + - email: michel.loiseleur@traefik.io + name: mloiseleur + - email: charlie.haley@traefik.io + name: charlie-haley + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-18.0.0.tgz + version: 18.0.0 + - annotations: + artifacthub.io/changes: "- \U0001F4DD Add annotations changelog for artifacthub.io + & update Maintainers\n" + apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.763723624Z" + description: A Traefik based Kubernetes ingress controller + digest: 7f4594fb4c6957490a195d8a312fd54b9b218443ba374e8a6c63f6a54a738439 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + - email: michel.loiseleur@traefik.io + name: mloiseleur + - email: charlie.haley@traefik.io + name: charlie-haley + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-17.0.5.tgz + version: 17.0.5 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.760074161Z" + description: A Traefik based Kubernetes ingress controller + digest: c4f1043a61007fd60d36a2b78a653b02c132c0559d7eb58eaaf42304bba17b19 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-17.0.4.tgz + version: 17.0.4 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.756422098Z" + description: A Traefik based Kubernetes ingress controller + digest: 1e7df87751bfd327b97a05646be5773086dcaf1778cd0d59064d4ec2aefc07f5 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-17.0.3.tgz + version: 17.0.3 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.751518814Z" + description: A Traefik based Kubernetes ingress controller + digest: a60702624b5087c18c349c0243b6a2176480deb04722fa8560664c4ea523bad6 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-17.0.2.tgz + version: 17.0.2 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.747876051Z" + description: A Traefik based Kubernetes ingress controller + digest: 5561fb490f43a7f045c5be025e4298b3d6cd3bc55c885174d17f5db1e281f475 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-17.0.1.tgz + version: 17.0.1 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.744303589Z" + description: A Traefik based Kubernetes ingress controller + digest: f1af940a0ae242f2487ea0ad0b57d4daa0d379e0425254a3d457547aa2d8b322 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-17.0.0.tgz + version: 17.0.0 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.740668027Z" + description: A Traefik based Kubernetes ingress controller + digest: c1b5d1d7edb259fe7b07097f1a9445f642562e70b165977a4d1fbd0313ae31d5 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-16.2.0.tgz + version: 16.2.0 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.736691959Z" + description: A Traefik based Kubernetes ingress controller + digest: e370106b69f2d58ab104ed5d81eb03491eb2aa04ac03b019c6cae7ed5181dffe + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-16.1.0.tgz + version: 16.1.0 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.732588788Z" + description: A Traefik based Kubernetes ingress controller + digest: 394d39cb7fa830c99ea3ccf2794a973e15e0ed4b8bc4c44d9209045477184329 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-16.0.0.tgz + version: 16.0.0 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.729090428Z" + description: A Traefik based Kubernetes ingress controller + digest: 003239ee297582c2cf59a5675bb2e9a26b96cc4c6087ca64b40f2c1e5f1c3171 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-15.3.1.tgz + version: 15.3.1 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.725497766Z" + description: A Traefik based Kubernetes ingress controller + digest: 24f50a6cffdf3decc0f9f0b541543b89ece888f518602a9c5184049fd6d60bb7 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-15.3.0.tgz + version: 15.3.0 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.72051058Z" + description: A Traefik based Kubernetes ingress controller + digest: e1c2709aeea12157868fa8dbfb60015f6726f2e760ba63adc6d55f4c33edd09c + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-15.2.2.tgz + version: 15.2.2 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.716932019Z" + description: A Traefik based Kubernetes ingress controller + digest: 202224ef0bf62b8fbe3d36ec9581759146b736d7f0bd282bc7c4330b991c3725 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-15.2.1.tgz + version: 15.2.1 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.713359857Z" + description: A Traefik based Kubernetes ingress controller + digest: b3f074b8c1754dc4064126c147f52b2265e939924b390eb9ced27e9fd87d57c4 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-15.2.0.tgz + version: 15.2.0 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.709526891Z" + description: A Traefik based Kubernetes ingress controller + digest: 5a89230719dd7befafa52c9a51c0057956b12bcb27eaad52059de5cfae017424 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-15.1.1.tgz + version: 15.1.1 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.704684208Z" + description: A Traefik based Kubernetes ingress controller + digest: 26ee284e75121f7862f8869be50b3874be3dcda6bad4c1557eb77ee8c6d51457 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-15.1.0.tgz + version: 15.1.0 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.701018745Z" + description: A Traefik based Kubernetes ingress controller + digest: 48d64b03074a5a21c57a37b35d4abdc5c650a58f3c90d9a104ed44c07e897363 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-15.0.0.tgz + version: 15.0.0 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.697481584Z" + description: A Traefik based Kubernetes ingress controller + digest: f795d7379b32fd6f34c6a17c7d72bd31fa1d88577bc559518783ad8ff75b55eb + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-14.0.2.tgz + version: 14.0.2 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.692044091Z" + description: A Traefik based Kubernetes ingress controller + digest: 0204907f8b31a3e25feab1b4797eba527ab0d5e05b49e2a274544d9a5021af95 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-14.0.1.tgz + version: 14.0.1 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.689391545Z" + description: A Traefik based Kubernetes ingress controller + digest: f5864223fbfe7bc724f9a9d9038411f9d21d07ad1a8fb602acb6de35dc0646ae + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-14.0.0.tgz + version: 14.0.0 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.686677198Z" + description: A Traefik based Kubernetes ingress controller + digest: db3dd496b2d28bc80d389e567119ed0808d2347f6ee26e1b2c257a256cce07e1 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-13.0.1.tgz + version: 13.0.1 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.683993052Z" + description: A Traefik based Kubernetes ingress controller + digest: 18c3e586e9a52e0c0059744dc244ac99b5bd5d2fe6cecdd6c6c4d387fa0f8e4c + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-13.0.0.tgz + version: 13.0.0 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.681134603Z" + description: A Traefik based Kubernetes ingress controller + digest: 93c05f4f6f0852dda3e6cc4de90d6c1283de799b65517e0c3abbf1422119d5c0 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-12.0.7.tgz + version: 12.0.7 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.678148052Z" + description: A Traefik based Kubernetes ingress controller + digest: e8986c7855bf6ead7c3eafc705faf106cd3e53a73ac32ad6e5c6ecac3b734eaf + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-12.0.6.tgz + version: 12.0.6 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.674507989Z" + description: A Traefik based Kubernetes ingress controller + digest: 8571f2dbc691b4e3998b1a4e0dd4fd17f00dff0e673c9c9467126e9fecdf88e0 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-12.0.5.tgz + version: 12.0.5 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.671840043Z" + description: A Traefik based Kubernetes ingress controller + digest: 7dc604f0b28605884ef6607f8f1cdc3aafa85467cc77ea4021d6443a99f043a8 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-12.0.4.tgz + version: 12.0.4 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.669225898Z" + description: A Traefik based Kubernetes ingress controller + digest: 3aded722109150bb7ce842db58023df526ed534754b42186b7a6c3db5f6cb941 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-12.0.3.tgz + version: 12.0.3 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.666573052Z" + description: A Traefik based Kubernetes ingress controller + digest: abc7fc3beaefbe5063ae624d4c37b560d4ce5141a6ce00c2736dadf161361e74 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-12.0.2.tgz + version: 12.0.2 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.663889906Z" + description: A Traefik based Kubernetes ingress controller + digest: 8883815f43a5edf6fe58e3db7ee4ffc7dba7265361e4ba0885b490b6a87d7671 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-12.0.1.tgz + version: 12.0.1 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.660945856Z" + description: A Traefik based Kubernetes ingress controller + digest: 2a3813956d3a454351762993c8f19bd77480764d575757602b4d8de8c28915eb + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-12.0.0.tgz + version: 12.0.0 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.657472496Z" + description: A Traefik based Kubernetes ingress controller + digest: 03b4be3fa36a6285727d38246ae8e99e173f69e56dbb5b0a67289b3d4180c3d8 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-11.1.1.tgz + version: 11.1.1 + - apiVersion: v2 + appVersion: 2.9.1 + created: "2022-11-03T10:23:17.65480755Z" + description: A Traefik based Kubernetes ingress controller + digest: cb4c533845637d6187eae35854a05fe5e827a82807b5f3c1162913f8b53f56da + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-11.1.0.tgz + version: 11.1.0 + - apiVersion: v2 + appVersion: 2.8.7 + created: "2022-11-03T10:23:17.652150804Z" + description: A Traefik based Kubernetes ingress controller + digest: 04f39b25b69edef6e18175a2186b076e6763d0a5b93f7399b0fd9148f623dfd9 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-11.0.0.tgz + version: 11.0.0 + - apiVersion: v2 + appVersion: 2.8.7 + created: "2022-11-03T10:23:17.620399958Z" + description: A Traefik based Kubernetes ingress controller + digest: c6dc839f56b93449a887c66a72e5822b7e08f49861734ed83be4428b67d8b9a6 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.33.0.tgz + version: 10.33.0 + - apiVersion: v2 + appVersion: 2.8.7 + created: "2022-11-03T10:23:17.617791813Z" + description: A Traefik based Kubernetes ingress controller + digest: 0523a72ab333eb41f113b58fc66aa2a10efed0c1307bbd636c9c892604f2e4f3 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.32.0.tgz + version: 10.32.0 + - apiVersion: v2 + appVersion: 2.8.7 + created: "2022-11-03T10:23:17.615155968Z" + description: A Traefik based Kubernetes ingress controller + digest: 774b33ea1610156a881dd79d929413cba0b2675202fa19b0201ea8ac52eb9216 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.31.0.tgz + version: 10.31.0 + - apiVersion: v2 + appVersion: 2.8.7 + created: "2022-11-03T10:23:17.612498022Z" + description: A Traefik based Kubernetes ingress controller + digest: f37f63b95de98a4f3a4a80c4332215ce2c8b4ac5d6f12fac35f1dc878e25d00b + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.30.1.tgz + version: 10.30.1 + - apiVersion: v2 + appVersion: 2.8.7 + created: "2022-11-03T10:23:17.60829955Z" + description: A Traefik based Kubernetes ingress controller + digest: f90865148b0ccea7e062a5f3667e7c2c0177a4f402198cf4ac99e3ceed84ced2 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.30.0.tgz + version: 10.30.0 + - apiVersion: v2 + appVersion: 2.8.7 + created: "2022-11-03T10:23:17.589488227Z" + description: A Traefik based Kubernetes ingress controller + digest: cf6a7fcdc6e608274046edc6a5ff984fc650e8235ed530ab0389dc26c5bd67a5 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.29.0.tgz + version: 10.29.0 + - apiVersion: v2 + appVersion: 2.8.7 + created: "2022-11-03T10:23:17.586873582Z" + description: A Traefik based Kubernetes ingress controller + digest: d5d1953780c72cf57879f598fcc11d74d1da54a9c816165da6007e3c90860bd4 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.28.0.tgz + version: 10.28.0 + - apiVersion: v2 + appVersion: 2.8.7 + created: "2022-11-03T10:23:17.584294937Z" + description: A Traefik based Kubernetes ingress controller + digest: d65bacbc39040ca27c5d752c6c883b68706ee4ebc630aae0dc96d9c7bf4b86f3 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.27.0.tgz + version: 10.27.0 + - apiVersion: v2 + appVersion: 2.8.7 + created: "2022-11-03T10:23:17.581644792Z" + description: A Traefik based Kubernetes ingress controller + digest: 8e2d7fdbbb5fae74d571663137149c19e5681d174833fcfe160f42ebe7ef923b + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.26.1.tgz + version: 10.26.1 + - apiVersion: v2 + appVersion: 2.8.7 + created: "2022-11-03T10:23:17.577787425Z" + description: A Traefik based Kubernetes ingress controller + digest: b00a6acd43e766d60accb9c0d50ca84e36659c37fa7bfa1490bf8b053c975173 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.26.0.tgz + version: 10.26.0 + - apiVersion: v2 + appVersion: 2.8.7 + created: "2022-11-03T10:23:17.575264582Z" + description: A Traefik based Kubernetes ingress controller + digest: 218f3d55fb32e9c46bfb56c08654e2e1422a47d031ce48456d116be540be050f + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.25.1.tgz + version: 10.25.1 + - apiVersion: v2 + appVersion: 2.8.7 + created: "2022-11-03T10:23:17.572729738Z" + description: A Traefik based Kubernetes ingress controller + digest: c59e787b1ac6d96fcca7798f14d9e98b74c1d65c3eb88f012d0b7f8829051d3c + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.25.0.tgz + version: 10.25.0 + - apiVersion: v2 + appVersion: 2.8.7 + created: "2022-11-03T10:23:17.570237995Z" + description: A Traefik based Kubernetes ingress controller + digest: 960ff003d822efa669ae467c174a664b326f49b005d085b2430d095f03d62967 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.24.5.tgz + version: 10.24.5 + - apiVersion: v2 + appVersion: 2.8.7 + created: "2022-11-03T10:23:17.567718952Z" + description: A Traefik based Kubernetes ingress controller + digest: a5a09bbaff6475b4879422848b0a7edf9d9390f1077a33c5f874c543b2c78e0b + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.24.4.tgz + version: 10.24.4 + - apiVersion: v2 + appVersion: 2.8.5 + created: "2022-11-03T10:23:17.565208709Z" + description: A Traefik based Kubernetes ingress controller + digest: cf2c3b1ffa07132e9ba2b2513f89ccff28538c4f8986d72809c6c7ea56fd44fe + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.24.3.tgz + version: 10.24.3 + - apiVersion: v2 + appVersion: 2.8.4 + created: "2022-11-03T10:23:17.561957053Z" + description: A Traefik based Kubernetes ingress controller + digest: 66c7edb5f3d37fa422837f8169d4e11e9febaf55c1a79f6c8cad6602e031232c + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.24.2.tgz + version: 10.24.2 + - apiVersion: v2 + appVersion: 2.8.0 + created: "2022-11-03T10:23:17.559116204Z" + description: A Traefik based Kubernetes ingress controller + digest: 12b31f7e28118743bcb8086ff9c37ccc7506812a320fae87ad5e86e198e4e938 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.24.1.tgz + version: 10.24.1 + - apiVersion: v2 + appVersion: 2.8.0 + created: "2022-11-03T10:23:17.556605061Z" + description: A Traefik based Kubernetes ingress controller + digest: ec878db0b3ca36dda265c334b07e798dfefe156fca31b4f91bdbe8ae598fb7ca + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.24.0.tgz + version: 10.24.0 + - apiVersion: v2 + appVersion: 2.7.1 + created: "2022-11-03T10:23:17.554106518Z" + description: A Traefik based Kubernetes ingress controller + digest: 8e67e0e08d1de9e4c485f487b565f80660bd8f26f02f07b7d40a876ba92642e7 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.23.0.tgz + version: 10.23.0 + - apiVersion: v2 + appVersion: 2.7.1 + created: "2022-11-03T10:23:17.551868079Z" + description: A Traefik based Kubernetes ingress controller + digest: 85e0c585827f23aca4bc722f6af6e2113fbb6ddbe7d213fd22c4dd0950a7fc5a + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.22.0.tgz + version: 10.22.0 + - apiVersion: v2 + appVersion: 2.7.1 + created: "2022-11-03T10:23:17.549658241Z" + description: A Traefik based Kubernetes ingress controller + digest: c82bef1cd04c75a687823677d951f833892fb11ddd7dec5392d744b817d1d43a + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.21.1.tgz + version: 10.21.1 + - apiVersion: v2 + appVersion: 2.7.0 + created: "2022-11-03T10:23:17.546190782Z" + description: A Traefik based Kubernetes ingress controller + digest: 17a55cfeaeb97318d4fcdb385f3bd89493f18d9ca5ff98ca14f1ad2a264d2945 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.21.0.tgz + version: 10.21.0 + - apiVersion: v2 + appVersion: 2.7.0 + created: "2022-11-03T10:23:17.543647938Z" + description: A Traefik based Kubernetes ingress controller + digest: 5a86f138813ca7b38305d2719a6cdceb5068307751abf577ef0b90752986d9fc + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.20.1.tgz + version: 10.20.1 + - apiVersion: v2 + appVersion: 2.7.0 + created: "2022-11-03T10:23:17.541476501Z" + description: A Traefik based Kubernetes ingress controller + digest: ff5621ff51d80f0469680000541a2a14fc0309ae8908516fe6b090bc05f03a1d + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.20.0.tgz + version: 10.20.0 + - apiVersion: v2 + appVersion: 2.6.6 + created: "2022-11-03T10:23:17.537253928Z" + description: A Traefik based Kubernetes ingress controller + digest: b3f341bd4ef758e3bbc9f627c13f0c9b9633d3680edea9b9bddf4e3512821904 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.19.5.tgz + version: 10.19.5 + - apiVersion: v2 + appVersion: 2.6.3 + created: "2022-11-03T10:23:17.53502159Z" + description: A Traefik based Kubernetes ingress controller + digest: ef2435980f516d6cef57c98ea578d59f54a7fa663b842a4370eb3ae480dbd00d + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.19.4.tgz + version: 10.19.4 + - apiVersion: v2 + appVersion: 2.6.2 + created: "2022-11-03T10:23:17.532168541Z" + description: A Traefik based Kubernetes ingress controller + digest: a88084cd568fcfa960ed3f23b395975449caa7fcf5cfda987e5b9f92222def13 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.19.3.tgz + version: 10.19.3 + - apiVersion: v2 + appVersion: 2.6.2 + created: "2022-11-03T10:23:17.529144689Z" + description: A Traefik based Kubernetes ingress controller + digest: b10396daa430c8ffdae035b1e581a410998df84e845152d465901a04f179b01c + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.19.2.tgz + version: 10.19.2 + - apiVersion: v2 + appVersion: 2.6.3 + created: "2022-11-03T10:23:17.526982951Z" + description: A Traefik based Kubernetes ingress controller + digest: bd02bb82bc1274696720db04591d6961f661c542fad3a8e7d906c3720d8541f8 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.19.1.tgz + version: 10.19.1 + - apiVersion: v2 + appVersion: 2.6.2 + created: "2022-11-03T10:23:17.524831614Z" + description: A Traefik based Kubernetes ingress controller + digest: 4a1ca1d178c7fe6bd1bb6ce747a5b0c5781a159557b7019eca93043054c5dead + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.19.0.tgz + version: 10.19.0 + - apiVersion: v2 + appVersion: 2.6.2 + created: "2022-11-03T10:23:17.522672577Z" + description: A Traefik based Kubernetes ingress controller + digest: 456bdcbeb44a91a4867d2f7abf852d657647d6cc6ea98e27d15194429abd2fbc + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.18.0.tgz + version: 10.18.0 + - apiVersion: v2 + appVersion: 2.6.2 + created: "2022-11-03T10:23:17.52052274Z" + description: A Traefik based Kubernetes ingress controller + digest: f061220e2f6eb9d029d7bcd3ab47e10621522b96ab92f31e8939983f5a5c97ec + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.17.0.tgz + version: 10.17.0 + - apiVersion: v2 + appVersion: 2.6.2 + created: "2022-11-03T10:23:17.518430804Z" + description: A Traefik based Kubernetes ingress controller + digest: 004b72c50d1a635a046f6099ea625dc67a72720142b14b8bf9997b1dc103f724 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.16.1.tgz + version: 10.16.1 + - apiVersion: v2 + appVersion: 2.6.1 + created: "2022-11-03T10:23:17.514728141Z" + description: A Traefik based Kubernetes ingress controller + digest: 0f63d3d7e6eb1e74669347a6be4c9f24880e77320bded6bc004bdd00b45c2abf + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.16.0.tgz + version: 10.16.0 + - apiVersion: v2 + appVersion: 2.6.1 + created: "2022-11-03T10:23:17.512628305Z" + description: A Traefik based Kubernetes ingress controller + digest: 452d9f8b808eff71c500bdee16af7391554da1c65bdc5a3b0f8253f56f09ffed + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.15.0.tgz + version: 10.15.0 + - apiVersion: v2 + appVersion: 2.6.1 + created: "2022-11-03T10:23:17.510554169Z" + description: A Traefik based Kubernetes ingress controller + digest: aadeb9b9b17574f049b78be237e3a85affbd1bfdfb50201a8d712d39c563ee5d + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.14.2.tgz + version: 10.14.2 + - apiVersion: v2 + appVersion: 2.6.0 + created: "2022-11-03T10:23:17.508491533Z" + description: A Traefik based Kubernetes ingress controller + digest: b2469dc4b309cb8532e8be89b21eeb5a69f6119cf385c8ed81d99e4e55644625 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.14.1.tgz + version: 10.14.1 + - apiVersion: v2 + appVersion: 2.6.0 + created: "2022-11-03T10:23:17.506440998Z" + description: A Traefik based Kubernetes ingress controller + digest: 1294ea78aecd392103e60b7fe3c40d2cdc1afc0da8314d63b108eb804a207872 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.14.0.tgz + version: 10.14.0 + - apiVersion: v2 + appVersion: 2.6.0 + created: "2022-11-03T10:23:17.504331362Z" + description: A Traefik based Kubernetes ingress controller + digest: ff9a9b6721ce4a06dd36977ed8e980f7ee4514838f06d49254eee652fd4ef7aa + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.13.0.tgz + version: 10.13.0 + - apiVersion: v2 + appVersion: 2.6.0 + created: "2022-11-03T10:23:17.500837402Z" + description: A Traefik based Kubernetes ingress controller + digest: c53e573557035600c0eb1daf02c13119528e825090f93b37ff686217b2e0e8ac + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.12.0.tgz + version: 10.12.0 + - apiVersion: v2 + appVersion: 2.6.0 + created: "2022-11-03T10:23:17.498819867Z" + description: A Traefik based Kubernetes ingress controller + digest: 5e0c57d4a7807f4cf6e35da7dbdf8c0052024641bb7939e3c168aae9f4ac7579 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.11.1.tgz + version: 10.11.1 + - apiVersion: v2 + appVersion: 2.6.0 + created: "2022-11-03T10:23:17.496745631Z" + description: A Traefik based Kubernetes ingress controller + digest: c900751ff939ea7dfe1202f8063fb40b9840e6bb9eee56b9b098c5fdaa64ba78 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.11.0.tgz + version: 10.11.0 + - apiVersion: v2 + appVersion: 2.6.0 + created: "2022-11-03T10:23:17.494736497Z" + description: A Traefik based Kubernetes ingress controller + digest: 078650f6b8bef81f073dfdc00e148f7ff3ed8b9fababbc4d8d8a973143b400ac + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.10.0.tgz + version: 10.10.0 + - apiVersion: v2 + appVersion: 2.5.6 + created: "2022-11-03T10:23:17.649522459Z" + description: A Traefik based Kubernetes ingress controller + digest: 5ccd432e87d4d7310415e0796cd43b720bf5d36817b13a0f3fb48587ced0b864 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.9.1.tgz + version: 10.9.1 + - apiVersion: v2 + appVersion: 2.5.4 + created: "2022-11-03T10:23:17.647397223Z" + description: A Traefik based Kubernetes ingress controller + digest: b9ba42a1af25fa5797b09662cca6ce748e79c34dfce0f39da943d525d4fd5fba + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.9.0.tgz + version: 10.9.0 + - apiVersion: v2 + appVersion: 2.5.4 + created: "2022-11-03T10:23:17.644104166Z" + description: A Traefik based Kubernetes ingress controller + digest: 8651044f6f73edf24b31fde524918f1b0b9ad94546012c2747bfb5679e3673b7 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.8.0.tgz + version: 10.8.0 + - apiVersion: v2 + appVersion: 2.5.4 + created: "2022-11-03T10:23:17.642085031Z" + description: A Traefik based Kubernetes ingress controller + digest: 0766d04c850b06d8fa85186fca4cf625fce3da1a0b852f37bfaba733c44ab4f9 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.7.1.tgz + version: 10.7.1 + - apiVersion: v2 + appVersion: 2.5.4 + created: "2022-11-03T10:23:17.640017396Z" + description: A Traefik based Kubernetes ingress controller + digest: c405970afa2b4b4e77f7effa3fd313da1b6b4556160e7535f77403953dd75cf2 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.7.0.tgz + version: 10.7.0 + - apiVersion: v2 + appVersion: 2.5.4 + created: "2022-11-03T10:23:17.637995661Z" + description: A Traefik based Kubernetes ingress controller + digest: d80163b2a3397a3365a77b8945c0084a1695b7f49b2f815384e8ad5e6a761f23 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.6.2.tgz + version: 10.6.2 + - apiVersion: v2 + appVersion: 2.5.3 + created: "2022-11-03T10:23:17.635931825Z" + description: A Traefik based Kubernetes ingress controller + digest: d7f3501d7a05521532ebb8e589aeca7d2891aadfedfb54ade11567cdf067001f + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.6.1.tgz + version: 10.6.1 + - apiVersion: v2 + appVersion: 2.5.3 + created: "2022-11-03T10:23:17.633805089Z" + description: A Traefik based Kubernetes ingress controller + digest: 1c05df449fabf47ce3ce3cfef4d1e71f09fc675027cf32b8f3af21753c37c678 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.6.0.tgz + version: 10.6.0 + - apiVersion: v2 + appVersion: 2.5.3 + created: "2022-11-03T10:23:17.631710053Z" + description: A Traefik based Kubernetes ingress controller + digest: d32972e775a53adcd6ea5746f02023b82554b16639712012e8f33d46c8ce40d8 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.5.0.tgz + version: 10.5.0 + - apiVersion: v2 + appVersion: 2.5.3 + created: "2022-11-03T10:23:17.627953788Z" + description: A Traefik based Kubernetes ingress controller + digest: 4a26b34e65d115421a00e938e72bfaf7007de225a535a2b7d398f8b4da1febc6 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.4.2.tgz + version: 10.4.2 + - apiVersion: v2 + appVersion: 2.5.3 + created: "2022-11-03T10:23:17.624407327Z" + description: A Traefik based Kubernetes ingress controller + digest: 0060e8b07343cf66e0a0c721de318788f1679460b69f5d2e2773636220505a0b + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.4.1.tgz + version: 10.4.1 + - apiVersion: v2 + appVersion: 2.5.3 + created: "2022-11-03T10:23:17.622417193Z" + description: A Traefik based Kubernetes ingress controller + digest: 700eb5d9c15cb97d5194363ba874ca7d80563a452a00c89f847ce14e2a9799db + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.4.0.tgz + version: 10.4.0 + - apiVersion: v2 + appVersion: 2.5.3 + created: "2022-11-03T10:23:17.605735406Z" + description: A Traefik based Kubernetes ingress controller + digest: 3eafe628c1f9ead398fc3532768d5527444d0c2e099aa2f49e2af6e0f786fae3 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.3.6.tgz + version: 10.3.6 + - apiVersion: v2 + appVersion: 2.5.3 + created: "2022-11-03T10:23:17.603691371Z" + description: A Traefik based Kubernetes ingress controller + digest: 38a4ea48e3b8cec05b96e00f44f0a0b41ab5edc3ea0fece6cd89fda60119a6fc + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.3.5.tgz + version: 10.3.5 + - apiVersion: v2 + appVersion: 2.5.1 + created: "2022-11-03T10:23:17.601660336Z" + description: A Traefik based Kubernetes ingress controller + digest: 238dffb06911d0bb4cf35d4776e7a3726c1fb9d6bbcd5a7b52db631ded1204bc + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.3.4.tgz + version: 10.3.4 + - apiVersion: v2 + appVersion: 2.5.1 + created: "2022-11-03T10:23:17.599607301Z" + description: A Traefik based Kubernetes ingress controller + digest: 3b88cd5cb204db5ced4ad84546c4b39b00013a4337104d76983f48593bad11a5 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.3.3.tgz + version: 10.3.3 + - apiVersion: v2 + appVersion: 2.5.1 + created: "2022-11-03T10:23:17.597574866Z" + description: A Traefik based Kubernetes ingress controller + digest: 9bdb6b3ea742426ad7788397127e85182c61091cee437f119819c39db743e069 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.3.2.tgz + version: 10.3.2 + - apiVersion: v2 + appVersion: 2.5.0 + created: "2022-11-03T10:23:17.593546696Z" + description: A Traefik based Kubernetes ingress controller + digest: d471850bf1d94a4dab2c1b6174634a2c6c910d1ea82d57f64b63a02b71872293 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.3.1.tgz + version: 10.3.1 + - apiVersion: v2 + appVersion: 2.5.0 + created: "2022-11-03T10:23:17.591484561Z" + description: A Traefik based Kubernetes ingress controller + digest: 6e9ce62c52c58af60b0c0df08c5e76f68eecabe980463c89e86f9ddb89c9dc78 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.3.0.tgz + version: 10.3.0 + - apiVersion: v2 + appVersion: 2.4.13 + created: "2022-11-03T10:23:17.539288463Z" + description: A Traefik based Kubernetes ingress controller + digest: 231f7ca8c3336b8205d7043acde712db9b3231ec525ff505a5b7ae56dea2a204 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.2.0.tgz + version: 10.2.0 + - apiVersion: v2 + appVersion: 2.4.13 + created: "2022-11-03T10:23:17.492704162Z" + description: A Traefik based Kubernetes ingress controller + digest: de6ad32d11668ed1235a3168873ac9d8ba7b56e54dcfb9d1f9f5deb07dd78898 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.1.6.tgz + version: 10.1.6 + - apiVersion: v2 + appVersion: 2.4.13 + created: "2022-11-03T10:23:17.490731228Z" + description: A Traefik based Kubernetes ingress controller + digest: 36e6e56ba7a1abeb7d43f6115651ac0ca31f1bd480348ddff60c0cdd41043b8f + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.1.5.tgz + version: 10.1.5 + - apiVersion: v2 + appVersion: 2.4.13 + created: "2022-11-03T10:23:17.488722293Z" + description: A Traefik based Kubernetes ingress controller + digest: af7567f280b70544ce27f5f7cafe4776a499bf2f23fc0d84ebd62fe34496eadc + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.1.4.tgz + version: 10.1.4 + - apiVersion: v2 + appVersion: 2.4.13 + created: "2022-11-03T10:23:17.486334952Z" + description: A Traefik based Kubernetes ingress controller + digest: fefdf58a13204c09b85bb44a7cd375cd9984246e3906c355a602cd5f43b6aa42 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.1.3.tgz + version: 10.1.3 + - apiVersion: v2 + appVersion: 2.4.13 + created: "2022-11-03T10:23:17.483032995Z" + description: A Traefik based Kubernetes ingress controller + digest: 9f178174225a61baf0f3925241ad72ceb270f720fa3650ed321eb91b79ed7207 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.1.2.tgz + version: 10.1.2 + - apiVersion: v2 + appVersion: 2.4.9 + created: "2022-11-03T10:23:17.481055661Z" + description: A Traefik based Kubernetes ingress controller + digest: 4f893b213712e5b70b58b5413fcea21cbab6142c54d2dcf1221f6fd3d6c6f88a + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.1.1.tgz + version: 10.1.1 + - apiVersion: v2 + appVersion: 2.4.9 + created: "2022-11-03T10:23:17.479050327Z" + description: A Traefik based Kubernetes ingress controller + digest: 83f7ce11791756ae49ffb06146381cf9bd5dbe3d6f4a5f20a4051d5b2cd25cfc + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.1.0.tgz + version: 10.1.0 + - apiVersion: v2 + appVersion: 2.4.9 + created: "2022-11-03T10:23:17.477033692Z" + description: A Traefik based Kubernetes ingress controller + digest: 5f36e826b61a178585d048c5cb361d98a23188ae62d0d869d81b25a3736bbcd1 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.0.2.tgz + version: 10.0.2 + - apiVersion: v2 + appVersion: 2.4.9 + created: "2022-11-03T10:23:17.475015458Z" + description: A Traefik based Kubernetes ingress controller + digest: f5be6ffd3191a99339200286faff320bf77a8f088b6cee6f14f06c5eb8897939 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.0.1.tgz + version: 10.0.1 + - apiVersion: v2 + appVersion: 2.4.9 + created: "2022-11-03T10:23:17.472939722Z" + description: A Traefik based Kubernetes ingress controller + digest: 570fd2aa1e748b8e567d8b490b5602dedfd06d482ab09043a5949fc9b578d4b0 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-10.0.0.tgz + version: 10.0.0 + - apiVersion: v2 + appVersion: 2.4.8 + created: "2022-11-03T10:23:17.938844036Z" + description: A Traefik based Kubernetes ingress controller + digest: 1fd9dcff88bf888efc5d5d3617d496736f2097aa11bfc26545eb2f778df1d735 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.20.1.tgz + version: 9.20.1 + - apiVersion: v2 + appVersion: 2.4.8 + created: "2022-11-03T10:23:17.937109706Z" + description: A Traefik based Kubernetes ingress controller + digest: f9d9708080bbe9d6d115f51d07ff3ba589cfc8796aa8e693c1e5dc2ff49f95b5 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.20.0.tgz + version: 9.20.0 + - apiVersion: v2 + appVersion: 2.4.8 + created: "2022-11-03T10:23:17.931114403Z" + description: A Traefik based Kubernetes ingress controller + digest: 26501493e87d328ac07680bbfde2ff34f14f7b804da91ac58b34bd45642ecdfe + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.19.2.tgz + version: 9.19.2 + - apiVersion: v2 + appVersion: 2.4.8 + created: "2022-11-03T10:23:17.929353472Z" + description: A Traefik based Kubernetes ingress controller + digest: 963dcf1d006c3c660a3e9a6f7484d1f5636925073a7656a421d1d21284dca04a + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.19.1.tgz + version: 9.19.1 + - apiVersion: v2 + appVersion: 2.4.8 + created: "2022-11-03T10:23:17.927607242Z" + description: A Traefik based Kubernetes ingress controller + digest: bb189d08653b9ca7d3efee03abbec970605170977f7249577b1e26ec11141db4 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.19.0.tgz + version: 9.19.0 + - apiVersion: v2 + appVersion: 2.4.8 + created: "2022-11-03T10:23:17.925891013Z" + description: A Traefik based Kubernetes ingress controller + digest: 704d428823683ed2d4065e08cf2c6b44f21b2a835078eaad6cbb92bce5124cea + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.18.3.tgz + version: 9.18.3 + - apiVersion: v2 + appVersion: 2.4.8 + created: "2022-11-03T10:23:17.924160683Z" + description: A Traefik based Kubernetes ingress controller + digest: f5dee9d2783230ddbbbb838f833ef2235e198fdcb7eadf68fc7317e70c83a542 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.18.2.tgz + version: 9.18.2 + - apiVersion: v2 + appVersion: 2.4.8 + created: "2022-11-03T10:23:17.922442754Z" + description: A Traefik based Kubernetes ingress controller + digest: 631a67ea940bd13acb5cc44a45f620b648037bca088189947e9305185ee88f2e + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.18.1.tgz + version: 9.18.1 + - apiVersion: v2 + appVersion: 2.4.8 + created: "2022-11-03T10:23:17.920703924Z" + description: A Traefik based Kubernetes ingress controller + digest: 0d56886df2ab8a0420c520e6cb5fbff5c7b7df2f43a267beaf25850953600bbd + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.18.0.tgz + version: 9.18.0 + - apiVersion: v2 + appVersion: 2.4.8 + created: "2022-11-03T10:23:17.918940093Z" + description: A Traefik based Kubernetes ingress controller + digest: 57fd019f2c45b85a212554f40427ed7ec713d66e88a820dd4409d95f747f327a + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.17.6.tgz + version: 9.17.6 + - apiVersion: v2 + appVersion: 2.4.7 + created: "2022-11-03T10:23:17.917236564Z" + description: A Traefik based Kubernetes ingress controller + digest: eea8557d512481a9d464faa61678acd36240ae5b5a2760b85a52958189524e4b + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.17.5.tgz + version: 9.17.5 + - apiVersion: v2 + appVersion: 2.4.7 + created: "2022-11-03T10:23:17.915053927Z" + description: A Traefik based Kubernetes ingress controller + digest: d7e0171541dea7600a13a1b97e004628c3aa1c9759c23e1689d2ecbc58732a58 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.17.4.tgz + version: 9.17.4 + - apiVersion: v2 + appVersion: 2.4.7 + created: "2022-11-03T10:23:17.912433181Z" + description: A Traefik based Kubernetes ingress controller + digest: 0c832de1aef6ed8be75c25b8ceb87868ad6943913a666d1a86314030430ae419 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.17.3.tgz + version: 9.17.3 + - apiVersion: v2 + appVersion: 2.4.7 + created: "2022-11-03T10:23:17.910772553Z" + description: A Traefik based Kubernetes ingress controller + digest: 9276f7a37184d6ba5a8c03ab5e8f3e617d8c7bfabe957455d2026285a75f9547 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.17.2.tgz + version: 9.17.2 + - apiVersion: v2 + appVersion: 2.4.7 + created: "2022-11-03T10:23:17.909077724Z" + description: A Traefik based Kubernetes ingress controller + digest: 19f2cf610ab43ea4d771d4064208965c2fd92c6368cb86dbea2235a3fb4cfc57 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.17.1.tgz + version: 9.17.1 + - apiVersion: v2 + appVersion: 2.4.7 + created: "2022-11-03T10:23:17.907394295Z" + description: A Traefik based Kubernetes ingress controller + digest: 4a4b5aba86df797478f6f400ba4cc446d269a4db50a2b95276aea91cc9b13f57 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.16.2.tgz + version: 9.16.2 + - apiVersion: v2 + appVersion: 2.4.6 + created: "2022-11-03T10:23:17.905722066Z" + description: A Traefik based Kubernetes ingress controller + digest: 249c0ee3f606df9f4ea75b89951f964967404887a45ebf140a50fbfbbff76cba + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.16.1.tgz + version: 9.16.1 + - apiVersion: v2 + appVersion: 2.4.6 + created: "2022-11-03T10:23:17.904016637Z" + description: A Traefik based Kubernetes ingress controller + digest: 2e35493d413c283eb10ac7123c590604267cbc05289360ad932405e5a6244ea8 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.15.2.tgz + version: 9.15.2 + - apiVersion: v2 + appVersion: 2.4.5 + created: "2022-11-03T10:23:17.902341808Z" + description: A Traefik based Kubernetes ingress controller + digest: 8fd535b9d71aa99e95dca72e1cb0d2a981d08c658bf3300ea05cf34f3ae800f3 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.15.1.tgz + version: 9.15.1 + - apiVersion: v2 + appVersion: 2.4.5 + created: "2022-11-03T10:23:17.900635178Z" + description: A Traefik based Kubernetes ingress controller + digest: 811c202ec3b08a1bcc4c444edb6edaf7a6c1e1bf1367ba6423e12c0d96cbe5df + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.14.4.tgz + version: 9.14.4 + - apiVersion: v2 + appVersion: 2.4.5 + created: "2022-11-03T10:23:17.89898305Z" + description: A Traefik based Kubernetes ingress controller + digest: 814ec23708f4b7c06a5ea13ee15735e052ed0b0d7840950488f282c6c10d5a52 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.14.3.tgz + version: 9.14.3 + - apiVersion: v2 + appVersion: 2.4.2 + created: "2022-11-03T10:23:17.897309621Z" + description: A Traefik based Kubernetes ingress controller + digest: 5539099a701027b64ca30b154c83e140e3c706abe820ce568310706c81718a5e + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.14.2.tgz + version: 9.14.2 + - apiVersion: v2 + appVersion: 2.4.2 + created: "2022-11-03T10:23:17.895039982Z" + description: A Traefik based Kubernetes ingress controller + digest: 54d651ced8a79707a405e99cc817c1ffc751790b251bd4ce6834be008f23c3a0 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.14.1.tgz + version: 9.14.1 + - apiVersion: v2 + appVersion: 2.4.0 + created: "2022-11-03T10:23:17.892533239Z" + description: A Traefik based Kubernetes ingress controller + digest: b71b190e50edc6122f6ced5effc0c51cefdd92a02da12f1d240d3a1b56c1f9b8 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.14.0.tgz + version: 9.14.0 + - apiVersion: v2 + appVersion: 2.4.0 + created: "2022-11-03T10:23:17.890868911Z" + description: A Traefik based Kubernetes ingress controller + digest: 0fa99b6f4224e249c43db17d68c90f1ea413efb93c27e0a47f1925b79dad55f9 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.13.0.tgz + version: 9.13.0 + - apiVersion: v2 + appVersion: 2.3.6 + created: "2022-11-03T10:23:17.889221482Z" + description: A Traefik based Kubernetes ingress controller + digest: 5aa7719fab17f0efa08569e92565e2e4301b48dee8affebdb8bf3f934198f1ab + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.12.3.tgz + version: 9.12.3 + - apiVersion: v2 + appVersion: 2.3.6 + created: "2022-11-03T10:23:17.887607554Z" + description: A Traefik based Kubernetes ingress controller + digest: eb0e9727b67f39a3318b0db77302bc7ba226d2c77c6377ab52ade6d54ee74fb5 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.12.2.tgz + version: 9.12.2 + - apiVersion: v2 + appVersion: 2.3.3 + created: "2022-11-03T10:23:17.886011327Z" + description: A Traefik based Kubernetes ingress controller + digest: f2be65a88546639a944c8e6e6c0eafedef6d5a719b5d3964e2d12e8cd1c024d9 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.12.1.tgz + version: 9.12.1 + - apiVersion: v2 + appVersion: 2.3.3 + created: "2022-11-03T10:23:17.884362199Z" + description: A Traefik based Kubernetes ingress controller + digest: 2032e323f04fbb7bb739d6590728e45f8f07ae768235380572c164e1d4ce0328 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.12.0.tgz + version: 9.12.0 + - apiVersion: v2 + appVersion: 2.3.3 + created: "2022-11-03T10:23:17.882404665Z" + description: A Traefik based Kubernetes ingress controller + digest: 77772e4b58b4b7546a5db99c51acf7b4cd507cd06dcf1a02c27ef35ec3a2df95 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.11.0.tgz + version: 9.11.0 + - apiVersion: v2 + appVersion: 2.3.3 + created: "2022-11-03T10:23:17.880843738Z" + description: A Traefik based Kubernetes ingress controller + digest: a5e26a7de6c36309c4aafd948d37e2d9a7d69d933a5cbcbf0a125a49e59b0b4c + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.10.2.tgz + version: 9.10.2 + - apiVersion: v2 + appVersion: 2.3.1 + created: "2022-11-03T10:23:17.879248211Z" + description: A Traefik based Kubernetes ingress controller + digest: faf6f60da16462bf82112e1aaa72d726f6125f755c576590d98c0c2569d578b6 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.10.1.tgz + version: 9.10.1 + - apiVersion: v2 + appVersion: 2.3.1 + created: "2022-11-03T10:23:17.877626283Z" + description: A Traefik based Kubernetes ingress controller + digest: 0a5bb66d56cb0502bad1472f41303c7b52b4f3b605e2d341644ee4a0b987052f + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.10.0.tgz + version: 9.10.0 + - apiVersion: v2 + appVersion: 2.3.1 + created: "2022-11-03T10:23:17.961682129Z" + description: A Traefik based Kubernetes ingress controller + digest: 3f37ac274bbd730382592566ca4bc8d14b019a313dc6011e3a1519dc9a8ab980 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.9.0.tgz + version: 9.9.0 + - apiVersion: v2 + appVersion: 2.3.1 + created: "2022-11-03T10:23:17.960065701Z" + description: A Traefik based Kubernetes ingress controller + digest: 59acd4367453e0bf5027b4e915cd01c18debc082a25f8efbf34d52120e89f084 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.8.4.tgz + version: 9.8.4 + - apiVersion: v2 + appVersion: 2.3.1 + created: "2022-11-03T10:23:17.958491074Z" + description: A Traefik based Kubernetes ingress controller + digest: d2c19ce8e0ce4f31af783339d3ac13ce5f31780040d64b8b9caf38148feb0317 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.8.3.tgz + version: 9.8.3 + - apiVersion: v2 + appVersion: 2.3.1 + created: "2022-11-03T10:23:17.956893846Z" + description: A Traefik based Kubernetes ingress controller + digest: 633775d1ff9d1d1486ed8ca91883d6d8f8e08dda217955f003f88d13900c6ee3 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + - https://github.com/traefik/traefik-helm-chart + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.8.2.tgz + version: 9.8.2 + - apiVersion: v2 + appVersion: 2.3.1 + created: "2022-11-03T10:23:17.954884012Z" + description: A Traefik based Kubernetes ingress controller + digest: c00572b0484e97646f8bd39578be3123e69955a83f0f20b7342ec4259a0fd664 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.8.1.tgz + version: 9.8.1 + - apiVersion: v2 + appVersion: 2.3.1 + created: "2022-11-03T10:23:17.952710274Z" + description: A Traefik based Kubernetes ingress controller + digest: 9ec2e8c392b3f3cf273de1ed55e7fe636bfa4a478ec1b2ae25ec3def9bbb2aba + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.8.0.tgz + version: 9.8.0 + - apiVersion: v2 + appVersion: 2.3.1 + created: "2022-11-03T10:23:17.950645439Z" + description: A Traefik based Kubernetes ingress controller + digest: 4787938cd7907f804cb53cb7371d9bb0bfafbade5179546aedf9c11fbb7dcf7d + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.7.0.tgz + version: 9.7.0 + - apiVersion: v2 + appVersion: 2.3.1 + created: "2022-11-03T10:23:17.949135113Z" + description: A Traefik based Kubernetes ingress controller + digest: bfb39c45e544c334f0445052e5420f46aaf5de51419a43cf2bc4ce28d2f8159b + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.6.0.tgz + version: 9.6.0 + - apiVersion: v2 + appVersion: 2.3.1 + created: "2022-11-03T10:23:17.947620387Z" + description: A Traefik based Kubernetes ingress controller + digest: 7f12103213e99a4ff59d0bd5f81dac51cc5e3bde407696bb021a4c3485229fa2 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.5.2.tgz + version: 9.5.2 + - apiVersion: v2 + appVersion: 2.3.1 + created: "2022-11-03T10:23:17.946155661Z" + description: A Traefik based Kubernetes ingress controller + digest: 7565c74fcfd64b946397a0c26fb2a83045756788552fa22733beb3c4c1b49b5e + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.5.1.tgz + version: 9.5.1 + - apiVersion: v2 + appVersion: 2.3.1 + created: "2022-11-03T10:23:17.944677436Z" + description: A Traefik based Kubernetes ingress controller + digest: dd116ff752b7e3553fc7f7946d0f47f6d3a74264fe35358d7c6407846c133e71 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.4.3.tgz + version: 9.4.3 + - apiVersion: v2 + appVersion: 2.3.0 + created: "2022-11-03T10:23:17.943229811Z" + description: A Traefik based Kubernetes ingress controller + digest: fb67ee549d317da166f557fdc666354e23ff9da69628d2607a665337e25a18df + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.4.2.tgz + version: 9.4.2 + - apiVersion: v2 + appVersion: 2.3.0 + created: "2022-11-03T10:23:17.941780786Z" + description: A Traefik based Kubernetes ingress controller + digest: af1a2aacba7f9c989763916a2a2384246713291f5d324e32cbd138f20c194c40 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.3/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.4.1.tgz + version: 9.4.1 + - apiVersion: v2 + appVersion: 2.3.0 + created: "2022-11-03T10:23:17.940298461Z" + description: A Traefik based Kubernetes ingress controller + digest: 58e49b9466ccfd94570e17e22111c266ae02aa17ccfab48b63df238adcc14f98 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.2/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.3.0.tgz + version: 9.3.0 + - apiVersion: v2 + appVersion: 2.2.8 + created: "2022-11-03T10:23:17.935059471Z" + description: A Traefik based Kubernetes ingress controller + digest: 32d69909f4e819ad9a71661b7fd18102841ab52673f8176e78744e72aad8b013 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.2/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.2.1.tgz + version: 9.2.1 + - apiVersion: v2 + appVersion: 2.2.8 + created: "2022-11-03T10:23:17.933055436Z" + description: A Traefik based Kubernetes ingress controller + digest: c34436b7a93a3f0abc5710fd4b6113734d08d95af96700eef2fe2137fe8dceeb + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/traefik/traefik/v2.2/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ldez@traefik.io + name: ldez + name: traefik + sources: + - https://github.com/traefik/traefik + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.2.0.tgz + version: 9.2.0 + - apiVersion: v2 + appVersion: 2.2.8 + created: "2022-11-03T10:23:17.875291443Z" + description: A Traefik based Kubernetes ingress controller + digest: fca77a5e763fa45228cccbd532732b6ea9294721e6301a61b1b95019fd05587d + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/containous/traefik/v2.2/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.1.1.tgz + version: 9.1.1 + - apiVersion: v2 + appVersion: 2.2.8 + created: "2022-11-03T10:23:17.873167806Z" + description: A Traefik based Kubernetes ingress controller + digest: bf682f64f3abc27384769438b68dc8824a4a5cbd62f75f87fbcfcbe8eb782ee6 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/containous/traefik/v2.2/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.1.0.tgz + version: 9.1.0 + - apiVersion: v2 + appVersion: 2.2.8 + created: "2022-11-03T10:23:17.871752082Z" + description: A Traefik based Kubernetes ingress controller + digest: 12365bbeeb3bd51203cd849a84d6c42e3a22b55ff2a37da7918092ad7e807e03 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/containous/traefik/v2.2/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + type: application + urls: + - https://helm.traefik.io/traefik/traefik-9.0.0.tgz + version: 9.0.0 + - apiVersion: v1 + appVersion: 2.2.8 + created: "2022-11-03T10:23:17.84839438Z" + description: A Traefik based Kubernetes ingress controller + digest: e864594c81a3521a8991fd99174d587dca8f6bba17bb50dfdc9bd4494712a41a + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/containous/traefik/v2.2/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.13.3.tgz + version: 8.13.3 + - apiVersion: v1 + appVersion: 2.2.8 + created: "2022-11-03T10:23:17.847028456Z" + description: A Traefik based Kubernetes ingress controller + digest: f27afe571b3af9d32f49b2da9fb2b24191bacedd709b0d732ef1314380c66ae4 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/containous/traefik/v2.2/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.13.2.tgz + version: 8.13.2 + - apiVersion: v1 + appVersion: 2.2.8 + created: "2022-11-03T10:23:17.845614332Z" + description: A Traefik based Kubernetes ingress controller + digest: b0231c0d3d7c98a46f5cd916e46de02f62bb4985ba9c3fb7f8daaa684662ad87 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/containous/traefik/v2.2/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.13.1.tgz + version: 8.13.1 + - apiVersion: v1 + appVersion: 2.2.8 + created: "2022-11-03T10:23:17.844213008Z" + description: A Traefik based Kubernetes ingress controller + digest: 39c59de2d8343ddacf348970423e20e0e4a07494b23d45544dcee8984eb8354f + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/containous/traefik/v2.2/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.13.0.tgz + version: 8.13.0 + - apiVersion: v1 + appVersion: 2.2.8 + created: "2022-11-03T10:23:17.842800184Z" + description: A Traefik based Kubernetes ingress controller + digest: fe698902d0cb3138dd183681d679d4ef997348d51d3988541268821703041f19 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/containous/traefik/v2.2/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.12.0.tgz + version: 8.12.0 + - apiVersion: v1 + appVersion: 2.2.8 + created: "2022-11-03T10:23:17.841552162Z" + description: A Traefik based Kubernetes ingress controller + digest: 225dd56fd5d61480f8bcf94337a442ac4021d0ff203925ad14979015e2b2b286 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/containous/traefik/v2.2/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.11.0.tgz + version: 8.11.0 + - apiVersion: v1 + appVersion: 2.2.8 + created: "2022-11-03T10:23:17.84028384Z" + description: A Traefik based Kubernetes ingress controller + digest: 11278d0d5eba67a98f289c72a081629d9b10b391d22e7576264860929cae304b + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/containous/traefik/v2.2/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.10.0.tgz + version: 8.10.0 + - apiVersion: v1 + appVersion: 2.2.8 + created: "2022-11-03T10:23:17.870349858Z" + description: A Traefik based Kubernetes ingress controller + digest: 3d3252872dd1df5e2c8634cba6c3724343af45fd140e7ea1437335fee855e900 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/containous/traefik/v2.2/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.9.2.tgz + version: 8.9.2 + - apiVersion: v1 + appVersion: 2.2.5 + created: "2022-11-03T10:23:17.869040435Z" + description: A Traefik based Kubernetes ingress controller + digest: b1b603b7607119493c72838ea570a5546c0b7fec2cea0b2bf18868700b20f41c + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/containous/traefik/v2.2/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.9.1.tgz + version: 8.9.1 + - apiVersion: v1 + appVersion: 2.2.1 + created: "2022-11-03T10:23:17.867792314Z" + description: A Traefik based Kubernetes ingress controller + digest: 07484175255b61195d83323927890d6a51ab38dc493d7da5a769f92e7b0ff8aa + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/containous/traefik/v2.2/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.9.0.tgz + version: 8.9.0 + - apiVersion: v1 + appVersion: 2.2.1 + created: "2022-11-03T10:23:17.865620976Z" + description: A Traefik based Kubernetes ingress controller + digest: a6f479387d142f066dfadf8d1510cf1e5bce0b5c1cbd71ebe2351b4ec65bce78 + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/containous/traefik/v2.2/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.8.1.tgz + version: 8.8.1 + - apiVersion: v1 + appVersion: 2.2.1 + created: "2022-11-03T10:23:17.864335554Z" + description: A Traefik based Kubernetes ingress controller + digest: 3431b994f9fa53a7a9acab8e5310fa1a371a2bd7c82f498c6e92d43b0167260a + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/containous/traefik/v2.2/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.8.0.tgz + version: 8.8.0 + - apiVersion: v1 + appVersion: 2.2.1 + created: "2022-11-03T10:23:17.863112433Z" + description: A Traefik based Kubernetes ingress controller + digest: 51b817710f110931ddf1d5b20c6b401f86cb1fb502a19392c03c273fa5fbd5df + home: https://traefik.io/ + icon: https://raw.githubusercontent.com/containous/traefik/v2.2/docs/content/assets/img/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.7.2.tgz + version: 8.7.2 + - apiVersion: v1 + appVersion: 2.2.1 + created: "2022-11-03T10:23:17.861814711Z" + description: A Traefik based Kubernetes ingress controller + digest: ebb99d5de829756b09ae83a390a1a098772d0b886e64f2cac03091940f3e904c + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.7.1.tgz + version: 8.7.1 + - apiVersion: v1 + appVersion: 2.2.1 + created: "2022-11-03T10:23:17.86058519Z" + description: A Traefik based Kubernetes ingress controller + digest: 6c59ba9cb81dd4161e3983cf008173cd744df44af5a36de8e5d5e7d94c26fcfc + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.7.0.tgz + version: 8.7.0 + - apiVersion: v1 + appVersion: 2.2.1 + created: "2022-11-03T10:23:17.859361569Z" + description: A Traefik based Kubernetes ingress controller + digest: 1aeb6ffb270dc05d17a6309cfa1694311313d686ff10ea9ca34d4d889362bb14 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.6.1.tgz + version: 8.6.1 + - apiVersion: v1 + appVersion: 2.2.1 + created: "2022-11-03T10:23:17.858151348Z" + description: A Traefik based Kubernetes ingress controller + digest: b3d25c485b77aef329e40af7326154c8005b9fc18a4ca8ed046b562e6dcd56ea + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.6.0.tgz + version: 8.6.0 + - apiVersion: v1 + appVersion: 2.2.1 + created: "2022-11-03T10:23:17.856396618Z" + description: A Traefik based Kubernetes ingress controller + digest: 9c30a88719e7b5fc206b38ada44e061e948542799b05a659a334e2a7c32ff33f + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.5.0.tgz + version: 8.5.0 + - apiVersion: v1 + appVersion: 2.2.1 + created: "2022-11-03T10:23:17.85476989Z" + description: A Traefik based Kubernetes ingress controller + digest: 63ced70cd1cf2c4fc816edf4513eb0706f27360699aebdcddd8cd78031c03b13 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.4.1.tgz + version: 8.4.1 + - apiVersion: v1 + appVersion: 2.2.1 + created: "2022-11-03T10:23:17.852925158Z" + description: A Traefik based Kubernetes ingress controller + digest: 14c9d2692bf34bd953d37ea3476bcab2ae6b63179c275a55e51d0a7f049401e1 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.4.0.tgz + version: 8.4.0 + - apiVersion: v1 + appVersion: 2.2.1 + created: "2022-11-03T10:23:17.851752838Z" + description: A Traefik based Kubernetes ingress controller + digest: 91163cc7d30e55fbb17efdb543ff75debb783429cc63e1ae9ac1dd1ece759d3f + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.3.0.tgz + version: 8.3.0 + - apiVersion: v1 + appVersion: 2.2.1 + created: "2022-11-03T10:23:17.850612818Z" + description: A Traefik based Kubernetes ingress controller + digest: 00a029b2d02e151f86d2aa84cbab5c68eb317703528164015e1ba2084a31f692 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.2.1.tgz + version: 8.2.1 + - apiVersion: v1 + appVersion: 2.2.1 + created: "2022-11-03T10:23:17.849512099Z" + description: A Traefik based Kubernetes ingress controller + digest: ddf852ba86066378a24c45c97ecc43e0cd657a5b17810d85e95c09f88f0396fb + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.2.0.tgz + version: 8.2.0 + - apiVersion: v1 + appVersion: 2.2.1 + created: "2022-11-03T10:23:17.839042319Z" + description: A Traefik based Kubernetes ingress controller + digest: 99445e69fb9d11d027aff8204edec9cf8f6133cc735fbc87e7c2c69abeb32f33 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.1.5.tgz + version: 8.1.5 + - apiVersion: v1 + appVersion: 2.2.1 + created: "2022-11-03T10:23:17.837707396Z" + description: A Traefik based Kubernetes ingress controller + digest: a44cebbdaffef59db916e5c1295bc185fcf746e681e4c38f4868eeba2ee0f511 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.1.4.tgz + version: 8.1.4 + - apiVersion: v1 + appVersion: 2.2.0 + created: "2022-11-03T10:23:17.836277271Z" + description: A Traefik based Kubernetes ingress controller + digest: eeec38b3c2c65ac594f8a95f376b7540a52cbbf1e8edab4847982807165771ef + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.1.3.tgz + version: 8.1.3 + - apiVersion: v1 + appVersion: 2.2.0 + created: "2022-11-03T10:23:17.834628043Z" + description: A Traefik based Kubernetes ingress controller + digest: 3084e6d80f3be9dbf816edc067125f787345535afe69646217ae32fd46dc13cd + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.1.2.tgz + version: 8.1.2 + - apiVersion: v1 + appVersion: 2.2.0 + created: "2022-11-03T10:23:17.833552425Z" + description: A Traefik based Kubernetes ingress controller + digest: 4f88f5daee2fa5bde7c07be49be4f98d12369341c4ba2fd8d11b1ae7e01ae9fa + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.1.1.tgz + version: 8.1.1 + - apiVersion: v1 + appVersion: 2.2.0 + created: "2022-11-03T10:23:17.832445706Z" + description: A Traefik based Kubernetes ingress controller + digest: 03964cad523150c11fe4929e884abfd499e188d4dbe9be051bc8e1e3dc43eda1 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.1.0.tgz + version: 8.1.0 + - apiVersion: v1 + appVersion: 2.2.0 + created: "2022-11-03T10:23:17.831394688Z" + description: A Traefik based Kubernetes ingress controller + digest: 968db18043bb89dc076415750ef0d04eff1068a6bf90659ca9c315ed07c349eb + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.0.4.tgz + version: 8.0.4 + - apiVersion: v1 + appVersion: 2.2.0 + created: "2022-11-03T10:23:17.830407871Z" + description: A Traefik based Kubernetes ingress controller + digest: d67a3ce20ed2450427c6ce6717265dda57977790d219f6671054c63db3bbf7f2 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.0.3.tgz + version: 8.0.3 + - apiVersion: v1 + appVersion: 2.2.0 + created: "2022-11-03T10:23:17.829379053Z" + description: A Traefik based Kubernetes ingress controller + digest: d7de298ba350a58f0b643a4646b180d0bfc98b46c2c056a6c3af897ada3aa6fa + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.0.2.tgz + version: 8.0.2 + - apiVersion: v1 + appVersion: 2.2.0 + created: "2022-11-03T10:23:17.828322335Z" + description: A Traefik based Kubernetes ingress controller + digest: 3c0a9b4c1e334423debaea61c829a86f052a90879c011637db16ea7b9969e2d0 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.0.1.tgz + version: 8.0.1 + - apiVersion: v1 + appVersion: 2.2.0 + created: "2022-11-03T10:23:17.827318717Z" + description: A Traefik based Kubernetes ingress controller + digest: 74851720eaa2e0024369ad9f6e0955d705bb21f168111fdf2b9233225907e50b + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-8.0.0.tgz + version: 8.0.0 + - apiVersion: v1 + appVersion: 2.2.0 + created: "2022-11-03T10:23:17.8263222Z" + description: A Traefik based Kubernetes ingress controller + digest: 24cb092bacffd4dfad3403edf731eb3eceec0ca443c4bb7206a65b665030ed3f + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-7.2.1.tgz + version: 7.2.1 + - apiVersion: v1 + appVersion: 2.2.0 + created: "2022-11-03T10:23:17.825314783Z" + description: A Traefik based Kubernetes ingress controller + digest: 411bfa5a984ee6c987aa9547277ff6bc9e708345f3106b304c5f115370ef35ef + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-7.2.0.tgz + version: 7.2.0 + - apiVersion: v2 + appVersion: 2.2.0 + created: "2022-11-03T10:23:17.824306466Z" + description: A Traefik based Kubernetes ingress controller + digest: d1bab7dc4fd5a7d165b563090a1d94edb652a817dffded14deb7ea895c7aa563 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-7.1.0.tgz + version: 7.1.0 + - apiVersion: v2 + appVersion: 2.2.0 + created: "2022-11-03T10:23:17.823299048Z" + description: A Traefik based Kubernetes ingress controller + digest: b6c5f0e1112970fc4817389c8261b3a1903433c1ed04f8a4a2fc0c72850c1dd2 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-7.0.0.tgz + version: 7.0.0 + - apiVersion: v2 + appVersion: 2.2.0 + created: "2022-11-03T10:23:17.822282031Z" + description: A Traefik based Kubernetes ingress controller + digest: 8da41502ca95540674f195867810bba24b56f27d2c3f927fab1d5720a30bcb72 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-6.4.0.tgz + version: 6.4.0 + - apiVersion: v2 + appVersion: 2.2.0 + created: "2022-11-03T10:23:17.821286314Z" + description: A Traefik based Kubernetes ingress controller + digest: 91b7d63da3c4b72b9d950a40e01a560f808df8f7fb4857c8f02cfd6d58367a31 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-6.3.0.tgz + version: 6.3.0 + - apiVersion: v2 + appVersion: 2.2.0 + created: "2022-11-03T10:23:17.820239396Z" + description: A Traefik based Kubernetes ingress controller + digest: d7e88a15ffb1880ddd96cccf8fd2edf28749b18ef3502cc3fe81c2cf4dc315d6 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-6.2.0.tgz + version: 6.2.0 + - apiVersion: v2 + appVersion: 2.1.4 + created: "2022-11-03T10:23:17.818865372Z" + description: A Traefik based Kubernetes ingress controller + digest: 5d76bbda1a5a82f4d9a7b0d4877bc1ec6465a53c416ef0b1d8d7da38e1ac842c + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-6.1.1.tgz + version: 6.1.1 + - apiVersion: v2 + appVersion: 2.1.3 + created: "2022-11-03T10:23:17.817678852Z" + description: A Traefik based Kubernetes ingress controller + digest: f90e45138f25eb8054780325c1f662f3fdbe24d2559afdcd1a7ce1c6e1aa8512 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-6.1.0.tgz + version: 6.1.0 + - apiVersion: v2 + appVersion: 2.1.3 + created: "2022-11-03T10:23:17.816148425Z" + description: A Traefik based Kubernetes ingress controller + digest: 36461cd80af33412fa6e98b941b7b83310e9f95c318554027883b131c3a93258 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-6.0.2.tgz + version: 6.0.2 + - apiVersion: v2 + appVersion: 2.1.3 + created: "2022-11-03T10:23:17.815206709Z" + description: A Traefik based Kubernetes ingress controller + digest: 467603d51db6e6d2fb54110ea913248f0ade03fbe3ed3e9f09f95e014ca4d8e2 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-6.0.0.tgz + version: 6.0.0 + - apiVersion: v2 + appVersion: 2.1.3 + created: "2022-11-03T10:23:17.814256993Z" + description: A Traefik based Kubernetes ingress controller + digest: 97e9777d9b598dc8bc777da74e05a8813748ee2d16a3679b5f89f5ca50e0055c + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-5.6.0.tgz + version: 5.6.0 + - apiVersion: v2 + appVersion: 2.1.3 + created: "2022-11-03T10:23:17.813330377Z" + description: A Traefik based Kubernetes ingress controller + digest: 7d04d1264dee8d581a627db64a7e4a7e7f05a67354b8969e82cae0e70706ff04 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-5.5.0.tgz + version: 5.5.0 + - apiVersion: v2 + appVersion: 2.1.3 + created: "2022-11-03T10:23:17.81237576Z" + description: A Traefik based Kubernetes ingress controller + digest: 4117d280cb6836542c11aa2da3a64eaaa6b61b322be72cab9d45b9acd8309039 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-5.3.3.tgz + version: 5.3.3 + - apiVersion: v2 + appVersion: 2.1.3 + created: "2022-11-03T10:23:17.811484145Z" + description: A Traefik based Kubernetes ingress controller + digest: 79ae37f191a7f5d5f1a49b8d94cea3389d43d660f360a0384a9dcd9c492550d2 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-5.3.2.tgz + version: 5.3.2 + - apiVersion: v2 + appVersion: 2.1.3 + created: "2022-11-03T10:23:17.81060063Z" + description: A Traefik based Kubernetes ingress controller + digest: e6693a12aa03b74ca967e03124ca73f16b8b0992d0ebb93864a8537893bd84a6 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-5.3.1.tgz + version: 5.3.1 + - apiVersion: v2 + appVersion: 2.1.3 + created: "2022-11-03T10:23:17.809725415Z" + description: A Traefik based Kubernetes ingress controller + digest: 204781ebcc04418782da29956c35293d0e90baf26a83938f1687e73914813948 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-5.3.0.tgz + version: 5.3.0 + - apiVersion: v2 + appVersion: 2.1.3 + created: "2022-11-03T10:23:17.808828399Z" + description: A Traefik based Kubernetes ingress controller + digest: d0e2af56eae8edc06de57e436fa36d38722c62ab2e62cd91d7e8b91d01189360 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-5.2.1.tgz + version: 5.2.1 + - apiVersion: v2 + appVersion: 2.1.3 + created: "2022-11-03T10:23:17.807907184Z" + description: A Traefik based Kubernetes ingress controller + digest: 89787f456584e0c305e084022f80ce0522f44f60b6cc0ccfc4c6db645874ff60 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-5.2.0.tgz + version: 5.2.0 + - apiVersion: v2 + appVersion: 2.1.3 + created: "2022-11-03T10:23:17.807025068Z" + description: A Traefik based Kubernetes ingress controller + digest: 1159d2842ec56483fb40893bc3be3607ac059c0d3f7f6d744843cc55462f57ba + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-5.1.0.tgz + version: 5.1.0 + - apiVersion: v2 + appVersion: 2.1.3 + created: "2022-11-03T10:23:17.806155753Z" + description: A Traefik based Kubernetes ingress controller + digest: 09b8a05223fabad2e16f29d55b743f465bbed8431459a4a3e38bdec8e8a4d107 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-5.0.0.tgz + version: 5.0.0 + - apiVersion: v2 + appVersion: 2.1.3 + created: "2022-11-03T10:23:17.805266738Z" + description: A Traefik based Kubernetes ingress controller + digest: 579995dc7087f84b8407e2ccdb4f2ce2831da42e4eb235426563ecd006ed6884 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-4.1.3.tgz + version: 4.1.3 + - apiVersion: v2 + appVersion: 2.1.3 + created: "2022-11-03T10:23:17.804310322Z" + description: A Traefik based Kubernetes ingress controller + digest: bcc5545963823dd92485973350da16d071ebedb78e508552ced6f5cf46dc7923 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-4.1.2.tgz + version: 4.1.2 + - apiVersion: v2 + appVersion: 2.1.3 + created: "2022-11-03T10:23:17.803406706Z" + description: A Traefik based Kubernetes ingress controller + digest: 23a13057b65123029a4f71cd8ac1eb3eb6ac8c849cd39d2c137efa69de78a14f + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-4.1.1.tgz + version: 4.1.1 + - apiVersion: v2 + appVersion: 2.1.3 + created: "2022-11-03T10:23:17.802547491Z" + description: A Traefik based Kubernetes ingress controller + digest: 7172e5ef2e904523e92e0bc5c34d87b4fbd2648dc38386deffdf52ff426d6f06 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-4.1.0.tgz + version: 4.1.0 + - apiVersion: v2 + appVersion: 2.1.3 + created: "2022-11-03T10:23:17.801174968Z" + description: A Traefik based Kubernetes ingress controller + digest: 2acd1bcc8367c919559e818d6a8190b7eb0fde9121cc4672ba7eb812d4175ada + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-4.0.0.tgz + version: 4.0.0 + - apiVersion: v1 + appVersion: 2.1.3 + created: "2022-11-03T10:23:17.799051131Z" + description: A Traefik based Kubernetes ingress controller + digest: 41525044f4673b5484cdb12863aec4c9363d0989a5337dbc5cfe591903b762d2 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-3.5.0.tgz + version: 3.5.0 + - apiVersion: v1 + appVersion: 2.1.3 + created: "2022-11-03T10:23:17.797943812Z" + description: A Traefik based Kubernetes ingress controller + digest: 85aeb1039e2e3131e15bb132062ebd9834b9e299fa461aad51c10fb9a6d0fc86 + home: https://traefik.io/ + icon: http://traefik.io/traefik.logo.png + keywords: + - traefik + - ingress + maintainers: + - email: emile@vauge.com + name: emilevauge + - email: daniel.tomcej@gmail.com + name: dtomcej + - email: ludovic@containo.us + name: ldez + name: traefik + sources: + - https://github.com/containous/traefik + urls: + - https://helm.traefik.io/traefik/traefik-3.4.0.tgz + version: 3.4.0 +generated: "2022-11-03T10:23:17.468307242Z" \ No newline at end of file diff --git a/internal/app/_testdata/index.yaml b/internal/app/_testdata/index.yaml new file mode 100644 index 0000000..b80d484 --- /dev/null +++ b/internal/app/_testdata/index.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +entries: + bar: + - apiVersion: v2 + appVersion: 1.0.1 + created: "2023-08-10T13:43:03.784910144Z" + dependencies: + - name: common + repository: https://charts.bitnami.com/bitnami + tags: + - bitnami-common + version: 1.x.x + description: Helm chart to deploy foo + digest: 5751efb02879f5fdbe4849318aa11a0253b25e8dcd86dzdf2aa55d7267163cbc + name: bar + type: application + urls: + - s3://foo/bar-1.0.1.tgz + version: bar-1.0.1 \ No newline at end of file diff --git a/internal/app/_testdata/test_chart/Chart.yaml b/internal/app/_testdata/test_chart/Chart.yaml new file mode 100644 index 0000000..b14811a --- /dev/null +++ b/internal/app/_testdata/test_chart/Chart.yaml @@ -0,0 +1,12 @@ +# Chart.yaml +apiVersion: v2 +name: traefik +version: 2.9.1 +description: A Traefik based Kubernetes ingress controller +dependencies: +- name: traefik + version: 11.1.1 + repository: https://helm.traefik.io/traefik +- name: kube-prometheus-stack + version: 40.3.1 + repository: https://prometheus-community.github.io/helm-charts diff --git a/internal/app/_testdata/test_chart2/Chart.yaml b/internal/app/_testdata/test_chart2/Chart.yaml new file mode 100644 index 0000000..b7d9bb0 --- /dev/null +++ b/internal/app/_testdata/test_chart2/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +appVersion: 0.1.0 +description: airbyte +name: airbyte +version: 0.1.0 +dependencies: + - name: kube-prometheus-stack + version: 40.3.1 + repository: https://prometheus-community.github.io/helm-charts diff --git a/internal/app/_testdata/test_chart3/Chart.yaml b/internal/app/_testdata/test_chart3/Chart.yaml new file mode 100644 index 0000000..35acf4d --- /dev/null +++ b/internal/app/_testdata/test_chart3/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +appVersion: 0.47.0 +description: Provides easy monitoring definitions for Kubernetes services, and deployment and management of Prometheus instances. +icon: https://raw.githubusercontent.com/prometheus/prometheus.github.io/master/assets/prometheus_logo-cb55bb5c346.png +name: prometheus-operator +version: 40.3.1 +dependencies: + - name: kube-prometheus-stack + version: 40.3.1 + repository: https://prometheus-community.github.io/helm-charts diff --git a/internal/app/_testdata/test_chart4/Chart.yaml b/internal/app/_testdata/test_chart4/Chart.yaml new file mode 100644 index 0000000..47d3ff7 --- /dev/null +++ b/internal/app/_testdata/test_chart4/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +appVersion: v2.3.3 +description: A Helm chart for ArgoCD, a declarative, GitOps continuous delivery tool for Kubernetes. +name: argo-cd +version: 4.5.8 +dependencies: + - name: kube-prometheus-stack + version: 40.3.1 + repository: https://prometheus-community.github.io/helm-charts diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..7457852 --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,338 @@ +package app + +import ( + "context" + "fmt" + "sync" + "time" + + awsConfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/eks" + "github.com/aws/aws-sdk-go-v2/service/elasticache" + "github.com/aws/aws-sdk-go-v2/service/kafka" + "github.com/aws/aws-sdk-go-v2/service/lambda" + "github.com/aws/aws-sdk-go-v2/service/rds" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/prometheus/client_golang/prometheus" + "github.com/qonto/upgrade-manager/config" + "github.com/qonto/upgrade-manager/internal/app/calculators" + soft "github.com/qonto/upgrade-manager/internal/app/core/software" + "github.com/qonto/upgrade-manager/internal/app/sources/argohelm" + eksSource "github.com/qonto/upgrade-manager/internal/app/sources/aws/eks" + elasticacheSource "github.com/qonto/upgrade-manager/internal/app/sources/aws/elasticache" + lambdaSource "github.com/qonto/upgrade-manager/internal/app/sources/aws/lambda" + mskSource "github.com/qonto/upgrade-manager/internal/app/sources/aws/msk" + rdsSource "github.com/qonto/upgrade-manager/internal/app/sources/aws/rds" + "github.com/qonto/upgrade-manager/internal/app/sources/deployments" + "github.com/qonto/upgrade-manager/internal/app/sources/filesystemhelm" + "github.com/qonto/upgrade-manager/internal/infra/aws" + "github.com/qonto/upgrade-manager/internal/infra/kubernetes" + "go.uber.org/zap" +) + +type App struct { + log *zap.Logger + sources []soft.Source + softwares []*soft.Software + Config config.Config + registry *prometheus.Registry + metrics appMetrics + k8sClient kubernetes.KubernetesClient + s3Api aws.S3Api + escApi aws.ElasticacheApi + eksApi aws.EKSApi + rdsApi aws.RDSApi + mskApi aws.MSKApi + lambdaApi aws.LambdaApi + done chan bool + wg sync.WaitGroup +} +type appMetrics struct { + scores *prometheus.GaugeVec + successLoads prometheus.Gauge + foundSoftwares prometheus.Gauge + successComputeScore prometheus.Gauge + processError *prometheus.CounterVec + loopExecTime prometheus.Gauge +} + +func New(l *zap.Logger, registry *prometheus.Registry, k8sClient kubernetes.KubernetesClient, config config.Config) (*App, error) { + app := &App{ + log: l, + registry: registry, + done: make(chan bool, 1), + k8sClient: k8sClient, + Config: config, + } + // TODO: make region mandatory ? + // helm sources cannot work withou + if app.Config.Global.AwsConfig.Region != "" { + awscfg, err := awsConfig.LoadDefaultConfig(context.TODO()) + awscfg.Region = app.Config.Global.AwsConfig.Region + app.log.Info(fmt.Sprintf("Initializing AWS configuration in region %s", awscfg.Region)) + if err != nil { + return app, err + } + app.s3Api = s3.NewFromConfig(awscfg) + app.escApi = elasticache.NewFromConfig(awscfg) + app.eksApi = eks.NewFromConfig(awscfg) + app.rdsApi = rds.NewFromConfig(awscfg) + app.lambdaApi = lambda.NewFromConfig(awscfg) + app.mskApi = kafka.NewFromConfig(awscfg) + } + if err := app.InitSources(); err != nil { + return app, err + } + if err := app.InitPrometheusMetrics(); err != nil { + return app, err + } + return app, nil +} + +// Initizalize the different software sources which have a config section specified +func (a *App) InitSources() error { + a.sources = nil + + if a.Config.Sources.FsHelm != nil { + for _, item := range a.Config.Sources.FsHelm { + softSource, err := filesystemhelm.NewSource(item, a.log, a.s3Api) + if err != nil { + a.log.Fatal(fmt.Sprint(err)) + } + a.sources = append(a.sources, softSource) + } + } + if a.Config.Sources.ArgocdHelm != nil { + for _, item := range a.Config.Sources.ArgocdHelm { + softSource, err := argohelm.NewSource(item, a.log, a.k8sClient, true, a.s3Api) + if err != nil { + a.log.Fatal(fmt.Sprint(err)) + } + a.sources = append(a.sources, softSource) + } + } + if a.Config.Sources.Deployments != nil { + for _, item := range a.Config.Sources.Deployments { + softSource, err := deployments.NewSource(a.log, a.k8sClient, item) + if err != nil { + a.log.Fatal(fmt.Sprint(err)) + } + a.sources = append(a.sources, softSource) + } + } + if a.Config.Sources.Aws.Elasticache.Enabled { + escSource, err := elasticacheSource.NewSource(a.escApi, a.log, &a.Config.Sources.Aws.Elasticache) + if err != nil { + a.log.Fatal(fmt.Sprint(err)) + } + a.sources = append(a.sources, escSource) + } + if a.Config.Sources.Aws.Eks.Enabled { + eksSource, err := eksSource.NewSource(a.eksApi, a.log, &a.Config.Sources.Aws.Eks) + if err != nil { + a.log.Fatal(fmt.Sprint(err)) + } + a.sources = append(a.sources, eksSource) + } + if a.Config.Sources.Aws.Msk.Enabled { + mskSource, err := mskSource.NewSource(a.mskApi, a.log, &a.Config.Sources.Aws.Msk) + if err != nil { + a.log.Fatal(fmt.Sprint(err)) + } + a.sources = append(a.sources, mskSource) + } + if a.Config.Sources.Aws.Rds.Enabled { + rdsSource, err := rdsSource.NewSource(a.rdsApi, a.log, &a.Config.Sources.Aws.Rds) + if err != nil { + a.log.Fatal(fmt.Sprint(err)) + } + a.sources = append(a.sources, rdsSource) + } + if a.Config.Sources.Aws.Lambda.Enabled { + lambdaSource, err := lambdaSource.NewSource(a.lambdaApi, a.log, &a.Config.Sources.Aws.Lambda) + if err != nil { + a.log.Fatal(fmt.Sprint(err)) + } + a.sources = append(a.sources, lambdaSource) + } + return nil +} + +func (a *App) InitPrometheusMetrics() error { + labels := []string{"app", "app_type", "current_version", "target_version", "isparent", "parent", "action"} + scores := prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "upgrade_manager_software_obsolescence_score", + Help: "obsolescence score for softwares discovered by upgrade-manager app", + }, labels) + peLabels := []string{"app", "app_type", "isparent", "parent", "error_type"} + processError := prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "upgrade_manager_software_process_error", + Help: "errors while processing softwares", + }, peLabels) + foundSoftwares := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "upgrade_manager_total_software_found", + Help: "Total number of softwares found in the auto-discovery process", + }) + successLoads := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "upgrade_manager_total_software_load_success", + Help: "Total amount of software with successfully loaded candidates", + }) + successComputeScore := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "upgrade_manager_total_software_obsolescence_score_compute_success", + Help: "Total amount of software with successfully computed obsolescence score", + }) + loopExecTime := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "upgrade_manager_main_loop_execution_time", + Help: "Time taken by the last main loop execution (find software, check versions and compute score)", + }) + a.metrics.scores = scores + a.metrics.processError = processError + a.metrics.foundSoftwares = foundSoftwares + a.metrics.successLoads = successLoads + a.metrics.successComputeScore = successComputeScore + a.metrics.loopExecTime = loopExecTime + err := a.registry.Register(scores) + if err != nil { + return err + } + err = a.registry.Register(processError) + if err != nil { + return err + } + err = a.registry.Register(successComputeScore) + if err != nil { + return err + } + err = a.registry.Register(successLoads) + if err != nil { + return err + } + err = a.registry.Register(foundSoftwares) + if err != nil { + return err + } + err = a.registry.Register(loopExecTime) + if err != nil { + return err + } + return nil +} + +// Run the app's main process +func (a *App) Start() { + interval, err := time.ParseDuration(a.Config.Global.Interval) + if err != nil { + a.log.Error("Failed parsing time interval. Using a default interval of 1h", zap.Error(err)) + interval = time.Hour + } + ticker := time.NewTicker(interval) + + go func() { + a.mainLoop() + defer a.wg.Done() + for { + select { + case <-ticker.C: + a.reset() + a.mainLoop() + + case <-a.done: + return + } + } + }() + a.wg.Add(1) +} + +func (a *App) Stop() { + close(a.done) + a.wg.Wait() +} + +func (a *App) reset() { + a.softwares = nil + a.metrics.processError.MetricVec.Reset() + a.metrics.successLoads.Set(0) + a.metrics.successComputeScore.Set(0) +} + +func (a *App) mainLoop() { + startTime := time.Now() + + // Load here all apps as softwares + a.loadSoftwares() + a.metrics.successLoads.Add(float64(len(a.softwares))) + a.log.Sugar().Infof("Found %d software(s) in total", len(a.softwares)) + a.metrics.foundSoftwares.Set(float64(len(a.softwares))) + // Process each software + for _, software := range a.softwares { + a.log.Debug("computing obsolescence score", zap.String("software", software.Name)) + if err := a.scoreSoftware(software); err != nil { + a.log.Error("failed to compute obsolescence score", zap.String("software", software.Name), zap.String("software_type", string(software.Type))) + a.metrics.processError.WithLabelValues(software.Name, string(software.Type), "1", software.Name, "compute score").Add(1) + continue + } + a.metrics.successComputeScore.Add(1) + a.log.Debug("obsolescence score computed", zap.String("software", software.Name), zap.String("software_type", string(software.Type)), zap.Int("score", software.CalculatedScore)) + } + + // not in reset function because we want to wait as much as possible to avoid empty metrics + a.metrics.scores.Reset() + + // print report and update metrics + a.report() + duration := time.Since(startTime) + a.metrics.loopExecTime.Set(duration.Seconds()) + a.log.Info(fmt.Sprintf("Main loop execution time: %f seconds", duration.Seconds())) +} + +// Load Softwares from all sources +func (a *App) loadSoftwares() { + for _, source := range a.sources { + found, err := source.Load() + if err != nil { + a.log.Error("failed to load softwares", zap.Error(err), zap.String("source", source.Name())) + a.metrics.processError.WithLabelValues("", "", "", "", "load software").Add(1) + } + a.softwares = append(a.softwares, found...) + a.log.Info(fmt.Sprintf("Found %d software(s)", len(found)), zap.String("source", source.Name())) + } +} + +// Calculate obsolescence score for the software +func (a *App) scoreSoftware(software *soft.Software) error { + c := calculators.New(a.log, software.Calculator, true) + err := c.CalculateObsolescenceScore(software) + if err != nil { + return err + } + return nil +} + +func (a *App) report() { + // Recap + a.log.Debug("") + a.log.Debug("") + a.log.Debug("************************************* RECAP **********************************************") + for idx, software := range a.softwares { + a.log.Debug(fmt.Sprintf("%d) Application '%s' of type '%s'-> Score: %d", idx+1, software.Name, software.Type, software.CalculatedScore)) + if software.CalculatedScore > 0 { + if len(software.VersionCandidates) > 0 { + a.metrics.scores.WithLabelValues(software.Name, string(software.Type), software.Version.Version, software.VersionCandidates[0].Version, "1", software.Name, "update").Set(float64(software.CalculatedScore)) + a.log.Debug("--> update: ", zap.String("software", software.Name), zap.String("software_type", string(software.Type)), zap.String("parent", "self"), zap.String("version", software.Version.Version), zap.String("target_version", software.VersionCandidates[0].Version)) + } else { + for i, dep := range software.Dependencies { + if len(dep.VersionCandidates) > 0 { + a.metrics.scores.WithLabelValues(software.Name, string(software.Type), software.Version.Version, "", "1", software.Name, "update_dependencies").Set(float64(software.CalculatedScore)) + a.metrics.scores.WithLabelValues(dep.Name, string(dep.Type), dep.Version.Version, dep.VersionCandidates[0].Version, "0", software.Name, "update").Set(float64(dep.CalculatedScore)) + a.log.Debug(fmt.Sprintf("--> update %d: ", i+1), zap.String("software", dep.Name), zap.String("software_type", string(dep.Type)), zap.String("parent", software.Name), zap.String("version", dep.Version.Version), zap.String("target_version", dep.VersionCandidates[0].Version)) + } + } + } + } + if software.CalculatedScore == 0 { + a.metrics.scores.WithLabelValues(software.Name, string(software.Type), software.Version.Version, "", "1", software.Name, "").Set(float64(software.CalculatedScore)) + } + a.log.Debug("------------------------------------------------------------------------") + } +} diff --git a/internal/app/calculators/candidate_count.go b/internal/app/calculators/candidate_count.go new file mode 100644 index 0000000..37b951a --- /dev/null +++ b/internal/app/calculators/candidate_count.go @@ -0,0 +1,20 @@ +package calculators + +import ( + soft "github.com/qonto/upgrade-manager/internal/app/core/software" +) + +const DefaultPerCandidateScore = 30 + +type CandidateCountCalculator struct { + checkDependencies bool + perCandidateScore int +} + +func (c *CandidateCountCalculator) CalculateObsolescenceScore(s *soft.Software) error { + softwaresToCalculate := GetSoftwaresToCalculate(s, c.checkDependencies) + for _, software := range softwaresToCalculate { + software.CalculatedScore = len(s.VersionCandidates) * c.perCandidateScore + } + return nil +} diff --git a/internal/app/calculators/candidate_count_test.go b/internal/app/calculators/candidate_count_test.go new file mode 100644 index 0000000..48b4196 --- /dev/null +++ b/internal/app/calculators/candidate_count_test.go @@ -0,0 +1,42 @@ +package calculators + +import ( + "testing" + + "github.com/qonto/upgrade-manager/internal/app/core/software" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" +) + +func TestCandidateCountCalculateObsolescence(t *testing.T) { + testCases := []struct { + description string + soft *software.Software + expectedScore int + }{ + { + description: "many versions", + soft: &software.Software{ + VersionCandidates: []software.Version{ + {Version: "1.0.0"}, + {Version: "1.1.0"}, + {Version: "1.2.0"}, + }, + }, + expectedScore: 3 * DefaultPerCandidateScore, + }, + { + description: "no versions", + soft: &software.Software{}, + expectedScore: 0 * DefaultPerCandidateScore, + }, + } + calc := New(zap.NewExample(), software.CandidateCountCalculator, true) + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + err := calc.CalculateObsolescenceScore(tc.soft) + assert.NoError(t, err) + assert.Equal(t, tc.soft.CalculatedScore, tc.expectedScore) + }) + } +} diff --git a/internal/app/calculators/date.go b/internal/app/calculators/date.go new file mode 100644 index 0000000..532ebcc --- /dev/null +++ b/internal/app/calculators/date.go @@ -0,0 +1,34 @@ +package calculators + +import ( + "sort" + + soft "github.com/qonto/upgrade-manager/internal/app/core/software" +) + +type ReleaseDateCalculator struct { + checkDependencies bool +} + +func (c *ReleaseDateCalculator) CalculateObsolescenceScore(s *soft.Software) error { + softwaresToCalculate := GetSoftwaresToCalculate(s, c.checkDependencies) + totalDaysLate := 0 + for _, software := range softwaresToCalculate { + sort.Slice(software.VersionCandidates, func(i, j int) bool { + v1 := software.VersionCandidates[i] + v2 := software.VersionCandidates[j] + return v2.ReleaseDate.Before(v1.ReleaseDate) + }) + softwareDaysLate := 0 + currentDate := software.Version.ReleaseDate + for _, candidate := range software.VersionCandidates { + days := int(candidate.ReleaseDate.Sub(currentDate).Hours() / 24) + if days > softwareDaysLate { + softwareDaysLate = days + } + } + totalDaysLate += softwareDaysLate + } + s.CalculatedScore = totalDaysLate * 5 + return nil +} diff --git a/internal/app/calculators/date_test.go b/internal/app/calculators/date_test.go new file mode 100644 index 0000000..018bed3 --- /dev/null +++ b/internal/app/calculators/date_test.go @@ -0,0 +1,73 @@ +package calculators + +import ( + "testing" + "time" + + s "github.com/qonto/upgrade-manager/internal/app/core/software" + "go.uber.org/zap" +) + +func TestDateCalculateObsolescenceScore(t *testing.T) { + testCases := []struct { + software s.Software + expectedScore int + }{ + { + software: s.Software{ + Version: s.Version{ReleaseDate: time.Now()}, + VersionCandidates: []s.Version{}, + Dependencies: []*s.Software{ + { + VersionCandidates: []s.Version{{ReleaseDate: time.Now()}}, + Version: s.Version{ReleaseDate: time.Now().Add(-49 * time.Hour)}, + }, + { + VersionCandidates: []s.Version{{ReleaseDate: time.Now()}}, + Version: s.Version{ReleaseDate: time.Now().Add(-73 * time.Hour)}, + }, + }, + }, + expectedScore: 25, + }, + { + software: s.Software{ + Version: s.Version{ReleaseDate: time.Now()}, + VersionCandidates: []s.Version{}, + Dependencies: []*s.Software{ + { + VersionCandidates: []s.Version{ + {ReleaseDate: time.Now().Add(-48 * time.Hour)}, + {ReleaseDate: time.Now()}, + }, + Version: s.Version{ReleaseDate: time.Now().Add(-49 * time.Hour)}, + }, + { + VersionCandidates: []s.Version{{ReleaseDate: time.Now()}}, + Version: s.Version{ReleaseDate: time.Now().Add(-73 * time.Hour)}, + }, + }, + }, + expectedScore: 25, + }, + { + software: s.Software{ + Version: s.Version{ReleaseDate: time.Now().Add(-73 * time.Hour)}, + VersionCandidates: []s.Version{ + {ReleaseDate: time.Now().Add(-23 * time.Hour)}, + }, + }, + expectedScore: 10, + }, + } + calculator := New(zap.NewExample(), s.ReleaseDateCalculator, true) + for _, tc := range testCases { + err := calculator.CalculateObsolescenceScore(&tc.software) + if err != nil { + t.Fatal(err) + } + if tc.software.CalculatedScore != tc.expectedScore { + t.Fatalf("Expected score of %d, got: %d", tc.expectedScore, tc.software.CalculatedScore) + } + } +} diff --git a/internal/app/calculators/default.go b/internal/app/calculators/default.go new file mode 100644 index 0000000..f463976 --- /dev/null +++ b/internal/app/calculators/default.go @@ -0,0 +1,147 @@ +package calculators + +import ( + "fmt" + + goversion "github.com/hashicorp/go-version" + soft "github.com/qonto/upgrade-manager/internal/app/core/software" + "github.com/qonto/upgrade-manager/internal/app/semver" + "go.uber.org/zap" +) + +const ( + defaultMajorVersionScore = 50 + defaultMinorVersionScore = 5 + defaultPatchVersionScore = 1 +) + +// Each calculator can come up with its own logic for computing +// the Obsolescence score + +// The default calculator parses versions using Semantic Versioning. +// It takes the last version available and computes the obsolescence score: +// +// x major versions late = m * 50 +// +// y minor versions late = y * 5 +// +// z patch versions late = z * 1 +type DefaultCalculator struct { + log *zap.Logger + scoreTable segmentScoreTable + checkDependencies bool +} + +// Mapping of score to allocate per version segment +type segmentScoreTable []int + +// Provides a based on input type. Falls back on DefaultCalculator +// +// NOTE: Should DefaultCalculator be renamed to SemverCalculator +// and MetaCalculator be renamed to DefaultCalculator? +func New(logger *zap.Logger, t soft.CalculatorType, checkDependencies bool) soft.Calculator { + switch t { //nolint + case soft.ReleaseDateCalculator: + return &ReleaseDateCalculator{ + checkDependencies: checkDependencies, + } + case soft.MetaCalculator: + return &MetaCalculator{ + log: logger, + checkDependencies: checkDependencies, + } + case soft.AugmentedSemverCalculator: + return &DefaultCalculator{ + log: logger, + checkDependencies: checkDependencies, + scoreTable: segmentScoreTable{ + defaultMajorVersionScore * 2, + defaultMajorVersionScore, + defaultMinorVersionScore, + }, + } + case soft.SkipCalculator: + return &SkipCalculator{} + case soft.CandidateCountCalculator: + return &CandidateCountCalculator{ + perCandidateScore: DefaultPerCandidateScore, + } + default: + return &DefaultCalculator{ + log: logger, + checkDependencies: checkDependencies, + scoreTable: segmentScoreTable{ + defaultMajorVersionScore, + defaultMinorVersionScore, + defaultPatchVersionScore, + }, + } + } +} + +// Compute Obsolescence score by comparing current and last version +// and computes the obsolescence score +// +// x major versions late = m * 50 +// +// y minor versions late = y * 5 +// +// z patch versions late = z * 1 +func (c *DefaultCalculator) CalculateObsolescenceScore(s *soft.Software) error { + softwaresToCalculate := GetSoftwaresToCalculate(s, c.checkDependencies) + c.log.Debug(fmt.Sprintf("Total of %d softwares to compute in order to compute software %s's total score", len(softwaresToCalculate), s.Name)) + topLevelScore := 0 + for _, software := range softwaresToCalculate { + semver.Sort(software.VersionCandidates) + // Retrieve semantic versions + lv, err := goversion.NewSemver(software.VersionCandidates[0].Version) + if err != nil { + return fmt.Errorf("failed to parse latest version candidate's version %s using semver: %w", software.VersionCandidates[0].Version, err) + } + cv, err := goversion.NewSemver(software.Version.Version) + if err != nil { + return fmt.Errorf("failed to parse current version %s using semver, %w", software.Version.Version, err) + } + latestVersion := lv.Segments() + currentVersion := cv.Segments() + c.log.Debug(fmt.Sprintf("Latest version found is %s, comparing with current version %s", lv, cv)) + + // compute score + for i := 0; i <= len(currentVersion)-1; i++ { + diff := latestVersion[i] - currentVersion[i] + if diff != 0 { + topLevelScore += c.scoreTable[i] * diff + software.CalculatedScore += c.scoreTable[i] * diff + break + } + } + s.CalculatedScore = topLevelScore + } + return nil +} + +// Returns a list of softwares to compute a score for. +// If checkDependencies is true, include dependency softwares +// with a depth of 1 in the dependency tree +func GetSoftwaresToCalculate(s *soft.Software, checkDependencies bool) []*soft.Software { + softwares := []*soft.Software{} + + // if a software is late (if it has at least one candidate) at top-level + if len(s.VersionCandidates) > 0 { + // then we calculate its score and don't care about the dependencies + softwares = append(softwares, s) + } else if checkDependencies { + // No Version Candidates provided for top-level software. + // then we calculate the score of its dependencies + for _, dep := range s.Dependencies { + if len(dep.VersionCandidates) < 1 { + // No Version Candidates provided for dependency software + // We skip the dependency + continue + } else { + softwares = append(softwares, dep) + } + } + } + return softwares +} diff --git a/internal/app/calculators/default_test.go b/internal/app/calculators/default_test.go new file mode 100644 index 0000000..196fc3f --- /dev/null +++ b/internal/app/calculators/default_test.go @@ -0,0 +1,58 @@ +package calculators + +import ( + "fmt" + "testing" + + s "github.com/qonto/upgrade-manager/internal/app/core/software" + "go.uber.org/zap" +) + +func TestCalculateObsolescenceScore(t *testing.T) { + testCases := []struct { + software s.Software + expectedScore int + }{ + { + software: s.Software{ + Name: "no-deps-and-up-to-date", + Version: s.Version{Version: "11.1.1"}, + VersionCandidates: []s.Version{}, + Calculator: s.SemverCalculator, + }, + expectedScore: 0, + }, + { + software: s.Software{ + Name: "deps and up-to-date", + Version: s.Version{Version: "11.1.1"}, + VersionCandidates: []s.Version{}, + Calculator: s.SemverCalculator, + Dependencies: []*s.Software{ + { + Name: "dep1", + VersionCandidates: []s.Version{{Version: "17.0.1"}}, + Version: s.Version{Version: "11.1.1"}, + }, + { + Name: "dep2", + VersionCandidates: []s.Version{{Version: "3.0.1"}}, + Version: s.Version{Version: "3.0.0"}, + }, + }, + }, + expectedScore: 6*defaultMajorVersionScore + defaultPatchVersionScore, + }, + } + for _, tc := range testCases { + fmt.Println(tc.software.Name) + calculator := New(zap.NewExample(), tc.software.Calculator, true) + err := calculator.CalculateObsolescenceScore(&tc.software) + if err != nil { + t.Fatal(err) + } + if tc.software.CalculatedScore != tc.expectedScore { + t.Fatalf("Expected score of %d, got: %d", tc.expectedScore, tc.software.CalculatedScore) + } + } +} diff --git a/internal/app/calculators/meta.go b/internal/app/calculators/meta.go new file mode 100644 index 0000000..dac5b04 --- /dev/null +++ b/internal/app/calculators/meta.go @@ -0,0 +1,39 @@ +package calculators + +import ( + "github.com/qonto/upgrade-manager/internal/app/core/software" + "go.uber.org/zap" +) + +var calculatorCache = make(map[software.CalculatorType]software.Calculator) + +type MetaCalculator struct { + log *zap.Logger + checkDependencies bool +} + +// Entrypoint calculator which supports softwares with +// dependencies having different Calculator Types +func (c *MetaCalculator) CalculateObsolescenceScore(s *software.Software) error { + softwaresToCompute := GetSoftwaresToCalculate(s, true) + for _, soft := range softwaresToCompute { + var sCalculator software.Calculator + + if existingCalculator, ok := calculatorCache[soft.Calculator]; ok { + sCalculator = existingCalculator + } else { + calculatorCache[soft.Calculator] = New(c.log, soft.Calculator, false) + sCalculator = calculatorCache[soft.Calculator] + } + + if err := sCalculator.CalculateObsolescenceScore(soft); err != nil { + return err + } + } + if s.CalculatedScore == 0 { + for _, dep := range s.Dependencies { + s.CalculatedScore += dep.CalculatedScore + } + } + return nil +} diff --git a/internal/app/calculators/meta_test.go b/internal/app/calculators/meta_test.go new file mode 100644 index 0000000..a5a4ef9 --- /dev/null +++ b/internal/app/calculators/meta_test.go @@ -0,0 +1,49 @@ +package calculators + +import ( + "fmt" + "testing" + + s "github.com/qonto/upgrade-manager/internal/app/core/software" + "go.uber.org/zap" +) + +func TestMetaCalculateObsolescenceScore(t *testing.T) { + testCases := []struct { + software s.Software + expectedScore int + }{ + { + software: s.Software{ + Name: "EKS-cluster-metacalculator", + Calculator: s.MetaCalculator, + Dependencies: []*s.Software{ + { + Name: "k8s", + VersionCandidates: []s.Version{{Version: "1.24"}}, + Version: s.Version{Version: "1.23"}, + Calculator: s.AugmentedSemverCalculator, + }, + { + Name: "ebs-addon", + VersionCandidates: []s.Version{{Version: "1.3.4"}}, + Version: s.Version{Version: "1.0.0"}, + Calculator: s.SemverCalculator, + }, + }, + }, + expectedScore: defaultMajorVersionScore + defaultMinorVersionScore*3, + }, + } + for _, tc := range testCases { + fmt.Println(tc.software.Name) + calculator := New(zap.NewExample(), tc.software.Calculator, true) + err := calculator.CalculateObsolescenceScore(&tc.software) + if err != nil { + t.Fatal(err) + } + if tc.software.CalculatedScore != tc.expectedScore { + t.Fatalf("Expected score of %d, got: %d", tc.expectedScore, tc.software.CalculatedScore) + } + } +} diff --git a/internal/app/calculators/skip.go b/internal/app/calculators/skip.go new file mode 100644 index 0000000..f6992bd --- /dev/null +++ b/internal/app/calculators/skip.go @@ -0,0 +1,12 @@ +package calculators + +import ( + soft "github.com/qonto/upgrade-manager/internal/app/core/software" +) + +// Accepts the arbitrarily set score by the source +type SkipCalculator struct{} + +func (c *SkipCalculator) CalculateObsolescenceScore(s *soft.Software) error { + return nil +} diff --git a/internal/app/core/software/contracts.go b/internal/app/core/software/contracts.go new file mode 100644 index 0000000..7eab664 --- /dev/null +++ b/internal/app/core/software/contracts.go @@ -0,0 +1,10 @@ +package software + +type Source interface { + Load() ([]*Software, error) + Name() string // return source name +} + +type Calculator interface { + CalculateObsolescenceScore(software *Software) error +} diff --git a/internal/app/core/software/software.go b/internal/app/core/software/software.go new file mode 100644 index 0000000..eba6614 --- /dev/null +++ b/internal/app/core/software/software.go @@ -0,0 +1,52 @@ +package software + +import ( + "fmt" +) + +type SoftwaresToUpdate struct { + Softwares []Software +} + +type CalculatorType string + +const ( + // Augmented Semver: for softwares where minor versions are considered major versions (ex: kubernetes 1.21 -> 1.22) + AugmentedSemverCalculator CalculatorType = "augmented-semver" + CandidateCountCalculator CalculatorType = "candidate-count" + MetaCalculator CalculatorType = "meta" + ReleaseDateCalculator CalculatorType = "release-date" + SemverCalculator CalculatorType = "semver" + SkipCalculator CalculatorType = "skip" +) + +type SoftwareType string + +type Software struct { + Calculator CalculatorType + CalculatedScore int + Dependencies []*Software + Name string + Type SoftwareType + Version Version + VersionCandidates []Version +} + +func ToCalculator(name string) (CalculatorType, error) { + switch name { + case string(SkipCalculator): + return SkipCalculator, nil + case string(AugmentedSemverCalculator): + return AugmentedSemverCalculator, nil + case string(CandidateCountCalculator): + return CandidateCountCalculator, nil + case string(MetaCalculator): + return MetaCalculator, nil + case string(ReleaseDateCalculator): + return ReleaseDateCalculator, nil + case string(SemverCalculator): + return SemverCalculator, nil + default: + return "", fmt.Errorf("Unknown calculator %s", name) + } +} diff --git a/internal/app/core/software/version.go b/internal/app/core/software/version.go new file mode 100644 index 0000000..b1a63ad --- /dev/null +++ b/internal/app/core/software/version.go @@ -0,0 +1,33 @@ +package software + +import "time" + +type Versions []*Version + +type Version struct { + Name string + Version string + ReleaseDate time.Time +} + +func (vs *Versions) Deduplicate() { + allKeys := make(map[string]bool) + list := Versions{} + for _, item := range *vs { + if _, value := allKeys[item.Version]; !value { + allKeys[item.Version] = true + list = append(list, item) + } + } + *vs = list +} + +func ToVersion(v string, optsfn ...func(*Version)) *Version { + version := &Version{ + Version: v, + } + for _, fn := range optsfn { + fn(version) + } + return version +} diff --git a/internal/app/filters/date.go b/internal/app/filters/date.go new file mode 100644 index 0000000..c45eb10 --- /dev/null +++ b/internal/app/filters/date.go @@ -0,0 +1,20 @@ +package filters + +import ( + "time" + + "github.com/qonto/upgrade-manager/internal/app/core/software" +) + +type RecentVersionsConfig struct { + Days uint `yaml:"days"` +} + +func RecentVersions(config RecentVersionsConfig) Filter { + return func(_ software.Version, candidateVersion software.Version) bool { + if candidateVersion.ReleaseDate.IsZero() { + return true + } + return candidateVersion.ReleaseDate.Add(time.Duration(config.Days) * 24 * time.Hour).Before(time.Now()) + } +} diff --git a/internal/app/filters/date_test.go b/internal/app/filters/date_test.go new file mode 100644 index 0000000..3fbd0fa --- /dev/null +++ b/internal/app/filters/date_test.go @@ -0,0 +1,45 @@ +package filters + +import ( + "testing" + "time" + + "github.com/qonto/upgrade-manager/internal/app/core/software" + "github.com/stretchr/testify/assert" +) + +func TestRecentVersions(t *testing.T) { + filter := RecentVersions(RecentVersionsConfig{Days: 20}) + + cases := []filterTestCase{ + { + Current: software.Version{}, + Candidate: software.Version{Version: "1.1.1"}, + Result: true, + }, + { + Current: software.Version{}, + Candidate: software.Version{Version: "1.1.1", ReleaseDate: time.Now().Add(-24 * 30 * time.Hour)}, + Result: true, + }, + { + Current: software.Version{}, + Candidate: software.Version{Version: "1.1.1", ReleaseDate: time.Now().Add(-24 * 21 * time.Hour)}, + Result: true, + }, + { + Current: software.Version{}, + Candidate: software.Version{Version: "1.1.1", ReleaseDate: time.Now().Add(-24 * 19 * time.Hour)}, + Result: false, + }, + { + Current: software.Version{}, + Candidate: software.Version{Version: "1.1.1", ReleaseDate: time.Now().Add(-24 * 10 * time.Hour)}, + Result: false, + }, + } + for _, c := range cases { + result := filter(c.Current, c.Candidate) + assert.Equalf(t, result, c.Result, "wrong result on versions %s/%s", c.Current.Version, c.Candidate.Version) + } +} diff --git a/internal/app/filters/filter.go b/internal/app/filters/filter.go new file mode 100644 index 0000000..06e1582 --- /dev/null +++ b/internal/app/filters/filter.go @@ -0,0 +1,32 @@ +package filters + +import ( + "github.com/qonto/upgrade-manager/internal/app/core/software" +) + +type Config struct { + SemverVersions *SemverVersionsConfig `yaml:"semver-versions"` + RecentVersions *RecentVersionsConfig `yaml:"recent-versions"` +} + +type Filter func(currentVersion software.Version, candidateVersion software.Version) bool + +func Build(config Config) Filter { + filters := []Filter{} + if config.SemverVersions != nil { + filters = append(filters, SemverVersions(*config.SemverVersions)) + } + if config.RecentVersions != nil { + filters = append(filters, RecentVersions(*config.RecentVersions)) + } + return func(currentVersion software.Version, candidateVersion software.Version) bool { + for _, filter := range filters { + result := filter(currentVersion, candidateVersion) + + if !result { + return false + } + } + return true + } +} diff --git a/internal/app/filters/filter_test.go b/internal/app/filters/filter_test.go new file mode 100644 index 0000000..0fee504 --- /dev/null +++ b/internal/app/filters/filter_test.go @@ -0,0 +1,55 @@ +package filters + +import ( + "testing" + "time" + + "github.com/qonto/upgrade-manager/internal/app/core/software" + "github.com/stretchr/testify/assert" +) + +func TestBuild(t *testing.T) { + config := Config{ + SemverVersions: &SemverVersionsConfig{ + RemovePreRelease: true, + RemoveFirstMajorVersion: true, + }, + RecentVersions: &RecentVersionsConfig{ + Days: 20, + }, + } + filter := Build(config) + + cases := []filterTestCase{ + { + Current: software.Version{Version: "1.1.0"}, + Candidate: software.Version{Version: "1.1.1-rc", ReleaseDate: time.Now().Add(-24 * 30 * time.Hour)}, + Result: false, + }, + { + Current: software.Version{Version: "1.1.0"}, + Candidate: software.Version{Version: "1.1.1-rc", ReleaseDate: time.Now().Add(-24 * 30 * time.Hour)}, + Result: false, + }, + { + Current: software.Version{Version: "1.1.0"}, + Candidate: software.Version{Version: "1.1.1", ReleaseDate: time.Now().Add(-24 * 30 * time.Hour)}, + Result: true, + }, + { + Current: software.Version{Version: "1.1.0"}, + Candidate: software.Version{Version: "1.1.1", ReleaseDate: time.Now().Add(-24 * 18 * time.Hour)}, + Result: false, + }, + { + Current: software.Version{Version: "1.1.0"}, + Candidate: software.Version{Version: "3.0.0-beta2"}, + Result: false, + }, + } + + for _, c := range cases { + result := filter(c.Current, c.Candidate) + assert.Equalf(t, result, c.Result, "wrong result on versions %s/%s", c.Current.Version, c.Candidate.Version) + } +} diff --git a/internal/app/filters/semver.go b/internal/app/filters/semver.go new file mode 100644 index 0000000..7c94c6a --- /dev/null +++ b/internal/app/filters/semver.go @@ -0,0 +1,40 @@ +package filters + +import ( + goversion "github.com/hashicorp/go-version" + "github.com/qonto/upgrade-manager/internal/app/core/software" +) + +type SemverVersionsConfig struct { + RemovePreRelease bool `yaml:"remove-pre-release"` + RemoveFirstMajorVersion bool `yaml:"remove-first-major-version"` +} + +func SemverVersions(config SemverVersionsConfig) Filter { + return func(currentVersion software.Version, candidateVersion software.Version) bool { + current, err := goversion.NewSemver(currentVersion.Version) + if err != nil { + // keep invalid semver releases + return true + } + candidate, err := goversion.NewSemver(candidateVersion.Version) + if err != nil { + return true + } + candidateSegments := candidate.Segments() + if current.GreaterThan(candidate) { + return false + } + if current.Equal(candidate) { + return false + } + min, patch := candidateSegments[1], candidateSegments[2] + if config.RemovePreRelease && candidate.Prerelease() != "" { + return false + } + if config.RemoveFirstMajorVersion && min == 0 && patch == 0 { + return false + } + return true + } +} diff --git a/internal/app/filters/semver_test.go b/internal/app/filters/semver_test.go new file mode 100644 index 0000000..6a1a24d --- /dev/null +++ b/internal/app/filters/semver_test.go @@ -0,0 +1,127 @@ +package filters + +import ( + "testing" + + "github.com/qonto/upgrade-manager/internal/app/core/software" + "github.com/stretchr/testify/assert" +) + +type filterTestCase struct { + Current software.Version + Candidate software.Version + Result bool +} + +func TestSemverVersionsIgnorePreRelease(t *testing.T) { + filter := SemverVersions(SemverVersionsConfig{RemovePreRelease: true}) + + cases := []filterTestCase{ + { + Current: software.Version{Version: "1.1.0"}, + Candidate: software.Version{Version: "1.1.1-rc"}, + + Result: false, + }, + { + Current: software.Version{Version: "1.1.0"}, + Candidate: software.Version{Version: "1.1.0"}, + + Result: false, + }, + { + Current: software.Version{Version: "1.1.0"}, + Candidate: software.Version{Version: "1.1.1"}, + + Result: true, + }, + { + Current: software.Version{Version: "1.1.0"}, + Candidate: software.Version{Version: "1.0.0"}, + + Result: false, + }, + { + Current: software.Version{Version: "1.1.0"}, + Candidate: software.Version{Version: "0.0.1"}, + + Result: false, + }, + { + Current: software.Version{Version: "1.1.0"}, + Candidate: software.Version{Version: "2.0.0"}, + + Result: true, + }, + { + Current: software.Version{Version: "1.1.0a"}, + Candidate: software.Version{Version: "2.0.0"}, + Result: true, + }, + { + Current: software.Version{Version: "1.1.0"}, + Candidate: software.Version{Version: "2.0.0a"}, + Result: true, + }, + } + for _, c := range cases { + result := filter(c.Current, c.Candidate) + assert.Equalf(t, result, c.Result, "wrong result on versions %s/%s", c.Current.Version, c.Candidate.Version) + } +} + +func TestSemverVersionsIgnoreFirstMajor(t *testing.T) { + filter := SemverVersions(SemverVersionsConfig{RemoveFirstMajorVersion: true}) + + cases := []filterTestCase{ + { + Current: software.Version{Version: "1.1.0"}, + Candidate: software.Version{Version: "1.1.1-rc"}, + Result: true, + }, + { + Current: software.Version{Version: "1.1.0"}, + Candidate: software.Version{Version: "2.0.1"}, + Result: true, + }, + { + Current: software.Version{Version: "1.1.0"}, + Candidate: software.Version{Version: "2.0.0"}, + Result: false, + }, + { + Current: software.Version{Version: "1.1.0"}, + Candidate: software.Version{Version: "3.0.0"}, + Result: false, + }, + { + Current: software.Version{Version: "1.1.0"}, + Candidate: software.Version{Version: "1.1.1"}, + Result: true, + }, + { + Current: software.Version{Version: "1.1.0"}, + Candidate: software.Version{Version: "1.0.0"}, + Result: false, + }, + { + Current: software.Version{Version: "1.1.0"}, + Candidate: software.Version{Version: "0.0.1"}, + Result: false, + }, + { + Current: software.Version{Version: "1.1.0a"}, + Candidate: software.Version{Version: "2.0.0"}, + Result: true, + }, + { + Current: software.Version{Version: "1.1.0"}, + Candidate: software.Version{Version: "2.0.0a"}, + Result: true, + }, + } + for _, c := range cases { + result := filter(c.Current, c.Candidate) + assert.Equalf(t, result, c.Result, "wrong result on versions %s/%s", c.Current.Version, c.Candidate.Version) + } +} diff --git a/internal/app/semver/semver.go b/internal/app/semver/semver.go new file mode 100644 index 0000000..9017c0d --- /dev/null +++ b/internal/app/semver/semver.go @@ -0,0 +1,25 @@ +package semver + +import ( + "sort" + + goversion "github.com/hashicorp/go-version" + "github.com/qonto/upgrade-manager/internal/app/core/software" +) + +func Sort(versions []software.Version) { + sort.Slice(versions, func(i, j int) bool { + iVersion, _ := goversion.NewSemver(versions[i].Version) + jVersion, _ := goversion.NewSemver(versions[j].Version) + // Filtering out versions older than current version + return iVersion.Core().Compare(jVersion.Core()) == 1 + }) +} + +func ExtractFromString(rawString string) (string, error) { + v, err := goversion.NewSemver(rawString) + if err != nil { + return "", err + } + return v.Core().String(), nil +} diff --git a/internal/app/semver/semver_test.go b/internal/app/semver/semver_test.go new file mode 100644 index 0000000..83b0541 --- /dev/null +++ b/internal/app/semver/semver_test.go @@ -0,0 +1,63 @@ +package semver + +import ( + "testing" + + "github.com/qonto/upgrade-manager/internal/app/core/software" +) + +func TestSortSoftwareVersions(t *testing.T) { + testCases := []struct { + versions []software.Version + expected string + }{ + { + versions: []software.Version{ + { + Version: "7.0.0", + }, + { + Version: "6.0.0", + }, + { + Version: "5.0.0", + }, + }, + expected: "7.0.0", + }, + { + versions: []software.Version{ + { + Version: "5.0.0", + }, + { + Version: "6.0.0", + }, + { + Version: "7.0.0", + }, + }, + expected: "7.0.0", + }, + } + for idx, testCase := range testCases { + Sort(testCase.versions) + if testCase.versions[0].Version != testCase.expected { + t.Errorf("Case %d, wrong first element in sorted slice. Expected %s, got: %s", idx+1, testCase.expected, testCase.versions[0].Version) + } + } +} + +func TestExtractFromString(t *testing.T) { + versions := software.Versions{ + {Version: "1.2.3"}, + {Version: "1.2.3-eksbuild.v2"}, + {Version: "1.2.3-alpha"}, + } + for _, v := range versions { + semVer, err := ExtractFromString(v.Version) + if semVer != "1.2.3" || err != nil { + t.Errorf("error while parsing semantic version") + } + } +} diff --git a/internal/app/sources/argohelm/config.go b/internal/app/sources/argohelm/config.go new file mode 100644 index 0000000..e3353ac --- /dev/null +++ b/internal/app/sources/argohelm/config.go @@ -0,0 +1,21 @@ +package argohelm + +import ( + "github.com/qonto/upgrade-manager/internal/app/filters" + "github.com/qonto/upgrade-manager/internal/infra/kubernetes" +) + +type Config struct { + Enabled bool `yaml:"enabled"` + Name string `yaml:"name"` + ClusterURL string `yaml:"cluster-url"` + ArgoCDNamespace string `yaml:"argocd-namespace" validate:"required"` + GitSecretsNamespace string `yaml:"git-credentials-secrets-namespace" validate:"required"` + GitCredentialsSecretsPattern string `yaml:"git-credentials-secrets-pattern" validate:"required"` + Filters FiltersConfig `yaml:"filters"` +} + +type FiltersConfig struct { + filters.Config `yaml:",inline"` + kubernetes.FiltersOptions `yaml:",inline"` +} diff --git a/internal/app/sources/argohelm/git_credentials.go b/internal/app/sources/argohelm/git_credentials.go new file mode 100644 index 0000000..de7a727 --- /dev/null +++ b/internal/app/sources/argohelm/git_credentials.go @@ -0,0 +1,65 @@ +package argohelm + +import ( + "context" + "fmt" + "regexp" + "strconv" + "time" + + "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/go-git/go-git/v5/plumbing/transport/ssh" + "github.com/qonto/upgrade-manager/internal/app/sources/utils/gitutils" + k8sClient "github.com/qonto/upgrade-manager/internal/infra/kubernetes" + "go.uber.org/zap" +) + +// Retrieve all git credentials in the namespace which have "repo" in their name +// +// We assume the secrets follow the argocd schema for secret.Data, meaning that +// git http secrets have the following data keys: "type: git", "password; xxx", +// "url: yyyxxx", "username: zzz" +// +// ssh secrets have "type: ssh", "sshPrivateKey; xxx", "url: yyyxxx" +func getGitRepoConnections(namespace string, r *regexp.Regexp, client k8sClient.KubernetesClient, log *zap.Logger) ([]*gitutils.RepoConnection, error) { + var connections []*gitutils.RepoConnection + ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) + defer cancel() + secrets, err := client.ListSecrets(ctx, namespace) + if err != nil { + return nil, err + } + log.Debug(fmt.Sprintf("Found %d secrets in namespace %s", len(secrets.Items), namespace)) + + for _, secret := range secrets.Items { + isRepoCredSecret := r.MatchString(secret.Name) + // TODO: handle case where ssh secret has no url to add the key to the local ssh-agent + // (or just implement trying all keys when trying a repo clone via ssh before using default auth?) + if isRepoCredSecret && secret.Data["url"] != nil { + repo := &gitutils.RepoConnection{} + repo.Url = string(secret.Data["url"]) + switch { + case secret.Data["username"] != nil && secret.Data["password"] != nil: + // if http connection credentials return http auth type with username/password + repo.Private = true + repo.Auth = &http.BasicAuth{Username: string(secret.Data["username"]), Password: string(secret.Data["password"])} + log.Debug(fmt.Sprintf("Adding connection of type https to git url %s from secret %s", repo.Url, secret.Name)) + connections = append(connections, repo) + case secret.Data["sshPrivateKey"] != nil && string(secret.Data["type"]) == "ssh": + // if ssh connection credentials return ssh auth type with the key + keys, err := ssh.NewPublicKeys("upgrade-manager", secret.Data["sshPrivateKey"], "") + if err != nil { + log.Warn(fmt.Sprintf("skipping secret %s: could not load key. %s", secret.Name, err)) + continue + } + repo.Auth = keys + repo.Private = true + connections = append(connections, repo) + default: + log.Debug(fmt.Sprintf("Skipping repo secret %s: not a proper ssh or https git repo. isRepoCredsSecret :%s, url:%s", secret.Name, strconv.FormatBool(isRepoCredSecret), string(secret.Data["url"]))) + continue + } + } + } + return connections, err +} diff --git a/internal/app/sources/argohelm/git_credentials_test.go b/internal/app/sources/argohelm/git_credentials_test.go new file mode 100644 index 0000000..d64fe86 --- /dev/null +++ b/internal/app/sources/argohelm/git_credentials_test.go @@ -0,0 +1,88 @@ +package argohelm + +import ( + "os" + "regexp" + "testing" + + "github.com/qonto/upgrade-manager/internal/infra/kubernetes" + "go.uber.org/zap" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestGetGitCredentialSecretsFromNamespace(t *testing.T) { + log := zap.NewExample() + sampleKeyPath := "../../_testdata/fakeSampleKey" + f, err := os.ReadFile(sampleKeyPath) + if err != nil { + t.Errorf("could not read sample private key at , %s", err) + } + k8sMock := new(kubernetes.KubernetesClientMock) + k8sMock.On("ListSecrets", "argocd").Return(&v1.SecretList{ + Items: []v1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "1-argocd-repo-secret-https", + Namespace: "argocd", + Annotations: map[string]string{}, + }, + Data: map[string][]byte{ + "url": []byte("https://repo1.git"), + "username": []byte("myuser"), + "password": []byte("mypassword"), + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "2-argocd-repo-secret-ssh", + Namespace: "argocd", + Annotations: map[string]string{}, + }, + Data: map[string][]byte{ + "sshPrivateKey": f, + "type": []byte("ssh"), + "url": []byte("git@repo2:namespace/project.git"), + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "3-argocd-repo-secret-https-malformed", // missing username + Namespace: "argocd", + Annotations: map[string]string{}, + }, + Data: map[string][]byte{ + "url": []byte("https://repo1.git"), + "password": []byte("mypassword"), + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "4-argocd-repo-secret-ssh-malformed", // malformed key + Namespace: "argocd", + Annotations: map[string]string{}, + }, + Data: map[string][]byte{ + "sshPrivateKey": []byte("malformedkey"), + "type": []byte("ssh"), + "url": []byte("git@repo2:namespace/project.git"), + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "5-not-a-creds-secret", + Namespace: "argocd", + Annotations: map[string]string{}, + }, + }, + }, + }, nil) + r := regexp.MustCompile(".*-repo-.*") + conns, err := getGitRepoConnections("argocd", r, k8sMock, log) + if err != nil { + t.Error(err) + } + if expectedConnCount := 2; len(conns) != expectedConnCount { + t.Errorf("found wrong number of git connections. Expected %d, got: %d", expectedConnCount, len(conns)) + } +} diff --git a/internal/app/sources/argohelm/source.go b/internal/app/sources/argohelm/source.go new file mode 100644 index 0000000..82b9aa8 --- /dev/null +++ b/internal/app/sources/argohelm/source.go @@ -0,0 +1,220 @@ +package argohelm + +import ( + "context" + "fmt" + "os" + "regexp" + "strconv" + "time" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/transport/http" + soft "github.com/qonto/upgrade-manager/internal/app/core/software" + "github.com/qonto/upgrade-manager/internal/app/filters" + "github.com/qonto/upgrade-manager/internal/app/sources/helm/versions" + "github.com/qonto/upgrade-manager/internal/app/sources/utils/gitutils" + "github.com/qonto/upgrade-manager/internal/infra/aws" + "github.com/qonto/upgrade-manager/internal/infra/kubernetes" + "go.uber.org/zap" + "helm.sh/helm/v3/pkg/chart" +) + +const ArgoHelm soft.SoftwareType = "argoHelm" + +type Source struct { + k8sClient kubernetes.KubernetesClient + log *zap.Logger + gitRepoConnections []*gitutils.RepoConnection + cfg Config + s3Api aws.S3Api + versionFilter filters.Filter +} + +// Returns a new argohelm software source +func NewSource(cfg Config, log *zap.Logger, k8sClient kubernetes.KubernetesClient, loadSecretFromNamespace bool, s3Api aws.S3Api) (*Source, error) { + if cfg.Filters.SemverVersions == nil { + cfg.Filters.SemverVersions = &filters.SemverVersionsConfig{} + } + chartFilter := filters.Build(cfg.Filters.Config) + s := &Source{ + log: log, + cfg: cfg, + k8sClient: k8sClient, + s3Api: s3Api, + versionFilter: chartFilter, + } + + if loadSecretFromNamespace { + if cfg.GitCredentialsSecretsPattern == "" { + cfg.GitCredentialsSecretsPattern = ".*-repo-.*" + } + r := regexp.MustCompile(cfg.GitCredentialsSecretsPattern) + connections, err := getGitRepoConnections(cfg.GitSecretsNamespace, r, k8sClient, log) + if err != nil { + s.log.Error(fmt.Sprintf("Failed to get Git Credential Secrets from namespace %s", cfg.GitSecretsNamespace)) + return nil, err + } + s.gitRepoConnections = connections + log.Info(fmt.Sprintf("Found %d connections", len(connections))) + } + return s, nil +} + +// TODO: fn on pointers +func (s *Source) Name() string { + return "argohelm" +} + +// Detects and provides a list of argocd helm applications as softwares +func (s *Source) Load() ([]*soft.Software, error) { + var softwares []*soft.Software + ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) + defer cancel() + filter, err := kubernetes.NewDestinationNamespaceFilter(s.cfg.Filters.FiltersOptions) + if err != nil { + return nil, err + } + apps, err := s.k8sClient.ListArgoApplications(ctx, s.cfg.ArgoCDNamespace, filter) + if err != nil { + return nil, err + } + s.log.Info(fmt.Sprintf("Found %d applications in %s namespace", len(apps), s.cfg.ArgoCDNamespace)) + for _, app := range apps { + topLevelSoftware, chart, err := s.argoAppToSoftware(app) + if err != nil { + s.log.Warn(fmt.Sprintf("Could not convert argo app %s to software: %s", app.Name, err)) + continue + } + err = versions.PopulateTopLevelSoftware(s.s3Api, s.log, topLevelSoftware, app.RepoURL, app.Chart, s.versionFilter) + if err != nil { + s.log.Warn(fmt.Sprintf("Could not populate top level software %s: %s", app.Name, err)) + } + if chart != nil { + err = versions.PopulateSoftwareDependencies(s.s3Api, s.log, topLevelSoftware, chart, ArgoHelm, s.versionFilter) + if err != nil { + s.log.Error(fmt.Sprintf("Could not load %s chart as software, error: %s", chart.Name(), err)) + continue + } + } + softwares = append(softwares, topLevelSoftware) + } + return softwares, nil +} + +// Convert Argo app to Software +func (s *Source) argoAppToSoftware(app *kubernetes.ArgoCDApplication) (*soft.Software, *chart.Chart, error) { + s.log.Debug(fmt.Sprintf("Converting argo app %s to software", app.Name)) + switch app.RepoBackendType { //nolint + case versions.HelmRepo: + // TODO: consider if we check dependencies or if we consider only the top-level chart in this case + software := &soft.Software{ + Name: app.Name, + Version: soft.Version{Version: app.CurrentVersion}, + Type: ArgoHelm, + } + s.log.Info(fmt.Sprintf("Adding software %s ", software.Name)) + return software, nil, nil + + case versions.GitRepo: + conn, err := s.matchGitRepoConnection(app.RepoURL) + if err != nil { + return nil, nil, err + } + dirName := os.TempDir() + "/tmp_" + app.Name + app.GitRevision + strconv.Itoa(int(time.Now().Unix())) + chart, err := getChart(dirName, conn, app.RepoURL, app.ChartFilePath, app.GitRevision) + if err != nil { + return nil, nil, err + } + software := &soft.Software{ + Name: chart.Name(), + Version: soft.Version{ + Version: versions.CleanVersion(chart.Metadata.Version), + }, + Type: ArgoHelm, + } + return software, &chart, nil + default: + return nil, nil, fmt.Errorf("unexpected repo type %s", app.RepoBackendType) + } +} + +// Retrieve Chart.yaml from the chart remote RepoUrl (git) +func getChart(dirName string, conn gitutils.RepoConnectionProvider, remoteRepoUrl string, chartfileDir string, revision string) (chart.Chart, error) { + r, err := conn.Clone(dirName, remoteRepoUrl, revision) + if err != nil { + return chart.Chart{}, err + } + w, err := r.Worktree() + if err != nil { + return chart.Chart{}, err + } + err = w.Checkout(&git.CheckoutOptions{Branch: plumbing.ReferenceName(revision), Create: true}) + if err != nil { + return chart.Chart{}, err + } + var c chart.Chart + if chartfileDir != "" { + c, err = versions.LoadChartFile(fmt.Sprintf("%s/%s/%s", dirName, chartfileDir, "Chart.yaml")) + if err != nil { + return chart.Chart{}, err + } + } else { + c, err = versions.LoadChartFile(fmt.Sprintf("%s/%s", dirName, "Chart.yaml")) + if err != nil { + return chart.Chart{}, err + } + } + err = os.RemoveAll(dirName) + if err != nil { + return chart.Chart{}, err + } + return c, nil +} + +// Retrieve the appropriate gitRepoConnection for the url +// based on repo url regex matching +func (s *Source) matchGitRepoConnection(url string) (*gitutils.RepoConnection, error) { + // if url matches exactly an existing repo connection, use it + + for _, conn := range s.gitRepoConnections { + if conn.Url == url { + s.log.Debug(fmt.Sprintf("found matching repo credential for %s", conn.Url)) + return conn, nil + } + } + + var foundCredentialTemplates []*gitutils.RepoConnection + + // else if url matches pattern of existing repo connection, use it + for _, conn := range s.gitRepoConnections { + r, err := regexp.Compile(conn.Url + ".*") + if err != nil { + return nil, err + } + if r.MatchString(url) { + s.log.Debug(fmt.Sprintf("Found matching repo credential template %s for %s", conn.Url, url)) + foundCredentialTemplates = append(foundCredentialTemplates, conn) + } + } + + if len(foundCredentialTemplates) > 0 { + if len(foundCredentialTemplates) > 1 { + s.log.Warn(fmt.Sprintf("Found %d (more than 1) repo credential templates for %s. Using first one found...", len(foundCredentialTemplates), url)) + } + return foundCredentialTemplates[0], nil + } + + // else use a default unauthenticated repo backend + s.log.Debug(fmt.Sprintf("using unauthenticated backend for %s", url)) + + return &gitutils.RepoConnection{ + Auth: &http.BasicAuth{ + Username: "", + Password: "", + }, + Url: url, + Private: false, + }, nil +} diff --git a/internal/app/sources/argohelm/source_test.go b/internal/app/sources/argohelm/source_test.go new file mode 100644 index 0000000..f13ec7a --- /dev/null +++ b/internal/app/sources/argohelm/source_test.go @@ -0,0 +1,70 @@ +package argohelm + +import ( + "testing" + + "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/qonto/upgrade-manager/internal/app/sources/utils/gitutils" + "go.uber.org/zap" +) + +func TestMatchGitRepoConnection(t *testing.T) { + s := Source{log: zap.NewExample()} + s.gitRepoConnections = []*gitutils.RepoConnection{ + { + Url: "https://github.foo.com/devops/kubernetes-resources/exactmatch.git", + }, + { + Url: "git@github.foo.com:devops/kubernetes-resources/exactmatch.git", + }, + { + Url: "https://prefix2/", + }, + { + Url: "https://prefix3.com", + }, + { + Url: "https://prefix1/", + }, + } + testCases := []struct { + url string + expectedMatchCount int + }{ + { + url: "https://github.foo.com/devops/kubernetes-resources/exactmatch.git", + expectedMatchCount: 2, // exact match + prefix for host in https mode + }, + { + url: "git@github.foo.com:devops/kubernetes-resources/exactmatch.git", + expectedMatchCount: 2, // exact match + prefix for host in https mode + }, + { + url: "https://github.foo.com/devops/kubernetes-resources/onematch.git", + expectedMatchCount: 1, + }, + { + url: "git@github.foo.com:devops/kubernetes-resources/onematch.git", + expectedMatchCount: 1, + }, + { + url: "git@github.foo.com:devoopsie/kubernetes-resources/nomatch.git", + expectedMatchCount: 0, + }, + } + for i, tc := range testCases { + conn, err := s.matchGitRepoConnection(tc.url) + if err != nil { + t.Error(err) + } + if tc.expectedMatchCount == 0 { + pass, ok := conn.Auth.(*http.BasicAuth) + if !ok { + t.Fatalf("Case %d: Expected 0 match and therefore a default empty basic auth. But was not basic auth", i+1) + } + if pass.Password != "" || pass.Username != "" { + t.Fatalf("Case %d: Expected 0 match and therefore a default empty basic auth. Password : %s, Username: %s", i+1, pass.Password, pass.Username) + } + } + } +} diff --git a/internal/app/sources/aws/config.go b/internal/app/sources/aws/config.go new file mode 100644 index 0000000..78230fa --- /dev/null +++ b/internal/app/sources/aws/config.go @@ -0,0 +1,17 @@ +package awsSource + +import ( + "github.com/qonto/upgrade-manager/internal/app/sources/aws/eks" + "github.com/qonto/upgrade-manager/internal/app/sources/aws/elasticache" + "github.com/qonto/upgrade-manager/internal/app/sources/aws/lambda" + "github.com/qonto/upgrade-manager/internal/app/sources/aws/msk" + "github.com/qonto/upgrade-manager/internal/app/sources/aws/rds" +) + +type Config struct { + Elasticache elasticache.Config `yaml:"elasticache"` + Eks eks.Config `yaml:"eks"` + Rds rds.Config `yaml:"rds"` + Lambda lambda.Config `yaml:"lambda"` + Msk msk.Config `yaml:"msk"` +} diff --git a/internal/app/sources/aws/eks/config.go b/internal/app/sources/aws/eks/config.go new file mode 100644 index 0000000..f7028ec --- /dev/null +++ b/internal/app/sources/aws/eks/config.go @@ -0,0 +1,9 @@ +package eks + +import "github.com/qonto/upgrade-manager/internal/app/filters" + +type Config struct { + Enabled bool `yaml:"enabled"` + RequestTimeout string `yaml:"request-timeout"` + Filters filters.Config `yaml:"filters"` +} diff --git a/internal/app/sources/aws/eks/source.go b/internal/app/sources/aws/eks/source.go new file mode 100644 index 0000000..02375a7 --- /dev/null +++ b/internal/app/sources/aws/eks/source.go @@ -0,0 +1,178 @@ +package eks + +import ( + "context" + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/service/eks" + "github.com/qonto/upgrade-manager/internal/app/core/software" + "github.com/qonto/upgrade-manager/internal/app/filters" + "github.com/qonto/upgrade-manager/internal/app/semver" + "github.com/qonto/upgrade-manager/internal/infra/aws" + "go.uber.org/zap" +) + +type Source struct { + log *zap.Logger + api aws.EKSApi + cfg *Config + filter filters.Filter +} + +const ( + EksCluster software.SoftwareType = "eks-cluster" + K8s software.SoftwareType = "k8s-engine" + EksAddon software.SoftwareType = "eks-addon" + DefaultTimeout time.Duration = time.Second * 15 +) + +func (s *Source) Name() string { + return "EKS" +} + +func NewSource(api aws.EKSApi, log *zap.Logger, cfg *Config) (*Source, error) { + // Current implementation of filters requires this map to be non-nil to filter old versions + // so we set RemovePreRelease to true to filter out old versions anyway. + // NOTE: this is slightly confusing and should probably be refactored later on + cfg.Filters = filters.Config{ + SemverVersions: &filters.SemverVersionsConfig{ + RemovePreRelease: true, + }, + } + filter := filters.Build(cfg.Filters) + return &Source{ + log: log, + api: api, + cfg: cfg, + filter: filter, + }, nil +} + +func (s *Source) Load() ([]*software.Software, error) { + timeout, err := time.ParseDuration(s.cfg.RequestTimeout) + if err != nil || s.cfg.RequestTimeout == "" { + timeout = DefaultTimeout + } + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + softwares := []*software.Software{} + res, err := s.api.ListClusters(ctx, &eks.ListClustersInput{}) + if err != nil { + s.log.Error(fmt.Sprintf("%s", err)) + return nil, err + } + + for i, clusterName := range res.Clusters { + topLevelSoft := &software.Software{ + Type: EksCluster, + Name: clusterName, + Calculator: software.MetaCalculator, + } + + // get cluster current version + clusterInfo, err := s.api.DescribeCluster(ctx, &eks.DescribeClusterInput{ + Name: &res.Clusters[i], + }) + if err != nil { + return nil, err + } + + // get k8s latest released version + k8sVersion, err := s.getK8sLatestVersion(ctx) + if err != nil { + return nil, err + } + s.log.Debug(fmt.Sprintf("Latest EKS version found for cluster '%s': %s ", clusterName, k8sVersion.Version)) + + topLevelSoft.Dependencies = append(topLevelSoft.Dependencies, &software.Software{ + Type: K8s, + Name: "k8s", + VersionCandidates: []software.Version{*k8sVersion}, + Version: software.Version{Version: *clusterInfo.Cluster.Version}, + Calculator: software.AugmentedSemverCalculator, + }) + + // load cluster addon dependencies + addons, err := s.api.ListAddons(ctx, &eks.ListAddonsInput{ClusterName: &res.Clusters[i]}) + if err != nil { + return nil, err + } + for j, addon := range addons.Addons { + // get currently deployed addon version + currentAddonConfig, err := s.api.DescribeAddon(ctx, &eks.DescribeAddonInput{ + AddonName: &addons.Addons[j], + ClusterName: &res.Clusters[i], + }) + if err != nil { + return nil, err + } + + currentAddonSemver, err := semver.ExtractFromString(*currentAddonConfig.Addon.AddonVersion) + if err != nil { + continue + } + addonDep := software.Software{ + Name: addon, + Calculator: software.SemverCalculator, + Version: software.Version{Version: currentAddonSemver}, + } + + // get all released version for the addon + versions, err := s.getAddonVersions(ctx, addon, *clusterInfo.Cluster.Version) + if err != nil { + return nil, err + } + + // filter candidates + for _, v := range versions { + if keep := s.filter(addonDep.Version, *v); keep { + addonDep.VersionCandidates = append(addonDep.VersionCandidates, *v) + } + } + topLevelSoft.Dependencies = append(topLevelSoft.Dependencies, &addonDep) + } + softwares = append(softwares, topLevelSoft) + s.log.Info(fmt.Sprintf("Tracking software %s, of type %s", topLevelSoft.Name, topLevelSoft.Type)) + } + return softwares, nil +} + +// Returns all available versions given an EKS Addon name +func (s *Source) getAddonVersions(ctx context.Context, name string, clusterVersion string) (software.Versions, error) { + versions := software.Versions{} + res, err := s.api.DescribeAddonVersions(ctx, &eks.DescribeAddonVersionsInput{KubernetesVersion: &clusterVersion}) + if err != nil { + return versions, err + } + for _, item := range res.Addons { + if *item.AddonName == name { + cleanAddonVersion := software.Versions{} + for _, v := range item.AddonVersions { + addonVersion, err := semver.ExtractFromString(*v.AddonVersion) + if err != nil { + return nil, err + } + cleanAddonVersion = append(cleanAddonVersion, &software.Version{ + Version: addonVersion, + }) + } + cleanAddonVersion.Deduplicate() + versions = append(versions, cleanAddonVersion...) + } + } + return versions, err +} + +// Return latest k8s version offered by AWS +func (s *Source) getK8sLatestVersion(ctx context.Context) (*software.Version, error) { + addonDesc, err := s.api.DescribeAddonVersions(ctx, &eks.DescribeAddonVersionsInput{}) + if err != nil { + return nil, err + } + k8sVersion := &software.Version{ + Version: *addonDesc.Addons[0].AddonVersions[0].Compatibilities[0].ClusterVersion, + } + + return k8sVersion, nil +} diff --git a/internal/app/sources/aws/eks/source_test.go b/internal/app/sources/aws/eks/source_test.go new file mode 100644 index 0000000..739de10 --- /dev/null +++ b/internal/app/sources/aws/eks/source_test.go @@ -0,0 +1,136 @@ +package eks + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/eks" + "github.com/aws/aws-sdk-go-v2/service/eks/types" + "github.com/qonto/upgrade-manager/internal/app/core/software" + "github.com/qonto/upgrade-manager/internal/app/filters" + awsInfra "github.com/qonto/upgrade-manager/internal/infra/aws" + "go.uber.org/zap" +) + +func TestLoad(t *testing.T) { + currentEksVersion := "1.23" + latestEksVersion := "1.24" + currentAddonVersion := "1.3.2" + latestAddonVersion := "1.5.0" + cls := []struct { + name string + k8sVersion string + k8sExpectedVersion string + expectedCalculator software.CalculatorType + addons []struct { + name string + version string + versions []string + expectedLatestVersion string + expectedCalculator software.CalculatorType + } + }{ + { + name: "cluster1", + k8sVersion: currentEksVersion, + k8sExpectedVersion: latestEksVersion, + addons: []struct { + name string + version string + versions []string + expectedLatestVersion string + expectedCalculator software.CalculatorType + }{ + { + name: "ebs-csi-driver", + version: fmt.Sprintf("%s-eksbuild.v2", currentAddonVersion), + expectedLatestVersion: latestAddonVersion, + expectedCalculator: software.MetaCalculator, + }, + }, + }, + } + mockApi := new(awsInfra.EksMock) + clusterNames := []string{} + + for _, cluster := range cls { + clusterNames = append(clusterNames, cluster.name) + } + + mockApi.On("ListClusters").Return( + &eks.ListClustersOutput{ + Clusters: clusterNames, + }, + ) + mockApi.On("ListAddons", eks.ListAddonsInput{ClusterName: &cls[0].name}).Return( + &eks.ListAddonsOutput{ + Addons: []string{cls[0].addons[0].name}, + }, + ) + mockApi.On("DescribeAddonVersions").Return( + &eks.DescribeAddonVersionsOutput{ + Addons: []types.AddonInfo{ + { + AddonVersions: []types.AddonVersionInfo{ + { + Compatibilities: []types.Compatibility{{ClusterVersion: &latestEksVersion}}, + AddonVersion: aws.String(fmt.Sprintf("%s-eksbuild.v4", cls[0].addons[0].expectedLatestVersion)), + }, + { + Compatibilities: []types.Compatibility{{ClusterVersion: &latestEksVersion}}, + AddonVersion: aws.String(fmt.Sprintf("%s-eksbuild.v3", cls[0].addons[0].expectedLatestVersion)), + }, + }, + AddonName: aws.String("ebs-csi-driver"), + }, + }, + }) + mockApi.On("DescribeCluster", eks.DescribeClusterInput{Name: &cls[0].name}).Return( + &eks.DescribeClusterOutput{ + Cluster: &types.Cluster{ + Version: &cls[0].k8sVersion, + }, + }, + ) + mockApi.On("DescribeAddon", eks.DescribeAddonInput{ + AddonName: &cls[0].addons[0].name, + ClusterName: &cls[0].name, + }).Return( + &eks.DescribeAddonOutput{ + Addon: &types.Addon{ + AddonName: &cls[0].addons[0].name, + AddonVersion: &cls[0].addons[0].version, + }, + }, + ) + src, err := NewSource(mockApi, zap.NewExample(), &Config{ + Enabled: true, + Filters: filters.Config{ + SemverVersions: &filters.SemverVersionsConfig{ + RemovePreRelease: true, + }, + }, + }) + if err != nil { + t.Fatal(err) + } + softs, err := src.Load() + if err != nil { + t.Fatal(err) + } + if len(softs) != 1 { + t.Error(fmt.Errorf("%s", "unexpected number of softwares")) + } + if softs[0].Calculator != software.MetaCalculator { + t.Errorf("wrong calculator type for soft %s", softs[0].Name) + } + for _, dep := range softs[0].Dependencies { + if dep.Name == "ebs-csi-driver" && dep.Calculator != software.SemverCalculator && dep.VersionCandidates[0].Version != latestAddonVersion { + t.Errorf("wrong result for dep %s", dep.Name) + } + if dep.Name == "k8s" && dep.Calculator != software.AugmentedSemverCalculator && dep.VersionCandidates[0].Version != latestEksVersion { + t.Errorf("wrong result for dep %s", dep.Name) + } + } +} diff --git a/internal/app/sources/aws/elasticache/config.go b/internal/app/sources/aws/elasticache/config.go new file mode 100644 index 0000000..e69bb53 --- /dev/null +++ b/internal/app/sources/aws/elasticache/config.go @@ -0,0 +1,9 @@ +package elasticache + +import "github.com/qonto/upgrade-manager/internal/app/filters" + +type Config struct { + Enabled bool `yaml:"enabled"` + RequestTimeout string `yaml:"request-timeout"` + Filters filters.Config `yaml:"filters"` +} diff --git a/internal/app/sources/aws/elasticache/provider.go b/internal/app/sources/aws/elasticache/provider.go new file mode 100644 index 0000000..287b8e9 --- /dev/null +++ b/internal/app/sources/aws/elasticache/provider.go @@ -0,0 +1,49 @@ +package elasticache + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go-v2/service/elasticache" + "github.com/aws/aws-sdk-go-v2/service/elasticache/types" + "github.com/qonto/upgrade-manager/internal/app/core/software" + "github.com/qonto/upgrade-manager/internal/app/filters" + "github.com/qonto/upgrade-manager/internal/infra/aws" + "go.uber.org/zap" +) + +type VersionProvider struct { + api aws.ElasticacheApi + log *zap.Logger +} + +func NewProvider(log *zap.Logger, api aws.ElasticacheApi) (*VersionProvider, error) { + return &VersionProvider{ + api: api, + log: log, + }, nil +} + +func (vp *VersionProvider) LoadCandidates(soft *software.Software, engine string, filter filters.Filter) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + res, err := vp.api.DescribeCacheEngineVersions(ctx, &elasticache.DescribeCacheEngineVersionsInput{}) + if err != nil { + return err + } + engineVersions := []types.CacheEngineVersion{} + for _, v := range res.CacheEngineVersions { + if *v.Engine == engine { + engineVersions = append(engineVersions, v) + } + } + + for _, version := range engineVersions { + candidate := software.Version{Version: *version.EngineVersion} + keep := filter(soft.Version, candidate) + if keep { + soft.VersionCandidates = append(soft.VersionCandidates, software.Version{Version: *version.EngineVersion}) + } + } + return nil +} diff --git a/internal/app/sources/aws/elasticache/provider_test.go b/internal/app/sources/aws/elasticache/provider_test.go new file mode 100644 index 0000000..8d6078e --- /dev/null +++ b/internal/app/sources/aws/elasticache/provider_test.go @@ -0,0 +1,108 @@ +package elasticache + +import ( + "testing" + + "github.com/qonto/upgrade-manager/internal/app/core/software" + "github.com/qonto/upgrade-manager/internal/app/filters" + "github.com/qonto/upgrade-manager/internal/infra/aws" + "go.uber.org/zap" +) + +func TestLoad(t *testing.T) { + mockApi := new(aws.ElasticacheMock) + testCases := []struct { + app *software.Software + filters filters.Config + softType software.SoftwareType + expectedVersionCandidateCount int + }{ + { + app: &software.Software{ + Version: software.Version{Version: "6.0.0"}, + Type: RedisElasticacheCluster, + }, + filters: filters.Config{ + SemverVersions: &filters.SemverVersionsConfig{ + RemovePreRelease: true, + RemoveFirstMajorVersion: true, + }, + }, + + expectedVersionCandidateCount: 2, + }, + { + app: &software.Software{ + Version: software.Version{Version: "6.0.0"}, + Type: RedisElasticacheCluster, + }, + filters: filters.Config{ + SemverVersions: &filters.SemverVersionsConfig{ + RemovePreRelease: true, + RemoveFirstMajorVersion: false, + }, + }, + expectedVersionCandidateCount: 3, + }, + { + app: &software.Software{ + Version: software.Version{Version: "4.2.4"}, + Type: MemcachedElasticacheCluster, + }, + filters: filters.Config{ + SemverVersions: &filters.SemverVersionsConfig{ + RemovePreRelease: true, + RemoveFirstMajorVersion: true, + }, + }, + expectedVersionCandidateCount: 1, + }, + { + app: &software.Software{ + Version: software.Version{Version: "4.2.4"}, + Type: MemcachedElasticacheCluster, + }, + filters: filters.Config{ + SemverVersions: &filters.SemverVersionsConfig{ + RemovePreRelease: true, + RemoveFirstMajorVersion: false, + }, + }, + expectedVersionCandidateCount: 2, + }, + { + app: &software.Software{ + Version: software.Version{Version: "5.0.1"}, + Type: MemcachedElasticacheCluster, + }, + filters: filters.Config{ + SemverVersions: &filters.SemverVersionsConfig{ + RemovePreRelease: true, + RemoveFirstMajorVersion: false, + }, + }, + expectedVersionCandidateCount: 0, + }, + } + for idx, tc := range testCases { + vp, err := NewProvider(zap.NewExample(), mockApi) + if err != nil { + t.Error(err) + } + var engine string + switch tc.app.Type { + case MemcachedElasticacheCluster: + engine = "memcached" + case RedisElasticacheCluster: + engine = "redis" + } + filter := filters.Build(tc.filters) + err = vp.LoadCandidates(tc.app, engine, filter) + if err != nil { + t.Error(err) + } + if len(tc.app.VersionCandidates) != tc.expectedVersionCandidateCount { + t.Errorf("Case %d: Wrong version candidate count. Expected: %d, got : %d)", idx+1, tc.expectedVersionCandidateCount, len(tc.app.VersionCandidates)) + } + } +} diff --git a/internal/app/sources/aws/elasticache/source.go b/internal/app/sources/aws/elasticache/source.go new file mode 100644 index 0000000..e421050 --- /dev/null +++ b/internal/app/sources/aws/elasticache/source.go @@ -0,0 +1,112 @@ +package elasticache + +import ( + "context" + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/service/elasticache" + "github.com/qonto/upgrade-manager/internal/app/core/software" + "github.com/qonto/upgrade-manager/internal/app/filters" + "github.com/qonto/upgrade-manager/internal/infra/aws" + "go.uber.org/zap" +) + +type Source struct { + log *zap.Logger + api aws.ElasticacheApi + cfg *Config + vp *VersionProvider + filter filters.Filter +} + +const ( + RedisElasticacheCluster software.SoftwareType = "elasticache-redis" + MemcachedElasticacheCluster software.SoftwareType = "elasticache-memcached" + DefaultTimeout time.Duration = time.Second * 15 +) + +func (s *Source) Name() string { + return "elasticache" +} + +func NewSource(api aws.ElasticacheApi, log *zap.Logger, cfg *Config) (*Source, error) { + // Current implementation of filters requires this map to be non-nil to filter old versions + // so we set RemovePreRelease to true to filter out old versions anyway. + // NOTE: this is slightly confusing and should probably be refactored later on + if cfg.Filters.SemverVersions == nil { + cfg.Filters = filters.Config{ + SemverVersions: &filters.SemverVersionsConfig{ + RemovePreRelease: true, + }, + } + } + chartFilter := filters.Build(cfg.Filters) + vp, err := NewProvider(log, api) + if err != nil { + log.Error("Failed to build elasticache version provider") + return &Source{}, err + } + return &Source{ + log: log, + api: api, + cfg: cfg, + vp: vp, + filter: chartFilter, + }, nil +} + +func (s *Source) Load() ([]*software.Software, error) { + softwares := []*software.Software{} + timeout, err := time.ParseDuration(s.cfg.RequestTimeout) + if err != nil || s.cfg.RequestTimeout == "" { + timeout = DefaultTimeout + } + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + res, err := s.api.DescribeCacheClusters(ctx, &elasticache.DescribeCacheClustersInput{}) + if err != nil { + return nil, err + } + processedReplicationGroupId := []string{} + + for _, cluster := range res.CacheClusters { + clusterProcessed := false + for _, id := range processedReplicationGroupId { + // for some reason DescribeCacheClusters return a list of node, so to deduplicate nodes inside a cluster, + // we check if if the replication group was already + if *cluster.ReplicationGroupId == id { + clusterProcessed = true + } + } + if !clusterProcessed { + processedReplicationGroupId = append(processedReplicationGroupId, *cluster.ReplicationGroupId) + + var softType software.SoftwareType + switch *cluster.Engine { + case "redis": + softType = RedisElasticacheCluster + case "memcached": + softType = MemcachedElasticacheCluster + default: + s.log.Error(fmt.Sprintf("unknown elasticache cluster type %s, skipping...", *cluster.Engine)) + continue + } + soft := &software.Software{ + Name: *cluster.ReplicationGroupId, + Type: softType, + Version: software.Version{ + Version: *cluster.EngineVersion, + }, + } + s.log.Info(fmt.Sprintf("Tracking software %s of type %s", *cluster.ReplicationGroupId, softType)) + + err = s.vp.LoadCandidates(soft, *cluster.Engine, s.filter) + if err != nil { + s.log.Warn(fmt.Sprintf("Fail to retrieve versions for software %s: %s", soft.Name, err.Error())) + } + softwares = append(softwares, soft) + } + } + return softwares, nil +} diff --git a/internal/app/sources/aws/elasticache/source_test.go b/internal/app/sources/aws/elasticache/source_test.go new file mode 100644 index 0000000..e8894f0 --- /dev/null +++ b/internal/app/sources/aws/elasticache/source_test.go @@ -0,0 +1,43 @@ +package elasticache + +import ( + "testing" + + "github.com/qonto/upgrade-manager/internal/app/filters" + "github.com/qonto/upgrade-manager/internal/infra/aws" + "go.uber.org/zap" +) + +func TestSourceLoad(t *testing.T) { + mockApi := new(aws.ElasticacheMock) + testCases := []struct { + cfg *Config + expectedSoftwareCount int + }{ + { + cfg: &Config{ + Enabled: true, + Filters: filters.Config{ + SemverVersions: &filters.SemverVersionsConfig{ + RemovePreRelease: true, + RemoveFirstMajorVersion: true, + }, + }, + }, + expectedSoftwareCount: 3, // REMINDER: per-cluster ID deduplication + }, + } + for idx, tc := range testCases { + source, err := NewSource(mockApi, zap.NewExample(), tc.cfg) + if err != nil { + t.Error(err) + } + softwares, err := source.Load() + if err != nil { + t.Error(err) + } + if len(softwares) != tc.expectedSoftwareCount { + t.Errorf("Case %d: Wrong software count. Expected: %d, got : %d", idx+1, tc.expectedSoftwareCount, len(softwares)) + } + } +} diff --git a/internal/app/sources/aws/lambda/config.go b/internal/app/sources/aws/lambda/config.go new file mode 100644 index 0000000..b7582c7 --- /dev/null +++ b/internal/app/sources/aws/lambda/config.go @@ -0,0 +1,12 @@ +package lambda + +import ( + "github.com/aws/aws-sdk-go-v2/service/lambda/types" +) + +type Config struct { + Enabled bool `yaml:"enabled"` + RequestTimeout string `yaml:"request-timeout"` + DeprecatedRuntimes []types.Runtime `yaml:"deprecated-runtimes"` + DeprecatedRuntimesScore int `yaml:"deprecated-runtimes-score"` +} diff --git a/internal/app/sources/aws/lambda/source.go b/internal/app/sources/aws/lambda/source.go new file mode 100644 index 0000000..a45c9b9 --- /dev/null +++ b/internal/app/sources/aws/lambda/source.go @@ -0,0 +1,121 @@ +package lambda + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go-v2/service/lambda" + "github.com/aws/aws-sdk-go-v2/service/lambda/types" + "github.com/qonto/upgrade-manager/internal/app/core/software" + "github.com/qonto/upgrade-manager/internal/infra/aws" + "go.uber.org/zap" +) + +type Source struct { + log *zap.Logger + api aws.LambdaApi + cfg *Config +} + +const ( + LambdaFunction software.SoftwareType = "lambda" + DefaultTimeout time.Duration = time.Second * 15 + DefaultDeprecatedRuntimesScore int = 100 +) + +func (s *Source) Name() string { + return "lambda" +} + +func NewSource(api aws.LambdaApi, log *zap.Logger, cfg *Config) (*Source, error) { + return &Source{ + api: api, + log: log, + cfg: cfg, + }, nil +} + +func (s *Source) Load() ([]*software.Software, error) { + var softwares []*software.Software + timeout, err := time.ParseDuration(s.cfg.RequestTimeout) + if err != nil || s.cfg.RequestTimeout == "" { + timeout = DefaultTimeout + } + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + rtVersionProvider := aws.NewLambdaRuntimeFamilyVersionProvider() + var deprecatedRuntimes []types.Runtime + if s.cfg.DeprecatedRuntimes != nil { + deprecatedRuntimes = s.cfg.DeprecatedRuntimes + } else { + deprecatedRuntimes = aws.GetDefaultDeprecatedRuntimesList() + } + + res, err := s.api.ListFunctions(ctx, &lambda.ListFunctionsInput{}) + if err != nil { + return nil, err + } + for _, f := range res.Functions { + soft := &software.Software{ + Name: *f.FunctionName, + Version: *software.ToVersion(string(f.Runtime)), + Type: LambdaFunction, + } + + if !aws.IsLambdaRuntime(f.Runtime) { + continue + } + + // set arbitrary score if the runtime is deprecated + for _, deprecrated := range deprecatedRuntimes { + if f.Runtime == deprecrated { + soft.Calculator = software.SkipCalculator + if s.cfg.DeprecatedRuntimesScore == 0 { + soft.CalculatedScore = DefaultDeprecatedRuntimesScore + } else { + soft.CalculatedScore = s.cfg.DeprecatedRuntimesScore + } + } + } + + if soft.CalculatedScore == 0 { + soft.Calculator = software.CandidateCountCalculator + } + + // load version candidates + familyRuntimes := rtVersionProvider(f.Runtime) + var found bool + for i := range familyRuntimes { + found = false + if familyRuntimes[i] == f.Runtime { + found = true + candidates := familyRuntimes[i+1:] + for _, rt := range candidates { + soft.VersionCandidates = append(soft.VersionCandidates, *software.ToVersion(string(rt))) + } + break + } + } + // If we did not find the function's runtime version in the list of supported versions, + // then all the runtime versions in the runtime family are candidates + if !found { + for _, rt := range familyRuntimes { + soft.VersionCandidates = append(soft.VersionCandidates, *software.ToVersion(string(rt))) + } + } + + // Reverse the slice to provide the most recent candidate as a first element (aws lambda version does not follow semver or another stable versioning logic) + // ex: + // python family: "python2.7", "python3.6", "python3.7" etc... + // nodejs family: "nodejs12.x", "nodejs", "nodejs4.3-edge", etc... + // java family: "java8", "java11", "java.al2", etc... + + for i, j := 0, len(soft.VersionCandidates)-1; i < j; i, j = i+1, j-1 { + soft.VersionCandidates[i], soft.VersionCandidates[j] = soft.VersionCandidates[j], soft.VersionCandidates[i] + } + + s.log.Info("Tracking software", zap.String("software", soft.Name), zap.String("software_type", string(soft.Type))) + softwares = append(softwares, soft) + } + return softwares, nil +} diff --git a/internal/app/sources/aws/lambda/source_test.go b/internal/app/sources/aws/lambda/source_test.go new file mode 100644 index 0000000..3efa334 --- /dev/null +++ b/internal/app/sources/aws/lambda/source_test.go @@ -0,0 +1,118 @@ +package lambda + +import ( + "errors" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/lambda" + "github.com/aws/aws-sdk-go-v2/service/lambda/types" + "github.com/qonto/upgrade-manager/internal/app/core/software" + "github.com/qonto/upgrade-manager/internal/app/sources/utils" + "github.com/qonto/upgrade-manager/internal/infra/aws" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "go.uber.org/zap" +) + +func TestLoad(t *testing.T) { + defaultBuildMock := func(mockApi *aws.LambdaMock) { + mockApi.On("ListFunctions", mock.Anything).Return( + &lambda.ListFunctionsOutput{ + Functions: []types.FunctionConfiguration{ + { + FunctionName: utils.Ptr("function1 - up to date"), + Runtime: types.RuntimeJava17, + }, + { + FunctionName: utils.Ptr("function2 - not deprecated but 2 versions late"), + Runtime: types.RuntimePython37, + }, + { + FunctionName: utils.Ptr("function3 - deprecated"), + Runtime: types.RuntimeNodejs12x, + }, + { + FunctionName: utils.Ptr("function4 - NotaRealRuntime"), + Runtime: "unknown runtime", + }, + }, + }, nil, + ) + } + testCases := []struct { + Description string + Config *Config + BuildMock func(mockApi *aws.LambdaMock) + ValidateResult func(t *testing.T, result []*software.Software, resultErr error) + }{ + { + Description: "default deprecrated runtimes list", + Config: &Config{ + Enabled: true, + DeprecatedRuntimesScore: DefaultDeprecatedRuntimesScore, + }, + ValidateResult: func(t *testing.T, result []*software.Software, resultErr error) { + t.Helper() + assert.NoError(t, resultErr) + assert.Len(t, result, 3) + assert.Equal(t, software.CandidateCountCalculator, result[0].Calculator) + assert.Empty(t, result[0].VersionCandidates) + assert.Equal(t, software.CandidateCountCalculator, result[1].Calculator) + assert.Len(t, result[1].VersionCandidates, 3) + assert.Equal(t, software.SkipCalculator, result[2].Calculator) + assert.Equal(t, DefaultDeprecatedRuntimesScore, result[2].CalculatedScore) + }, + }, + { + Description: "custom deprecated runtimes list", + Config: &Config{ + Enabled: true, + DeprecatedRuntimes: append(aws.GetDefaultDeprecatedRuntimesList(), types.RuntimePython37), + DeprecatedRuntimesScore: 150, + }, + ValidateResult: func(t *testing.T, result []*software.Software, resultErr error) { + t.Helper() + assert.NoError(t, resultErr) + assert.Len(t, result, 3) + assert.Equal(t, software.CandidateCountCalculator, result[0].Calculator) + assert.Empty(t, result[0].VersionCandidates) + assert.Equal(t, software.SkipCalculator, result[1].Calculator) + assert.Equal(t, 150, result[2].CalculatedScore) + assert.Equal(t, software.SkipCalculator, result[2].Calculator) + assert.Equal(t, 150, result[2].CalculatedScore) + }, + }, + { + Description: "failed call to ListFunction", + Config: &Config{ + Enabled: true, + DeprecatedRuntimes: append(aws.GetDefaultDeprecatedRuntimesList(), types.RuntimePython38), + }, + BuildMock: func(mockApi *aws.LambdaMock) { + mockApi.On("ListFunctions", mock.Anything).Return( + &lambda.ListFunctionsOutput{}, errors.New("failed to list functions"), + ) + }, + ValidateResult: func(t *testing.T, result []*software.Software, resultErr error) { + t.Helper() + assert.Error(t, resultErr) + assert.Empty(t, result) + }, + }, + } + for i := range testCases { + t.Run(testCases[i].Description, func(t *testing.T) { + mockApi := new(aws.LambdaMock) + if testCases[i].BuildMock == nil { + testCases[i].BuildMock = defaultBuildMock + } + testCases[i].BuildMock(mockApi) + source, err := NewSource(mockApi, zap.NewExample(), testCases[i].Config) + + assert.NoError(t, err) + softwares, err := source.Load() + testCases[i].ValidateResult(t, softwares, err) + assert.NotEmpty(t, source.Name()) + }) + } +} diff --git a/internal/app/sources/aws/msk/config.go b/internal/app/sources/aws/msk/config.go new file mode 100644 index 0000000..4d200b5 --- /dev/null +++ b/internal/app/sources/aws/msk/config.go @@ -0,0 +1,9 @@ +package msk + +import "github.com/qonto/upgrade-manager/internal/app/filters" + +type Config struct { + Enabled bool `yaml:"enabled"` + RequestTimeout string `yaml:"request-timeout"` + Filters filters.Config `yaml:"filters"` +} diff --git a/internal/app/sources/aws/msk/source.go b/internal/app/sources/aws/msk/source.go new file mode 100644 index 0000000..e759d0e --- /dev/null +++ b/internal/app/sources/aws/msk/source.go @@ -0,0 +1,75 @@ +package msk + +import ( + "context" + "strings" + "time" + + "github.com/aws/aws-sdk-go-v2/service/kafka" + "github.com/qonto/upgrade-manager/internal/app/core/software" + "github.com/qonto/upgrade-manager/internal/app/filters" + "github.com/qonto/upgrade-manager/internal/infra/aws" + "go.uber.org/zap" +) + +type Source struct { + api aws.MSKApi + log *zap.Logger + cfg *Config + filter filters.Filter +} + +const ( + MskCluster software.SoftwareType = "msk cluster" + DefaultTimeout time.Duration = time.Second * 15 +) + +func (s *Source) Name() string { + return "MSK" +} + +func NewSource(api aws.MSKApi, log *zap.Logger, cfg *Config) (*Source, error) { + cfg.Filters = filters.Config{ + SemverVersions: &filters.SemverVersionsConfig{ + RemovePreRelease: true, + }, + } + filter := filters.Build(cfg.Filters) + return &Source{ + log: log, + api: api, + cfg: cfg, + filter: filter, + }, nil +} + +func (s *Source) Load() ([]*software.Software, error) { + softwares := []*software.Software{} + res, err := s.api.ListClustersV2(context.TODO(), &kafka.ListClustersV2Input{}) + if err != nil { + return nil, err + } + for _, cluster := range res.ClusterInfoList { + res, err := s.api.GetCompatibleKafkaVersions(context.TODO(), &kafka.GetCompatibleKafkaVersionsInput{ + ClusterArn: cluster.ClusterArn, + }) + if err != nil { + return nil, err + } + versionCandidates := []software.Version{} + for _, v := range res.CompatibleKafkaVersions[0].TargetVersions { + versionCandidate := strings.ReplaceAll(v, ".tiered", "") + versionCandidates = append(versionCandidates, software.Version{Version: versionCandidate}) + } + s := &software.Software{ + Calculator: software.SemverCalculator, + Name: *cluster.ClusterName, + Type: MskCluster, + Version: software.Version{Version: *cluster.Provisioned.CurrentBrokerSoftwareInfo.KafkaVersion}, + VersionCandidates: versionCandidates, + } + softwares = append(softwares, s) + } + + return softwares, nil +} diff --git a/internal/app/sources/aws/msk/source_test.go b/internal/app/sources/aws/msk/source_test.go new file mode 100644 index 0000000..78b9d99 --- /dev/null +++ b/internal/app/sources/aws/msk/source_test.go @@ -0,0 +1,49 @@ +package msk + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/service/kafka" + "github.com/aws/aws-sdk-go-v2/service/kafka/types" + "github.com/qonto/upgrade-manager/internal/app/sources/utils" + "github.com/qonto/upgrade-manager/internal/infra/aws" + "github.com/stretchr/testify/mock" + "go.uber.org/zap" +) + +func TestLoad(t *testing.T) { + api := new(aws.MockMSKApi) + api.On("ListClustersV2", mock.Anything).Return( + &kafka.ListClustersV2Output{ + ClusterInfoList: []types.Cluster{ + { + ClusterName: utils.Ptr("mycluster"), + ClusterArn: utils.Ptr("arn:myclusterarn"), + Provisioned: &types.Provisioned{ + CurrentBrokerSoftwareInfo: &types.BrokerSoftwareInfo{ + KafkaVersion: utils.Ptr("2.0.0"), + }, + }, + }, + }, + }) + api.On("GetCompatibleKafkaVersions", mock.Anything).Return( + &kafka.GetCompatibleKafkaVersionsOutput{ + CompatibleKafkaVersions: []types.CompatibleKafkaVersion{ + { + TargetVersions: []string{ + "2.2.3", + "2.3.4.tiered", + }, + }, + }, + }) + source, err := NewSource(api, zap.NewExample(), &Config{}) + if err != nil { + t.Error(err) + } + _, err = source.Load() + if err != nil { + t.Error(err) + } +} diff --git a/internal/app/sources/aws/rds/config.go b/internal/app/sources/aws/rds/config.go new file mode 100644 index 0000000..00554a3 --- /dev/null +++ b/internal/app/sources/aws/rds/config.go @@ -0,0 +1,10 @@ +package rds + +import "github.com/qonto/upgrade-manager/internal/app/filters" + +type Config struct { + Enabled bool `yaml:"enabled"` + AggregationLevel string `yaml:"aggregation-level"` // cluster or instance + RequestTimeout string `yaml:"request-timeout"` + Filters filters.Config `yaml:"filters"` +} diff --git a/internal/app/sources/aws/rds/source.go b/internal/app/sources/aws/rds/source.go new file mode 100644 index 0000000..0c7c626 --- /dev/null +++ b/internal/app/sources/aws/rds/source.go @@ -0,0 +1,123 @@ +package rds + +import ( + "context" + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/service/rds" + "github.com/qonto/upgrade-manager/internal/app/core/software" + "github.com/qonto/upgrade-manager/internal/app/filters" + "github.com/qonto/upgrade-manager/internal/infra/aws" + "go.uber.org/zap" +) + +type Source struct { + api aws.RDSApi + log *zap.Logger + cfg *Config + filter filters.Filter + engineToSoftTypeMapping map[string]software.SoftwareType +} + +const ( + RdsPostgreSQL software.SoftwareType = "rds-postgresql" + RdsAuroraPostgreSQL software.SoftwareType = "rds-aurora-postgresql" + RdsAuroraMySQL software.SoftwareType = "rds-aurora-mysql" + RdsMariaDB software.SoftwareType = "rds-mariadb" + RdsDocDB software.SoftwareType = "rds-docdb" + RdsMySQL software.SoftwareType = "rds-mysql" + RdsOracle software.SoftwareType = "rds-oracle-ee" + RdsNeptune software.SoftwareType = "rds-neptune" + DefaultTimeout time.Duration = time.Second * 15 +) + +func (s *Source) Name() string { + return "RDS" +} + +func NewSource(api aws.RDSApi, log *zap.Logger, cfg *Config) (*Source, error) { + mapping := map[string]software.SoftwareType{ + "aurora-mysql": RdsAuroraMySQL, + "aurora-postgresql": RdsAuroraPostgreSQL, + "docdb": RdsDocDB, + "mariadb": RdsMariaDB, + "mysql": RdsMySQL, + "neptune": RdsNeptune, + "oracle-ee": RdsOracle, + "postgres": RdsPostgreSQL, + } + // Current implementation of filters requires this map to be non-nil to filter old versions + // so we set RemovePreRelease to true to filter out old versions anyway. + // NOTE: this is slightly confusing and should probably be refactored later on + cfg.Filters = filters.Config{ + SemverVersions: &filters.SemverVersionsConfig{ + RemovePreRelease: true, + }, + } + filter := filters.Build(cfg.Filters) + return &Source{ + log: log, + api: api, + cfg: cfg, + filter: filter, + engineToSoftTypeMapping: mapping, + }, nil +} + +func (s *Source) Load() ([]*software.Software, error) { + timeout, err := time.ParseDuration(s.cfg.RequestTimeout) + if err != nil || s.cfg.RequestTimeout == "" { + timeout = DefaultTimeout + } + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + var softwares []*software.Software + pgEngineName := "postgres" + defaultEngineVersions, err := s.api.DescribeDBEngineVersions(ctx, &rds.DescribeDBEngineVersionsInput{}) + if err != nil { + return nil, err + } + // The below second api call is required. DescribeDBEngineVersions does not return postgres versions by default (only aurora-postgresql) + pgEngineVersions, err := s.api.DescribeDBEngineVersions(ctx, &rds.DescribeDBEngineVersionsInput{Engine: &pgEngineName}) + if err != nil { + return nil, err + } + defaultEngineVersions.DBEngineVersions = append(defaultEngineVersions.DBEngineVersions, pgEngineVersions.DBEngineVersions...) + versionRegistry := make(map[string][]software.Version) + for _, ev := range defaultEngineVersions.DBEngineVersions { + versionRegistry[*ev.Engine] = append(versionRegistry[*ev.Engine], software.Version{Version: *ev.EngineVersion}) + } + // Get DB instances info + res, err := s.api.DescribeDBInstances(context.TODO(), &rds.DescribeDBInstancesInput{}) + if err != nil { + return nil, err + } + for _, cluster := range res.DBInstances { + // if aggregation level is clsuter, add the instance only if it is marked as a replica with a source db + if s.cfg.AggregationLevel == "cluster" { + if cluster.ReadReplicaSourceDBInstanceIdentifier != nil { + continue + } + } + // Create software for the DB instance + cv := software.Version{Version: *cluster.EngineVersion} + candidates := versionRegistry[*cluster.Engine] + var versionCandidates []software.Version + for _, v := range candidates { + if keep := s.filter(cv, v); keep { + versionCandidates = append(versionCandidates, v) + } + } + softwares = append(softwares, &software.Software{ + Name: *cluster.DBInstanceIdentifier, + Calculator: software.SemverCalculator, + Type: s.engineToSoftTypeMapping[*cluster.Engine], + Version: software.Version{Version: *cluster.EngineVersion}, + VersionCandidates: versionCandidates, + }) + s.log.Info(fmt.Sprintf("Tracking software %s, of type %s", *cluster.DBInstanceIdentifier, s.engineToSoftTypeMapping[*cluster.Engine])) + } + + return softwares, nil +} diff --git a/internal/app/sources/aws/rds/source_test.go b/internal/app/sources/aws/rds/source_test.go new file mode 100644 index 0000000..60de5de --- /dev/null +++ b/internal/app/sources/aws/rds/source_test.go @@ -0,0 +1,71 @@ +package rds + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/service/rds" + "github.com/aws/aws-sdk-go-v2/service/rds/types" + "github.com/qonto/upgrade-manager/internal/infra/aws" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" +) + +func TestLoad(t *testing.T) { + dbInfo := []struct { + dbId string + dbEngine string + dbVersion string + }{{"pg1", "postgres", "13.3"}, {"pg1-replica", "postgres", "13.3"}, {"pg2", "mysql", "8.0.23"}} + dbVersions := map[string][]string{ + "postgres": {"15.2", "14.5", "11.1"}, + "mysql": {"8.0.28", "8.0.27", "5.7.11"}, + } + api := new(aws.MockRDSApi) + api.On("DescribeDBInstances").Return(&rds.DescribeDBInstancesOutput{ + DBInstances: []types.DBInstance{ + {DBInstanceIdentifier: &dbInfo[0].dbId, Engine: &dbInfo[0].dbEngine, EngineVersion: &dbInfo[0].dbVersion}, + {DBInstanceIdentifier: &dbInfo[1].dbId, Engine: &dbInfo[1].dbEngine, EngineVersion: &dbInfo[0].dbVersion, ReadReplicaSourceDBInstanceIdentifier: &dbInfo[0].dbId}, + {DBInstanceIdentifier: &dbInfo[2].dbId, Engine: &dbInfo[2].dbEngine, EngineVersion: &dbInfo[0].dbVersion}, + }, + }) + api.On("DescribeDBEngineVersions").Return(&rds.DescribeDBEngineVersionsOutput{ + DBEngineVersions: []types.DBEngineVersion{ + {Engine: &dbInfo[0].dbEngine, EngineVersion: &dbVersions[dbInfo[0].dbEngine][0]}, + {Engine: &dbInfo[0].dbEngine, EngineVersion: &dbVersions[dbInfo[0].dbEngine][1]}, + {Engine: &dbInfo[0].dbEngine, EngineVersion: &dbVersions[dbInfo[0].dbEngine][2]}, + {Engine: &dbInfo[2].dbEngine, EngineVersion: &dbVersions[dbInfo[2].dbEngine][0]}, + {Engine: &dbInfo[2].dbEngine, EngineVersion: &dbVersions[dbInfo[2].dbEngine][1]}, + {Engine: &dbInfo[2].dbEngine, EngineVersion: &dbVersions[dbInfo[2].dbEngine][2]}, + }, + }, + ) + + tcases := []struct { + cfg Config + expectedSoftwareCount int + }{ + { + cfg: Config{ + AggregationLevel: "cluster", + }, + expectedSoftwareCount: 2, + }, + { + cfg: Config{ + AggregationLevel: "instance", + }, + expectedSoftwareCount: 3, + }, + } + for _, tc := range tcases { + src, err := NewSource(api, zap.NewExample(), &tc.cfg) + if err != nil { + t.Error(err) + } + softwares, err := src.Load() + if err != nil { + t.Error(err) + } + assert.Equal(t, tc.expectedSoftwareCount, len(softwares)) + } +} diff --git a/internal/app/sources/deployments/config.go b/internal/app/sources/deployments/config.go new file mode 100644 index 0000000..bfe50cc --- /dev/null +++ b/internal/app/sources/deployments/config.go @@ -0,0 +1,14 @@ +package deployments + +import ( + "github.com/qonto/upgrade-manager/internal/app/filters" + "github.com/qonto/upgrade-manager/internal/infra/registry" +) + +type Config struct { + Name string `yaml:"name"` + Namespace string `yaml:"namespace"` + LabelSelector map[string]string `yaml:"label-selector"` + Registries map[string]registry.Config `yaml:"registries"` + Filters filters.Config `yaml:"filters"` +} diff --git a/internal/app/sources/deployments/source.go b/internal/app/sources/deployments/source.go new file mode 100644 index 0000000..46f1380 --- /dev/null +++ b/internal/app/sources/deployments/source.go @@ -0,0 +1,179 @@ +package deployments + +import ( + "context" + "fmt" + "strings" + "time" + + goversion "github.com/hashicorp/go-version" + "github.com/qonto/upgrade-manager/internal/app/core/software" + soft "github.com/qonto/upgrade-manager/internal/app/core/software" + "github.com/qonto/upgrade-manager/internal/app/filters" + "github.com/qonto/upgrade-manager/internal/infra/kubernetes" + "github.com/qonto/upgrade-manager/internal/infra/registry" + "go.uber.org/zap" +) + +const ( + Deployments soft.SoftwareType = "kubernetes-deployment" + + calculatorAnnotation = "upgrade-manager.qonto.com/calculator" +) + +type Source struct { + k8sClient kubernetes.KubernetesClient + defaultRegistryClient *registry.Client + registryClients map[string]*registry.Client + log *zap.Logger + cfg Config + filter filters.Filter +} + +// TODO: fn on pointers +func (s *Source) Name() string { + return "deployments" +} + +func NewSource(log *zap.Logger, k8sClient kubernetes.KubernetesClient, cfg Config) (*Source, error) { + filter := filters.Build(cfg.Filters) + s := &Source{ + log: log, + cfg: cfg, + k8sClient: k8sClient, + filter: filter, + } + registryClients := make(map[string]*registry.Client) + defaultRegistryClient, err := registry.New(®istry.Config{}) + if err != nil { + return nil, err + } + s.defaultRegistryClient = defaultRegistryClient + + for registryName := range cfg.Registries { + registryConfig := cfg.Registries[registryName] + registryClient, err := registry.New(®istryConfig) + if err != nil { + return nil, err + } + registryClients[registryName] = registryClient + } + s.registryClients = registryClients + return s, nil +} + +type image struct { + Repository string + Version string + Registry string +} + +func buildImage(s string) (image, error) { + dotSplitted := strings.Split(s, ":") + if len(dotSplitted) < 2 { + return image{}, fmt.Errorf("Invalid container image %s", s) + } + + return image{ + Repository: dotSplitted[0], + Version: dotSplitted[1], + Registry: strings.Split(s, "/")[0], + }, nil +} + +func (s *Source) Load() ([]*soft.Software, error) { + var softwares []*soft.Software + ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) + defer cancel() + + deployments, err := s.k8sClient.ListDeployments(ctx, kubernetes.ListRequest{Namespace: s.cfg.Namespace, LabelSelector: s.cfg.LabelSelector}) + if err != nil { + return nil, err + } + s.log.Info(fmt.Sprintf("Found %d deployments", len(deployments.Items))) + for _, deployment := range deployments.Items { + name := deployment.Name + containers := deployment.Spec.Template.Spec.Containers + + calculatorAnnotation, ok := deployment.Annotations[calculatorAnnotation] + calculator := software.SemverCalculator + if ok { + calculator, err = soft.ToCalculator(calculatorAnnotation) + if err != nil { + s.log.Error(fmt.Sprintf("Invalid calculator for deployment %s: %s", name, err.Error())) + continue + } + } + dependencies := []*soft.Software{} + for _, container := range containers { + image, err := buildImage(container.Image) + if err != nil { + s.log.Error(err.Error()) + continue + } + registryClient, ok := s.registryClients[image.Registry] + if !ok { + s.log.Debug(fmt.Sprintf("Using default registry client for image %s", image)) + registryClient = s.defaultRegistryClient + } + tags, err := registryClient.Tags(image.Repository) + if err != nil { + s.log.Error(fmt.Sprintf("Fail to retrieve tags for repository %s: %s", image.Repository, err.Error())) + continue + } + containerSoftware := &soft.Software{ + Calculator: calculator, + Name: fmt.Sprintf("%s-%s", name, container.Name), + Version: soft.Version{Version: image.Version}, + Type: Deployments, + } + if registryClient.ReleaseDateRetrievalEnabled() { + configFile, err := registryClient.ConfigFile(container.Image) + if err != nil { + s.log.Error(fmt.Sprintf("Fail to retrieve release date for image %s: %s", container.Image, err.Error())) + continue + } + containerSoftware.Version.ReleaseDate = configFile.Created.Time + } + versionCandidates := []soft.Version{} + for _, tag := range tags { + if calculator == software.SemverCalculator { + _, err := goversion.NewSemver(tag) + if err != nil { + s.log.Debug(fmt.Sprintf("Skipping non-semver version %s for image %s", tag, image.Repository)) + continue + } + } + versionCandidate := soft.Version{Version: tag} + // optimization to filter semver versions before retrieving creation date + keep := s.filter(containerSoftware.Version, versionCandidate) + if !keep { + continue + } + if registryClient.ReleaseDateRetrievalEnabled() { + depImage := fmt.Sprintf("%s:%s", image.Repository, tag) + configFile, err := registryClient.ConfigFile(depImage) + if err != nil { + s.log.Error(fmt.Sprintf("Fail to retrieve release date for image %s: %s", depImage, err.Error())) + continue + } + versionCandidate.ReleaseDate = configFile.Created.Time + } + keep = s.filter(containerSoftware.Version, versionCandidate) + if keep { + versionCandidates = append(versionCandidates, versionCandidate) + } + } + s.log.Debug(fmt.Sprintf("Found %d version candidates for repository %s", len(versionCandidates), image.Repository)) + containerSoftware.VersionCandidates = versionCandidates + dependencies = append(dependencies, containerSoftware) + } + softwares = append(softwares, &soft.Software{ + Calculator: calculator, + Name: name, + Type: Deployments, + Dependencies: dependencies, + }) + } + return softwares, nil +} diff --git a/internal/app/sources/deployments/source_test.go b/internal/app/sources/deployments/source_test.go new file mode 100644 index 0000000..b23db28 --- /dev/null +++ b/internal/app/sources/deployments/source_test.go @@ -0,0 +1,67 @@ +package deployments + +import ( + "testing" + + "github.com/qonto/upgrade-manager/internal/app/core/software" + "github.com/qonto/upgrade-manager/internal/infra/kubernetes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "go.uber.org/zap" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func createDeployment(image string, deploymentName string, containerName string, calculator software.CalculatorType) appsv1.Deployment { + return appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: deploymentName, + Annotations: map[string]string{ + calculatorAnnotation: string(calculator), + }, + }, + Spec: appsv1.DeploymentSpec{ + Template: v1.PodTemplateSpec{ + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: containerName, + Image: image, + }, + }, + }, + }, + }, + } +} + +// TODO: mock registry client +// for now it is performing actual calls to docker.io when running go test +func TestLoad(t *testing.T) { + k8sMock := new(kubernetes.KubernetesClientMock) + k8sMock.On("ListDeployments", mock.Anything).Return( + &appsv1.DeploymentList{ + Items: []appsv1.Deployment{ + createDeployment("falcosecurity/falcosidekick:2.24.0", "sidekiq", "falcosidekick", software.ReleaseDateCalculator), + }, + }, + nil) + source, err := NewSource(zap.NewExample(), k8sMock, Config{}) + assert.NoError(t, err) + softwares, err := source.Load() + assert.NoError(t, err) + assert.Equal(t, len(softwares), 1) + assert.Equal(t, len(softwares[0].Dependencies), 1) + assert.Equal(t, softwares[0].Dependencies[0].Name, "sidekiq-falcosidekick") + assert.True(t, len(softwares[0].Dependencies[0].VersionCandidates) > 0) + assert.True(t, softwares[0].Dependencies[0].VersionCandidates[0].ReleaseDate.IsZero()) + found := false + for _, version := range softwares[0].Dependencies[0].VersionCandidates { + if version.Version == "2.26.0" { + found = true + break + } + } + assert.True(t, found, "Release 2.26.0 was not found") +} diff --git a/internal/app/sources/filesystemhelm/config.go b/internal/app/sources/filesystemhelm/config.go new file mode 100644 index 0000000..aae6821 --- /dev/null +++ b/internal/app/sources/filesystemhelm/config.go @@ -0,0 +1,11 @@ +package filesystemhelm + +import ( + "github.com/qonto/upgrade-manager/internal/app/filters" +) + +type Config struct { + Enabled bool `yaml:"enabled"` + Paths []string `yaml:"paths" validate:"dive,file"` + Filters filters.Config `yaml:"filters"` +} diff --git a/internal/app/sources/filesystemhelm/source.go b/internal/app/sources/filesystemhelm/source.go new file mode 100644 index 0000000..c049a41 --- /dev/null +++ b/internal/app/sources/filesystemhelm/source.go @@ -0,0 +1,75 @@ +package filesystemhelm + +import ( + "fmt" + + soft "github.com/qonto/upgrade-manager/internal/app/core/software" + "github.com/qonto/upgrade-manager/internal/app/filters" + "github.com/qonto/upgrade-manager/internal/app/sources/helm/versions" + "github.com/qonto/upgrade-manager/internal/infra/aws" + "go.uber.org/zap" + "helm.sh/helm/v3/pkg/chart" +) + +const FileSystemHelm soft.SoftwareType = "filesystemHelm" + +type Source struct { + Charts []chart.Chart + log *zap.Logger + cfg Config + s3Api aws.S3Api + filter filters.Filter +} + +func NewSource(cfg Config, log *zap.Logger, s3Api aws.S3Api) (*Source, error) { + if cfg.Filters.SemverVersions == nil { + cfg.Filters.SemverVersions = &filters.SemverVersionsConfig{} + } + chartFilter := filters.Build(cfg.Filters) + + s := Source{ + log: log, + cfg: cfg, + s3Api: s3Api, + filter: chartFilter, + } + // is the source initialized properly + if cfg.Enabled { + for _, chartFilePath := range cfg.Paths { + // Move in the Load() function + c, err := versions.LoadChartFile(chartFilePath) + if err != nil { + return &s, err + } + s.Charts = append(s.Charts, c) + } + } + return &s, nil +} + +func (s *Source) Load() ([]*soft.Software, error) { + softwares := make([]*soft.Software, 0, len(s.Charts)) + // for chart.yaml we found + for i := range s.Charts { + chart := s.Charts[i] + topLevelSoftware := &soft.Software{ + Name: chart.Name(), + Version: soft.Version{ + Version: versions.CleanVersion(chart.Metadata.Version), + }, + Type: FileSystemHelm, + } + + err := versions.PopulateSoftwareDependencies(s.s3Api, s.log, topLevelSoftware, &chart, FileSystemHelm, s.filter) + if err != nil { + s.log.Error(fmt.Sprintf("Could not load %s chart as software, error: %s", chart.Name(), err)) + continue + } + softwares = append(softwares, topLevelSoftware) + } + return softwares, nil +} + +func (s *Source) Name() string { + return "fshelm" +} diff --git a/internal/app/sources/filesystemhelm/source_test.go b/internal/app/sources/filesystemhelm/source_test.go new file mode 100644 index 0000000..c1435fb --- /dev/null +++ b/internal/app/sources/filesystemhelm/source_test.go @@ -0,0 +1,48 @@ +package filesystemhelm + +import ( + "fmt" + "os" + "testing" + + "github.com/qonto/upgrade-manager/internal/infra/aws" + "github.com/stretchr/testify/mock" + "go.uber.org/zap" +) + +func TestLoadSoftware(t *testing.T) { + testChartsDir := "../../_testdata" + expectedChartCount := 4 + cfg := Config{ + Enabled: true, + Paths: []string{ + testChartsDir + "/test_chart/Chart.yaml", + testChartsDir + "/test_chart2/Chart.yaml", + testChartsDir + "/test_chart3/Chart.yaml", + testChartsDir + "/test_chart4/Chart.yaml", + }, + } + indexFilePath := fmt.Sprintf("%s/index.yaml", testChartsDir) + indexFile, err := os.ReadFile(indexFilePath) + if err != nil { + t.Fatal(err) + } + + s3mock := new(aws.S3Mock) + s3mock.On("GetObject", mock.Anything).Return(indexFile, nil) + + s, err := NewSource(cfg, zap.NewExample(), s3mock) + if err != nil { + t.Fatalf("Error with NewSource(): %s", err) + } + if len(s.Charts) != expectedChartCount { + t.Fatalf("Wrong chart count returned from NewSource(). Expected : %d, got: %d", expectedChartCount, len(s.Charts)) + } + softwares, err := s.Load() + if err != nil { + t.Fatalf("Error with Source.Load(): %s", err) + } + if len(softwares) != expectedChartCount { + t.Fatalf("Wrong software count returned from Source.Load(). Expected : %d, got: %d", expectedChartCount, len(s.Charts)) + } +} diff --git a/internal/app/sources/helm/versions/chart.go b/internal/app/sources/helm/versions/chart.go new file mode 100644 index 0000000..ee08aba --- /dev/null +++ b/internal/app/sources/helm/versions/chart.go @@ -0,0 +1,21 @@ +package versions + +import ( + "os" + + "gopkg.in/yaml.v2" + "helm.sh/helm/v3/pkg/chart" +) + +// Load helm Chart.yaml file from local filesystem into a chart.Chart struct +func LoadChartFile(filePath string) (chart.Chart, error) { + f, err := os.ReadFile(filePath) + if err != nil { + return chart.Chart{}, err + } + var metadata chart.Metadata + if err := yaml.Unmarshal(f, &metadata); err != nil { + return chart.Chart{}, err + } + return chart.Chart{Metadata: &metadata}, nil +} diff --git a/internal/app/sources/helm/versions/chart_test.go b/internal/app/sources/helm/versions/chart_test.go new file mode 100644 index 0000000..6105867 --- /dev/null +++ b/internal/app/sources/helm/versions/chart_test.go @@ -0,0 +1,24 @@ +package versions + +import ( + "testing" +) + +func TestLoadChartFiles(t *testing.T) { + testChartsDir := "../../../_testdata" + paths := []string{ + testChartsDir + "/test_chart/Chart.yaml", + testChartsDir + "/test_chart2/Chart.yaml", + testChartsDir + "/test_chart3/Chart.yaml", + testChartsDir + "/test_chart4/Chart.yaml", + } + for i, path := range paths { + c, err := LoadChartFile(path) + if err != nil { + t.Error(err) + } + if c.Name() == "" || c.Metadata.Name == "" { + t.Errorf("Chart %d: Not properly loaded helm chart file", i) + } + } +} diff --git a/internal/app/sources/helm/versions/repo_backend.go b/internal/app/sources/helm/versions/repo_backend.go new file mode 100644 index 0000000..2c2bc49 --- /dev/null +++ b/internal/app/sources/helm/versions/repo_backend.go @@ -0,0 +1,110 @@ +package versions + +import ( + "context" + "fmt" + "io" + "net/url" + "regexp" + "time" + + "github.com/qonto/upgrade-manager/internal/infra/aws" + "go.uber.org/zap" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/repo" +) + +type RepoBackend interface { + getIndexFile() (*repo.IndexFile, error) + getFile(ctx context.Context, url *url.URL) (io.ReadCloser, error) +} + +type RepoBackendType string + +// Chart reprensented by a Chart.yaml hosted on a remote git repository +const ( + GitRepo RepoBackendType = "gitRepo" + + // Chart Hosted on Chart repository with proper index.yaml + HelmRepo RepoBackendType = "helmRepo" + + // Chart Hosted on S3 Bucket with proper index.yaml + S3HelmRepo RepoBackendType = "s3helmRepo" + + // Chart reprensented by a Chart.yaml hosted on the local file-system + LocalRepo RepoBackendType = "localRepo" + + // When a chart.yaml references a dependency with a file//path/to/chart/directory/ like expression, the file + // is therefore "local" to the remote git repo. This is not referencing a local git repo on your local desktop + GitRepoLocal RepoBackendType = "gitRepoLocal" + + // Default timeout duration when calling external helm repositories + DefaultTimeout time.Duration = time.Second * 15 +) + +func getRepoBackendType(repoUrl string) (RepoBackendType, error) { + o := regexp.MustCompile("(?:git|ssh|https?).*.git$") + u, err := url.Parse(repoUrl) + + if o.MatchString(repoUrl) { + return GitRepo, nil + } + if err != nil { + r := regexp.MustCompile("(?:git|ssh|ociZ|https?|git@:.+):(.*)") + + if r.MatchString(repoUrl) { + return GitRepo, nil + } + return "", err + } + switch u.Scheme { + case "s3": + return S3HelmRepo, nil + case "https", "oci": + // determine if GitRepo or HelmRepo + return HelmRepo, nil + case "file": + return GitRepoLocal, nil + } + return "", fmt.Errorf("could not determine RepoBackendType for url %s", repoUrl) +} + +func buildRepoBackend(repoURL string, chartName string, log *zap.Logger, s3Api aws.S3Api) (RepoBackend, error) { + repoType, err := getRepoBackendType(repoURL) + if err != nil { + return nil, err + } + var repoBackend RepoBackend + switch repoType { + case HelmRepo: + log.Debug(fmt.Sprintf("Configuring HelmRepoBackend for %s", chartName)) + if repoURL == "" || chartName == "" { + return nil, fmt.Errorf("missing helm metadata, metadata.RepoURL: %s, metadata.ChartName: %s", repoURL, chartName) + } + cfg := &repo.Entry{ + URL: repoURL, + Name: chartName, + } + cr, err := repo.NewChartRepository(cfg, getter.All(&cli.EnvSettings{})) + if err != nil { + return nil, err + } + repoBackend = &HelmRepoBackend{ChartRepo: cr} + case S3HelmRepo: + log.Debug(fmt.Sprintf("Configuring S3HelmRepoBackend for %s and repo %s", chartName, repoURL)) + + bucketUrl, err := url.Parse(repoURL) + if err != nil { + return nil, err + } + repoBackend = &S3HelmRepoBackend{ + s3client: s3Api, + bucketUrl: bucketUrl, + } + case LocalRepo, GitRepo, GitRepoLocal: + log.Debug(fmt.Sprintf("Chart url pointing to a repo of type %s. No chart helm repository backend to create for top-level chart", repoType)) + return nil, nil + } + return repoBackend, nil +} diff --git a/internal/app/sources/helm/versions/repo_backend_helm.go b/internal/app/sources/helm/versions/repo_backend_helm.go new file mode 100644 index 0000000..f8f2477 --- /dev/null +++ b/internal/app/sources/helm/versions/repo_backend_helm.go @@ -0,0 +1,45 @@ +package versions + +import ( + "context" + "io" + "net/http" + "net/url" + + "helm.sh/helm/v3/pkg/repo" +) + +type HelmRepoBackend struct { + ChartRepo *repo.ChartRepository +} + +// get index file from helm chart repository and return it with +// sorted chart versions +func (h *HelmRepoBackend) getIndexFile() (*repo.IndexFile, error) { + indexFilePath, err := h.ChartRepo.DownloadIndexFile() + if err != nil { + return &repo.IndexFile{}, err + } + index, err := repo.LoadIndexFile(indexFilePath) + if err != nil { + return &repo.IndexFile{}, err + } + index.SortEntries() + + return index, nil +} + +// get file from remote helm chart repository +// TODO: configure http server +func (h *HelmRepoBackend) getFile(ctx context.Context, url *url.URL) (io.ReadCloser, error) { + req, err := http.NewRequestWithContext(ctx, "GET", url.String(), http.NoBody) + if err != nil { + return nil, err + } + client := http.DefaultClient + res, err := client.Do(req) + if err != nil { + return nil, err + } + return res.Body, nil +} diff --git a/internal/app/sources/helm/versions/repo_backend_s3.go b/internal/app/sources/helm/versions/repo_backend_s3.go new file mode 100644 index 0000000..a7b1e81 --- /dev/null +++ b/internal/app/sources/helm/versions/repo_backend_s3.go @@ -0,0 +1,87 @@ +package versions + +import ( + "context" + "fmt" + "io" + "net/url" + "os" + + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/qonto/upgrade-manager/internal/infra/aws" + "helm.sh/helm/v3/pkg/repo" +) + +type S3HelmRepoBackend struct { + s3client aws.S3Api + bucketUrl *url.URL +} + +// Retrieve a file from an S3 backend based on its full url (s3 bucketname + path) +func (h *S3HelmRepoBackend) getFile(ctx context.Context, url *url.URL) (io.ReadCloser, error) { + if url.Scheme != "s3" { + return nil, fmt.Errorf("wrong scheme to get file from s3 url %s, , expected: %s, got: %s", "s3", url, url.Scheme) + } + var bucketKey string + if url.Path != "" && url.Path[0:1] == "/" { + bucketKey = url.Path[1:] + } + f, err := h.s3client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: &url.Host, + Key: &bucketKey, + }, + ) + if err != nil { + return nil, err + } + return f.Body, nil +} + +// Retrieve the index.yaml file from the helm chart repository and return them +// in chronological order +func (h *S3HelmRepoBackend) getIndexFile() (*repo.IndexFile, error) { + var index *repo.IndexFile + indexUrl, err := url.Parse(h.bucketUrl.String() + "/index.yaml") + if err != nil { + return index, err + } + ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancel() + body, err := h.getFile(ctx, indexUrl) + if err != nil { + return index, err + } + raw, err := io.ReadAll(body) + if err != nil { + return nil, err + } + index, err = byteSliceToIndexFile(raw) + if err != nil { + return index, err + } + index.SortEntries() + if err := body.Close(); err != nil { + return index, err + } + return index, nil +} + +func byteSliceToIndexFile(data []byte) (*repo.IndexFile, error) { + f, err := os.CreateTemp(os.TempDir(), "*.yaml") + if err != nil { + return nil, err + } + err = os.WriteFile(f.Name(), data, 0o600) + if err != nil { + return nil, err + } + idx, err := repo.LoadIndexFile(f.Name()) + if err != nil { + return nil, err + } + err = os.Remove(f.Name()) + if err != nil { + return nil, err + } + return idx, err +} diff --git a/internal/app/sources/helm/versions/repo_backend_s3_test.go b/internal/app/sources/helm/versions/repo_backend_s3_test.go new file mode 100644 index 0000000..bd500ab --- /dev/null +++ b/internal/app/sources/helm/versions/repo_backend_s3_test.go @@ -0,0 +1,102 @@ +package versions + +import ( + "bytes" + "context" + "io" + "net/url" + "os" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/qonto/upgrade-manager/internal/infra/aws" +) + +func TestS3RepoBackendGetFile(t *testing.T) { + testCases := map[string]any{ + "s3://foo-bucket/key/to/bar": true, // url, expected result of test + "s3://foo-bucket/bar": true, + "https://foo-website.com": false, + } + for tc, expectSuccess := range testCases { + u, err := url.Parse(tc) + if err != nil { + t.Errorf("Failed parsing input url, %s", err) + } + s3backend := S3HelmRepoBackend{ + s3client: aws.MockS3GetApi(func(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) { + if params.Bucket == nil { + t.Errorf("No bucket name provided for s3 bucket") + } + if params.Key == nil { + t.Errorf("No key provided for file in s3 bucket") + } + + return &s3.GetObjectOutput{Body: io.NopCloser(bytes.NewReader([]byte("hello from s3")))}, nil + }), + } + _, err = s3backend.getFile(context.TODO(), u) + if err != nil { + if expectSuccess == true { + t.Errorf("Unexpected test result, expected successful test but the test failed") + } + } else { + if expectSuccess == false { + t.Errorf("Unexpected test result, expected failed test but the test was successful") + } + } + } +} + +func TestS3RepoBackendGetIndexFile(t *testing.T) { + sampleIndexFile := "../../../_testdata/index.yaml" + testCases := map[string]any{ + "s3://foo-bucket/key/to/bar": true, + "s3://foo-bucket/bar": true, + "https://foo-website.com": false, + } + for tc, expectSuccess := range testCases { + u, err := url.Parse(tc) + if err != nil { + t.Errorf("Failed parsing input url, %s", err) + } + s3backend := S3HelmRepoBackend{ + s3client: aws.MockS3GetApi(func(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) { + if params.Bucket == nil { + t.Errorf("No bucket name provided for s3 bucket") + } + if params.Key == nil { + t.Errorf("No key provided for file in s3 bucket") + } + file, err := os.ReadFile(sampleIndexFile) + if err != nil { + t.Errorf("error reading sample file index.yaml at %s", sampleIndexFile) + } + return &s3.GetObjectOutput{Body: io.NopCloser(bytes.NewReader(file))}, nil + }), + bucketUrl: u, + } + _, err = s3backend.getIndexFile() + if err != nil { + if expectSuccess == true { + t.Errorf("Unexpected test result, expected successful test but the test failed") + } + } else { + if expectSuccess == false { + t.Errorf("Unexpected test result, expected failed test but the test was successful") + } + } + } +} + +func TestByteSliceToIndexFile(t *testing.T) { + sampleIndexFile := "../../../_testdata/index.yaml" + f, err := os.ReadFile(sampleIndexFile) + if err != nil { + t.Error(err) + } + _, err = byteSliceToIndexFile(f) + if err != nil { + t.Error(err) + } +} diff --git a/internal/app/sources/helm/versions/repo_backend_test.go b/internal/app/sources/helm/versions/repo_backend_test.go new file mode 100644 index 0000000..f004aec --- /dev/null +++ b/internal/app/sources/helm/versions/repo_backend_test.go @@ -0,0 +1,26 @@ +package versions + +import "testing" + +func TestGetRepoBackendType(t *testing.T) { + testCases := map[string]RepoBackendType{ + "https://github.foo.com/repo.git": GitRepo, + "git@github.foo.com:bar/repo.git": GitRepo, + "https://github.com/repo.git": GitRepo, + "https://bitnami-labs.github.io/sealed-secrets": HelmRepo, + "https://helm.traefik.io/traefik/traefik-17.0.2.tgz": HelmRepo, + "https://helm.traefik.io/traefik": HelmRepo, + "oci://registry-1.docker.io/bitnamicharts/some-chart": HelmRepo, + "s3://s3-based-repositry": S3HelmRepo, + "s3://s3-based-chart-archive.tgz": S3HelmRepo, + } + for url, expected := range testCases { + result, err := getRepoBackendType(url) + if err != nil { + t.Errorf("Failed retrieving repo backend type: %s", err) + } + if result != expected { + t.Errorf("Got wrong backend type for %s, got: %s expected: %s", url, result, expected) + } + } +} diff --git a/internal/app/sources/helm/versions/versions.go b/internal/app/sources/helm/versions/versions.go new file mode 100644 index 0000000..2a78400 --- /dev/null +++ b/internal/app/sources/helm/versions/versions.go @@ -0,0 +1,175 @@ +package versions + +import ( + "archive/tar" + "compress/gzip" + "context" + "fmt" + "net/url" + "strings" + "time" + + "github.com/qonto/upgrade-manager/internal/app/core/software" + soft "github.com/qonto/upgrade-manager/internal/app/core/software" + "github.com/qonto/upgrade-manager/internal/app/filters" + "github.com/qonto/upgrade-manager/internal/infra/aws" + "go.uber.org/zap" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/repo" +) + +func PopulateTopLevelSoftware(s3Api aws.S3Api, log *zap.Logger, topLevelSoftware *soft.Software, repoURL string, chartName string, filter filters.Filter) error { + log.Debug(fmt.Sprintf("Populate top level software for chart %s repo backend %s", chartName, repoURL)) + repoBackend, err := buildRepoBackend(repoURL, chartName, log, s3Api) + if err != nil { + return err + } + if repoBackend == nil { + return nil + } + err = computeSoftwareVersions(repoBackend, chartName, topLevelSoftware, filter, log) + if err != nil { + return err + } + return nil +} + +func PopulateSoftwareDependencies(s3Api aws.S3Api, log *zap.Logger, topLevelSoftware *soft.Software, chart *chart.Chart, st soft.SoftwareType, filter filters.Filter) error { + softwareDependencies := []*soft.Software{} + for _, dependency := range chart.Metadata.Dependencies { + var depName string + if dependency.Alias != "" { + depName = fmt.Sprintf("%s (%s)", dependency.Alias, dependency.Name) + } else { + depName = dependency.Name + } + softwareDependency := &soft.Software{ + Name: depName, + Version: soft.Version{ + Version: CleanVersion(dependency.Version), + }, + Type: st, + } + log.Debug(fmt.Sprintf("Populate dependency software for dep %s repo backend %s", depName, dependency.Repository)) + + depRepoBackend, err := buildRepoBackend(dependency.Repository, dependency.Name, log, s3Api) + if err != nil { + return err + } + // we cannot do anything with this dependency TODO log ? + if depRepoBackend == nil { + continue + } + err = computeSoftwareVersions(depRepoBackend, dependency.Name, softwareDependency, filter, log) + if err != nil { + return err + } + softwareDependencies = append(softwareDependencies, softwareDependency) + } + + topLevelSoftware.Dependencies = softwareDependencies + return nil +} + +func computeSoftwareVersions(repoBackend RepoBackend, chartName string, s *soft.Software, filter filters.Filter, log *zap.Logger) error { + index, err := repoBackend.getIndexFile() + if err != nil { + return err + } + filteredVersions := []soft.Version{} + + allVersions, ok := index.Entries[chartName] + if !ok { + return fmt.Errorf("Fail to get chart versions for chart %s (repo %s)", chartName, repoBackend) + } + log.Debug(fmt.Sprintf("Found a total of %d versions for chart %s", allVersions.Len(), chartName)) + + trustedCreationDate := trustIndexFileCreatedField(allVersions) + log.Debug("Trusting index.yml file Created field") + + for i := range allVersions { + chartVersion := allVersions[i] + candidateVersion := software.Version{ + Name: chartVersion.Name, + Version: chartVersion.Version, + } + + if trustedCreationDate { + candidateVersion.ReleaseDate = chartVersion.Created + } else { + // prefilter charts + // can fail for dates versions because not released yet + keep := filter(s.Version, candidateVersion) + if !keep { + continue + } + releaseDate, err := getReleaseDateFromArchive(chartVersion, repoBackend) + if err != nil { + log.Warn(fmt.Sprintf("Fail to get release date from archive for chart %s version %s: %s", chartVersion.Name, chartVersion.Version, err.Error())) + continue + } + candidateVersion.ReleaseDate = releaseDate + } + keep := filter(s.Version, candidateVersion) + if keep { + filteredVersions = append(filteredVersions, candidateVersion) + } + } + + log.Debug(fmt.Sprintf("Found a total of %d newer versions for chart %s", len(filteredVersions), chartName)) + s.VersionCandidates = filteredVersions + return nil +} + +// Defines if we should trust the index.yaml file Created field (known behavior: https://github.com/Talend/helm-charts-public/issues/5) +func trustIndexFileCreatedField(versions repo.ChartVersions) bool { + for _, testVersion := range versions { + yt, mt, dt := testVersion.Created.UTC().Date() + sameDateRelease := 0 + // test if properly set created timestamp or if we need to check directly the tgz date + // check the first 2 most recent versions vs all other versions + for _, v := range versions { + y, m, d := v.Created.UTC().Date() + if y == yt && m == mt && d == dt { + sameDateRelease++ + } + } + // if more than a third of the versions matched the testVersion + if sameDateRelease > len(versions)/3 { + return false + } + } + return true +} + +func CleanVersion(s string) string { + cleanups := map[string]string{"^": "", "*": "0", "~": "", ">": "", "<": "", "=": "", "!": ""} + for old, new := range cleanups { + s = strings.ReplaceAll(s, old, new) + } + return s +} + +// Retrieves the release date of the chart based on the last modification date of the actual chart .tgz file +func getReleaseDateFromArchive(v *repo.ChartVersion, backend RepoBackend) (time.Time, error) { + url, err := url.Parse(v.URLs[0]) + if err != nil { + return time.Now(), err + } + ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancel() + rawTarGZ, err := backend.getFile(ctx, url) + if err != nil { + return time.Now(), err + } + uncompressedStream, err := gzip.NewReader(rawTarGZ) + if err != nil { + return time.Now(), err + } + tarReader := tar.NewReader(uncompressedStream) + entry, err := tarReader.Next() + if err != nil { + return time.Now(), err + } + return entry.FileInfo().ModTime(), nil +} diff --git a/internal/app/sources/utils/gitutils/git.go b/internal/app/sources/utils/gitutils/git.go new file mode 100644 index 0000000..8b29a97 --- /dev/null +++ b/internal/app/sources/utils/gitutils/git.go @@ -0,0 +1,44 @@ +package gitutils + +import ( + "os" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/transport" +) + +type RepoConnectionProvider interface { + Clone(directory string, repoUrl string, revision string) (*git.Repository, error) // Clone a git repository to the directory specified +} + +type RepoConnection struct { + Url string + Auth transport.AuthMethod + Private bool +} + +// type GitUrlType string + +// const SshUrl gitRepoUrl = "sshUrl" +// const HttpUrl gitRepoUrl = "httpUrl" + +func (rc *RepoConnection) Clone(directory string, repoUrl string, revision string) (*git.Repository, error) { + if err := os.MkdirAll(directory, 0o700); err != nil { + return &git.Repository{}, err + } + r, err := git.PlainClone(directory, false, + &git.CloneOptions{ + URL: repoUrl, + ReferenceName: plumbing.ReferenceName("refs/heads/" + revision), + SingleBranch: true, + Depth: 1, + Auth: rc.Auth, + }) + return r, err +} + +// func IsGitUrlPrefix(u string) bool { +// r, _ := regexp.Compile("(?:git|ssh|https?|git@:.+):(.*)") +// return r.MatchString(u) +// } diff --git a/internal/app/sources/utils/utils.go b/internal/app/sources/utils/utils.go new file mode 100644 index 0000000..947538c --- /dev/null +++ b/internal/app/sources/utils/utils.go @@ -0,0 +1,5 @@ +package utils + +func Ptr[T any](v T) *T { + return &v +} diff --git a/internal/build/build.go b/internal/build/build.go new file mode 100644 index 0000000..32d4cfe --- /dev/null +++ b/internal/build/build.go @@ -0,0 +1,13 @@ +package build + +import "fmt" + +var ( + Version = "development" //nolint + CommitSHA = "unknown" //nolint + Date = "unknown" //nolint +) + +func VersionMessage() string { + return fmt.Sprintf("version %s (commit %s) released %s", Version, CommitSHA, Date) +} diff --git a/internal/infra/aws/config.go b/internal/infra/aws/config.go new file mode 100644 index 0000000..73e6cd1 --- /dev/null +++ b/internal/infra/aws/config.go @@ -0,0 +1,7 @@ +package aws + +type Configs []Config + +type Config struct { + Region string `yaml:"region"` +} diff --git a/internal/infra/aws/eks.go b/internal/infra/aws/eks.go new file mode 100644 index 0000000..ed38041 --- /dev/null +++ b/internal/infra/aws/eks.go @@ -0,0 +1,15 @@ +package aws + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/eks" +) + +type EKSApi interface { + DescribeAddon(ctx context.Context, params *eks.DescribeAddonInput, optFns ...func(*eks.Options)) (*eks.DescribeAddonOutput, error) + DescribeAddonVersions(ctx context.Context, params *eks.DescribeAddonVersionsInput, optFns ...func(*eks.Options)) (*eks.DescribeAddonVersionsOutput, error) + DescribeCluster(ctx context.Context, params *eks.DescribeClusterInput, optFns ...func(*eks.Options)) (*eks.DescribeClusterOutput, error) + ListClusters(ctx context.Context, params *eks.ListClustersInput, optFns ...func(*eks.Options)) (*eks.ListClustersOutput, error) + ListAddons(ctx context.Context, params *eks.ListAddonsInput, optFns ...func(*eks.Options)) (*eks.ListAddonsOutput, error) +} diff --git a/internal/infra/aws/eks_mock.go b/internal/infra/aws/eks_mock.go new file mode 100644 index 0000000..fc56a6f --- /dev/null +++ b/internal/infra/aws/eks_mock.go @@ -0,0 +1,37 @@ +package aws + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/eks" + "github.com/stretchr/testify/mock" +) + +type EksMock struct { + mock.Mock +} + +func (m *EksMock) ListClusters(ctx context.Context, params *eks.ListClustersInput, optFns ...func(*eks.Options)) (*eks.ListClustersOutput, error) { + args := m.Called() + return args.Get(0).(*eks.ListClustersOutput), nil //nolint +} + +func (m *EksMock) DescribeCluster(ctx context.Context, params *eks.DescribeClusterInput, optFns ...func(*eks.Options)) (*eks.DescribeClusterOutput, error) { + args := m.Called(*params) + return args.Get(0).(*eks.DescribeClusterOutput), nil //nolint +} + +func (m *EksMock) DescribeAddon(ctx context.Context, params *eks.DescribeAddonInput, optFns ...func(*eks.Options)) (*eks.DescribeAddonOutput, error) { + args := m.Called(*params) + return args.Get(0).(*eks.DescribeAddonOutput), nil //nolint +} + +func (m *EksMock) DescribeAddonVersions(ctx context.Context, params *eks.DescribeAddonVersionsInput, optFns ...func(*eks.Options)) (*eks.DescribeAddonVersionsOutput, error) { + args := m.Called() + return args.Get(0).(*eks.DescribeAddonVersionsOutput), nil //nolint +} + +func (m *EksMock) ListAddons(ctx context.Context, params *eks.ListAddonsInput, optFns ...func(*eks.Options)) (*eks.ListAddonsOutput, error) { + args := m.Called(*params) + return args.Get(0).(*eks.ListAddonsOutput), nil //nolint +} diff --git a/internal/infra/aws/elasticache.go b/internal/infra/aws/elasticache.go new file mode 100644 index 0000000..996b7f5 --- /dev/null +++ b/internal/infra/aws/elasticache.go @@ -0,0 +1,12 @@ +package aws + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/elasticache" +) + +type ElasticacheApi interface { + DescribeCacheClusters(ctx context.Context, params *elasticache.DescribeCacheClustersInput, optFns ...func(*elasticache.Options)) (*elasticache.DescribeCacheClustersOutput, error) + DescribeCacheEngineVersions(ctx context.Context, params *elasticache.DescribeCacheEngineVersionsInput, optFns ...func(*elasticache.Options)) (*elasticache.DescribeCacheEngineVersionsOutput, error) +} diff --git a/internal/infra/aws/elasticache_mock.go b/internal/infra/aws/elasticache_mock.go new file mode 100644 index 0000000..e155492 --- /dev/null +++ b/internal/infra/aws/elasticache_mock.go @@ -0,0 +1,99 @@ +package aws + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/elasticache" + "github.com/aws/aws-sdk-go-v2/service/elasticache/types" +) + +type ElasticacheMock struct{} + +func (m *ElasticacheMock) DescribeCacheClusters(ctx context.Context, params *elasticache.DescribeCacheClustersInput, optFns ...func(*elasticache.Options)) (*elasticache.DescribeCacheClustersOutput, error) { + return &elasticache.DescribeCacheClustersOutput{ + CacheClusters: []types.CacheCluster{ + { + ARN: aws.String("someRandomArn1"), + CacheClusterId: aws.String("redis1-001"), + Engine: aws.String("redis"), + ReplicationGroupId: aws.String("redis1"), + EngineVersion: aws.String("1.2.3"), + }, + { + ARN: aws.String("someRandomArn2"), + CacheClusterId: aws.String("redis1-002"), + Engine: aws.String("redis"), + ReplicationGroupId: aws.String("redis1"), + EngineVersion: aws.String("1.2.3"), + }, + { + ARN: aws.String("someRandomArn3"), + CacheClusterId: aws.String("automation-001"), + Engine: aws.String("redis"), + ReplicationGroupId: aws.String("automation"), + EngineVersion: aws.String("1.2.3"), + }, + { + ARN: aws.String("someRandomArn4"), + CacheClusterId: aws.String("automation-002"), + Engine: aws.String("redis"), + ReplicationGroupId: aws.String("automation"), + EngineVersion: aws.String("1.2.3"), + }, + { + ARN: aws.String("someRandomArn5"), + CacheClusterId: aws.String("memcached1-001"), + Engine: aws.String("memcached"), + ReplicationGroupId: aws.String("memcached1"), + EngineVersion: aws.String("1.2.3"), + }, + { + ARN: aws.String("someRandomArn6"), + CacheClusterId: aws.String("memcached1-002"), + Engine: aws.String("memcached"), + ReplicationGroupId: aws.String("memcached1"), + EngineVersion: aws.String("1.2.3"), + }, + }, + }, nil +} + +func (m *ElasticacheMock) DescribeCacheEngineVersions(ctx context.Context, params *elasticache.DescribeCacheEngineVersionsInput, optFns ...func(*elasticache.Options)) (*elasticache.DescribeCacheEngineVersionsOutput, error) { + return &elasticache.DescribeCacheEngineVersionsOutput{ + CacheEngineVersions: []types.CacheEngineVersion{ + { + Engine: aws.String("redis"), + EngineVersion: aws.String("7.0.1"), + }, + { + Engine: aws.String("redis"), + EngineVersion: aws.String("7.0.0"), + }, + { + Engine: aws.String("redis"), + EngineVersion: aws.String("6.2.6"), + }, + { + Engine: aws.String("redis"), + EngineVersion: aws.String("6.0.0"), + }, + { + Engine: aws.String("memcached"), + EngineVersion: aws.String("5.0.1"), + }, + { + Engine: aws.String("memcached"), + EngineVersion: aws.String("5.0.0"), + }, + { + Engine: aws.String("memcached"), + EngineVersion: aws.String("4.2.4"), + }, + { + Engine: aws.String("memcached"), + EngineVersion: aws.String("4.0.0"), + }, + }, + }, nil +} diff --git a/internal/infra/aws/lambda.go b/internal/infra/aws/lambda.go new file mode 100644 index 0000000..aa970d5 --- /dev/null +++ b/internal/infra/aws/lambda.go @@ -0,0 +1,100 @@ +package aws + +import ( + "context" + "regexp" + + "github.com/aws/aws-sdk-go-v2/service/lambda" + "github.com/aws/aws-sdk-go-v2/service/lambda/types" +) + +type LambdaApi interface { + ListFunctions(ctx context.Context, params *lambda.ListFunctionsInput, optFns ...func(*lambda.Options)) (*lambda.ListFunctionsOutput, error) +} + +// There is no API to retrieve a list of available runtimes. +// +// As a workaround, we source them from the lambda/types packages +// To fetch latest versions available +func ListLambdaRuntimes() []types.Runtime { + return types.RuntimePython39.Values() +} + +func ListSupportedRuntimes() []types.Runtime { + supportedRuntimes := []types.Runtime{} + rtList := ListLambdaRuntimes() + drtList := GetDefaultDeprecatedRuntimesList() + for _, rt := range rtList { + supported := true + for _, drt := range drtList { + if rt == drt { + supported = false + break + } + } + if supported { + supportedRuntimes = append(supportedRuntimes, rt) + } + } + return supportedRuntimes +} + +func IsLambdaRuntime(runtime types.Runtime) bool { + rtList := ListLambdaRuntimes() + for _, rt := range rtList { + if rt == runtime { + return true + } + } + return false +} + +func GetDefaultDeprecatedRuntimesList() []types.Runtime { + return []types.Runtime{ + "nodejs", + "nodejs4.3", + "nodejs4.3-edge", + "nodejs6.10", + "nodejs8.10", + "nodejs10.x", + "nodejs12.x", + "java8", + "java8.al2", + "python2.7", + "python3.6", + "dotnetcore1.0", + "dotnetcore2.0", + "dotnet6", + "ruby2.5", + } +} + +// Returns a function that, given a runtime, returns all the runtimes of the same family +func NewLambdaRuntimeFamilyVersionProvider() func(runtime types.Runtime) []types.Runtime { + rtList := ListSupportedRuntimes() + rtVersionByFamily := map[string][]types.Runtime{} + matchers := map[string]*regexp.Regexp{ + "python": regexp.MustCompile("python.+"), + "nodejs": regexp.MustCompile("nodejs.+"), + "java": regexp.MustCompile("java.+"), + "dotnetcore": regexp.MustCompile("dotnetcore.+"), + "dotnet": regexp.MustCompile("dotnet.+"), + "ruby": regexp.MustCompile("ruby.+"), + "go": regexp.MustCompile("go.+"), + } + for _, rt := range rtList { + for family, expr := range matchers { + if expr.MatchString(string(rt)) { + rtVersionByFamily[family] = append(rtVersionByFamily[family], rt) + } + } + } + return func(runtime types.Runtime) []types.Runtime { + for family, expr := range matchers { + if expr.MatchString(string(runtime)) { + return rtVersionByFamily[family] + } + } + return nil + } +} diff --git a/internal/infra/aws/lambda_mock.go b/internal/infra/aws/lambda_mock.go new file mode 100644 index 0000000..96b1cf4 --- /dev/null +++ b/internal/infra/aws/lambda_mock.go @@ -0,0 +1,17 @@ +package aws + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/lambda" + "github.com/stretchr/testify/mock" +) + +type LambdaMock struct { + mock.Mock +} + +func (m *LambdaMock) ListFunctions(ctx context.Context, params *lambda.ListFunctionsInput, optFns ...func(*lambda.Options)) (*lambda.ListFunctionsOutput, error) { + args := m.Called() + return args.Get(0).(*lambda.ListFunctionsOutput), args.Error(1) //nolint +} diff --git a/internal/infra/aws/lambda_test.go b/internal/infra/aws/lambda_test.go new file mode 100644 index 0000000..179d6e8 --- /dev/null +++ b/internal/infra/aws/lambda_test.go @@ -0,0 +1,79 @@ +package aws + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/service/lambda/types" + "github.com/stretchr/testify/assert" +) + +func TestGetDefaultDeprecatedRuntimesList(t *testing.T) { + rt := GetDefaultDeprecatedRuntimesList() + assert.NotEmpty(t, rt) +} + +func TestIsLambdaRuntime(t *testing.T) { + rts := ListLambdaRuntimes() + expectedCount := len(rts) + actualCount := 0 + + notLambdaRuntimes := []types.Runtime{"perl", "cobol"} + rts = append(rts, notLambdaRuntimes...) + for _, rt := range rts { + if IsLambdaRuntime(rt) { + actualCount++ + } + } + assert.Equal(t, expectedCount, actualCount) +} + +func TestFamilyVersionProvider(t *testing.T) { + defaultValidateResult := func(t *testing.T, rts []types.Runtime) { + t.Helper() + assert.NotEmpty(t, rts) + } + testCases := []struct { + Description string + Runtime types.Runtime + ValidateResult func(t *testing.T, rts []types.Runtime) + }{ + { + Description: "python", + Runtime: "python3.9", + }, + { + Description: "nodejs", + Runtime: "nodejs18.x", + }, + { + Description: "go", + Runtime: "go1.x", + }, + { + Description: "dotnetcore", + Runtime: "dotnetcore1.0", + }, + { + Description: "java", + Runtime: "java8", + }, + { + Description: "unknown runtime", + Runtime: "unknown", + ValidateResult: func(t *testing.T, rts []types.Runtime) { + t.Helper() + assert.Empty(t, rts) + }, + }, + } + provideFamilyVersions := NewLambdaRuntimeFamilyVersionProvider() + for i := range testCases { + t.Run(testCases[i].Description, func(t *testing.T) { + result := provideFamilyVersions(testCases[i].Runtime) + if testCases[i].ValidateResult == nil { + testCases[i].ValidateResult = defaultValidateResult + } + testCases[i].ValidateResult(t, result) + }) + } +} diff --git a/internal/infra/aws/mock.go b/internal/infra/aws/mock.go new file mode 100644 index 0000000..2c68284 --- /dev/null +++ b/internal/infra/aws/mock.go @@ -0,0 +1,21 @@ +package aws + +import ( + "bytes" + "context" + "io" + + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/stretchr/testify/mock" +) + +type S3Mock struct { + mock.Mock +} + +func (m *S3Mock) GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) { + args := m.Called(*params.Bucket) + b := args.Get(0).([]byte) //nolint + output := &s3.GetObjectOutput{Body: io.NopCloser(bytes.NewReader(b))} + return output, args.Error(1) //nolint +} diff --git a/internal/infra/aws/msk.go b/internal/infra/aws/msk.go new file mode 100644 index 0000000..b43397b --- /dev/null +++ b/internal/infra/aws/msk.go @@ -0,0 +1,12 @@ +package aws + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/kafka" +) + +type MSKApi interface { + GetCompatibleKafkaVersions(ctx context.Context, params *kafka.GetCompatibleKafkaVersionsInput, optFns ...func(*kafka.Options)) (*kafka.GetCompatibleKafkaVersionsOutput, error) + ListClustersV2(ctx context.Context, params *kafka.ListClustersV2Input, optFns ...func(*kafka.Options)) (*kafka.ListClustersV2Output, error) +} diff --git a/internal/infra/aws/msk_mock.go b/internal/infra/aws/msk_mock.go new file mode 100644 index 0000000..e4f0202 --- /dev/null +++ b/internal/infra/aws/msk_mock.go @@ -0,0 +1,22 @@ +package aws + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/kafka" + "github.com/stretchr/testify/mock" +) + +type MockMSKApi struct { + mock.Mock +} + +func (m *MockMSKApi) GetCompatibleKafkaVersions(ctx context.Context, params *kafka.GetCompatibleKafkaVersionsInput, optFns ...func(*kafka.Options)) (*kafka.GetCompatibleKafkaVersionsOutput, error) { + args := m.Called() + return args.Get(0).(*kafka.GetCompatibleKafkaVersionsOutput), nil //nolint +} + +func (m *MockMSKApi) ListClustersV2(ctx context.Context, params *kafka.ListClustersV2Input, optFns ...func(*kafka.Options)) (*kafka.ListClustersV2Output, error) { + args := m.Called() + return args.Get(0).(*kafka.ListClustersV2Output), nil //nolint +} diff --git a/internal/infra/aws/rds.go b/internal/infra/aws/rds.go new file mode 100644 index 0000000..52e362c --- /dev/null +++ b/internal/infra/aws/rds.go @@ -0,0 +1,12 @@ +package aws + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/rds" +) + +type RDSApi interface { + DescribeDBEngineVersions(ctx context.Context, params *rds.DescribeDBEngineVersionsInput, optFns ...func(*rds.Options)) (*rds.DescribeDBEngineVersionsOutput, error) + DescribeDBInstances(ctx context.Context, params *rds.DescribeDBInstancesInput, optFns ...func(*rds.Options)) (*rds.DescribeDBInstancesOutput, error) +} diff --git a/internal/infra/aws/rds_mock.go b/internal/infra/aws/rds_mock.go new file mode 100644 index 0000000..228ffcb --- /dev/null +++ b/internal/infra/aws/rds_mock.go @@ -0,0 +1,22 @@ +package aws + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/rds" + "github.com/stretchr/testify/mock" +) + +type MockRDSApi struct { + mock.Mock +} + +func (m *MockRDSApi) DescribeDBInstances(ctx context.Context, params *rds.DescribeDBInstancesInput, optFns ...func(*rds.Options)) (*rds.DescribeDBInstancesOutput, error) { + args := m.Called() + return args.Get(0).(*rds.DescribeDBInstancesOutput), nil //nolint +} + +func (m *MockRDSApi) DescribeDBEngineVersions(ctx context.Context, params *rds.DescribeDBEngineVersionsInput, optFns ...func(*rds.Options)) (*rds.DescribeDBEngineVersionsOutput, error) { + args := m.Called() + return args.Get(0).(*rds.DescribeDBEngineVersionsOutput), nil //nolint +} diff --git a/internal/infra/aws/s3.go b/internal/infra/aws/s3.go new file mode 100644 index 0000000..3a64690 --- /dev/null +++ b/internal/infra/aws/s3.go @@ -0,0 +1,11 @@ +package aws + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/s3" +) + +type S3Api interface { + GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) +} diff --git a/internal/infra/aws/s3_mock.go b/internal/infra/aws/s3_mock.go new file mode 100644 index 0000000..77b98ea --- /dev/null +++ b/internal/infra/aws/s3_mock.go @@ -0,0 +1,13 @@ +package aws + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/s3" +) + +type MockS3GetApi func(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) + +func (m MockS3GetApi) GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) { + return m(ctx, params, optFns...) +} diff --git a/internal/infra/http/server.go b/internal/infra/http/server.go new file mode 100644 index 0000000..6dbe202 --- /dev/null +++ b/internal/infra/http/server.go @@ -0,0 +1,130 @@ +package http + +import ( + "context" + "fmt" + "net/http" + "os" + "sync" + "time" + + "github.com/gin-gonic/gin" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "go.uber.org/zap" +) + +type Config struct { + Host string `validate:"required"` + Port uint32 `validate:"required,gt=1024,lt=65535"` + WriteTimeout int `yaml:"write-timeout" validate:"gt=-1,lt=60"` + ReadTimeout int `yaml:"read-timeout" validate:"gt=-1,lt=60"` + ReadHeaderTimeout int `yaml:"read-header-timeout" validate:"gt=-1,lt=60"` +} + +type HTTPServer struct { + server *http.Server + Engine *gin.Engine + logger *zap.Logger + + wg sync.WaitGroup + registry *prometheus.Registry + requestHistogram *prometheus.HistogramVec + responseCounter *prometheus.CounterVec +} + +func healthz(context *gin.Context) { + context.JSON(200, "ok") +} + +func New(registry *prometheus.Registry, logger *zap.Logger, config Config) (*HTTPServer, error) { + var defaultTimeout int = 10 + engine := gin.New() + address := fmt.Sprintf("%s:%d", config.Host, config.Port) + if config.WriteTimeout == 0 { + config.WriteTimeout = defaultTimeout + } + if config.ReadTimeout == 0 { + config.ReadTimeout = defaultTimeout + } + if config.ReadHeaderTimeout == 0 { + config.ReadHeaderTimeout = defaultTimeout + } + server := &http.Server{ + WriteTimeout: time.Duration(config.WriteTimeout) * time.Second, + ReadTimeout: time.Duration(config.ReadTimeout) * time.Second, + ReadHeaderTimeout: time.Duration(config.ReadHeaderTimeout) * time.Second, + Addr: address, + Handler: engine, + } + respCounter := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "http_responses_total", + Help: "Count the number of HTTP responses.", + }, + []string{"method", "status", "rule"}) + + buckets := []float64{ + 0.05, 0.1, 0.2, 0.4, 0.8, 1, + 1.5, 2, 3, 5, + } + + reqHistogram := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "http_requests_duration_second", + Help: "Time to execute http requests", + Buckets: buckets, + }, + []string{"method", "rule"}) + + return &HTTPServer{ + server: server, + Engine: engine, + logger: logger, + requestHistogram: reqHistogram, + responseCounter: respCounter, + registry: registry, + }, nil +} + +func (h *HTTPServer) Start() error { + h.logger.Info(fmt.Sprintf("Starting HTTP server on %s", h.server.Addr)) + err := h.registry.Register(h.responseCounter) + if err != nil { + return err + } + + err = h.registry.Register(h.requestHistogram) + if err != nil { + return err + } + + go func() { + defer h.wg.Done() + + err = h.server.ListenAndServe() + + if err != nil && err != http.ErrServerClosed { //nolint + h.logger.Error(fmt.Sprintf("HTTP server error: %s", err.Error())) + exitCode := 2 + os.Exit(exitCode) + } + }() + h.wg.Add(1) + h.Engine.GET("/healthz", healthz) + var gatherer prometheus.Gatherer = h.registry + h.Engine.GET("/metrics", gin.WrapH(promhttp.HandlerFor(gatherer, promhttp.HandlerOpts{}))) + return nil +} + +func (h *HTTPServer) Stop() error { + h.logger.Info("Stopping HTTP Server") + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) //nolint + defer cancel() + if err := h.server.Shutdown(ctx); err != nil { + h.logger.Error(err.Error()) + return err + } + h.wg.Wait() + return nil +} diff --git a/internal/infra/kubernetes/argocd.go b/internal/infra/kubernetes/argocd.go new file mode 100644 index 0000000..bdbd64b --- /dev/null +++ b/internal/infra/kubernetes/argocd.go @@ -0,0 +1,137 @@ +package kubernetes + +import ( + "context" + "fmt" + + "github.com/qonto/upgrade-manager/internal/app/sources/helm/versions" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type ArgoCDApplication struct { + Name string `json:"name"` + DestinationNamespace string `json:"destination-namespace"` + Project string `json:"project"` + Server string `json:"server"` + RepoURL string `json:"repoURL"` + ChartFilePath string `json:"path"` + Chart string `json:"chart"` + CurrentVersion string `json:"version"` + RepoBackendType versions.RepoBackendType `json:"repobackendtype"` // Git or Helm + GitRevision string // master / dev / ... +} + +var applicationGroup = schema.GroupVersionResource{ + Group: "argoproj.io", + Version: "v1alpha1", + Resource: "applications", +} + +// ListArgoApplications Retrieve helm-based argocd Applications from a kubernetes cluster's namespace' +func (c *Client) ListArgoApplications(ctx context.Context, namespace string, filters ...ArgoCDAppFilter) ([]*ArgoCDApplication, error) { + var apps []*ArgoCDApplication + + rawApps, err := c.dynamicClient.Resource(applicationGroup).Namespace(namespace).List(ctx, v1.ListOptions{}) + if err != nil { + return nil, err + } + for _, rawApp := range rawApps.Items { + app, err := rawToArgoApplication(rawApp.UnstructuredContent()) + if err != nil { + c.logger.Info(fmt.Sprintf("Skipping app %s: not a properly deployed ArgoCD Helm App (error %s)", app.Name, err.Error())) + continue + } + if len(filters) > 0 { + for _, filter := range filters { + if keep := filter(app); keep { + apps = append(apps, app) + } + } + } else { + apps = append(apps, app) + } + } + for _, app := range apps { + c.logger.Debug(fmt.Sprintf("Tracking app %s with version %s, destination namespace: %s", app.Name, app.CurrentVersion, app.DestinationNamespace)) + } + return apps, nil +} + +func rawToArgoApplication(raw map[string]any) (*ArgoCDApplication, error) { + name, found, err := unstructured.NestedString(raw, "metadata", "name") + if err != nil || !found { + return nil, err + } + newApp := &ArgoCDApplication{Name: name} + + _, isHelmApp, err := unstructured.NestedMap(raw, "spec", "source", "helm") + if err != nil { + return newApp, err + } + _, isDeployedApp, err := unstructured.NestedSlice(raw, "status", "history") + if err != nil { + return newApp, err + } + if !isHelmApp || !isDeployedApp { + return newApp, fmt.Errorf("not a properly deployed Argo Helm application") + } + // These fields exist an all apps + server, found, err := unstructured.NestedString(raw, "spec", "destination", "server") + if err != nil || !found { + return newApp, err + } + + namespace, found, err := unstructured.NestedString(raw, "spec", "destination", "namespace") + if err != nil || !found { + return newApp, err + } + + project, found, err := unstructured.NestedString(raw, "spec", "project") + if err != nil || !found { + return newApp, err + } + + repoUrl, found, err := unstructured.NestedString(raw, "spec", "source", "repoURL") + if err != nil || !found { + return newApp, err + } + + // At least one of these fields exist (either git repo with chart.yaml or helm repo with index.yaml) + chartFilePath, chartFilePathFound, err := unstructured.NestedString(raw, "spec", "source", "path") + if err != nil { + return newApp, err + } + + repoChart, helmChartFound, err := unstructured.NestedString(raw, "spec", "source", "chart") + if err != nil { + return newApp, err + } + if !chartFilePathFound && !helmChartFound { + return newApp, fmt.Errorf("both spec.source.chart and spec.source.path were not defined in %s", name) + } + if chartFilePathFound { + newApp.RepoBackendType = versions.GitRepo + targetRevision, found, err := unstructured.NestedString(raw, "spec", "source", "targetRevision") + if err != nil || !found { + return newApp, err + } + newApp.GitRevision = targetRevision + } else { + newApp.RepoBackendType = versions.HelmRepo + } + + currentRevision, found, err := unstructured.NestedString(raw, "status", "operationState", "syncResult", "revision") + if err != nil || !found { + return newApp, err + } + newApp.Server = server + newApp.Project = project + newApp.RepoURL = repoUrl + newApp.ChartFilePath = chartFilePath + newApp.Chart = repoChart + newApp.CurrentVersion = currentRevision + newApp.DestinationNamespace = namespace + return newApp, nil +} diff --git a/internal/infra/kubernetes/argocd_test.go b/internal/infra/kubernetes/argocd_test.go new file mode 100644 index 0000000..cb7264a --- /dev/null +++ b/internal/infra/kubernetes/argocd_test.go @@ -0,0 +1,208 @@ +package kubernetes + +import ( + "context" + "testing" + + "go.uber.org/zap" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/dynamic/fake" +) + +func TestRawToArgoApplication(t *testing.T) { + type testCase struct { + argoApp map[string]any + expectedSuccess bool + } + testCases := []testCase{ + { + expectedSuccess: true, + argoApp: map[string]any{ + "metadata": map[string]any{ + "name": "normalapp", + }, "spec": map[string]any{ + "source": map[string]any{ + "helm": map[string]any{ + "releaseName": "alertmanager-webhook-logger", + }, + "repoURL": "https://github.qonto.co/devops/kubernetes-resources/alertmanager-webhook-logger.git", + "path": "data-staging", + "targetRevision": "master", + }, + "project": "monitoring", + "destination": map[string]any{ + "server": "https://kubernetes.default.svc", + "namespace": "monitoring", + }, + }, + "status": map[string]any{ + "history": []any{}, + "operationState": map[string]any{ + "syncResult": map[string]any{ + "revision": "1.0.0", + }, + }, + }, + }, + }, + { + expectedSuccess: false, + argoApp: map[string]any{ + "metadata": map[string]any{ + "name": "nostatusapp", + }, "spec": map[string]any{ + "source": map[string]any{ + "helm": map[string]any{ + "releaseName": "alertmanager-webhook-logger", + }, + "repoURL": "https://github.qonto.co/devops/kubernetes-resources/alertmanager-webhook-logger.git", + "path": "data-staging", + "targetRevision": "master", + }, + "project": "monitoring", + "destination": map[string]any{ + "server": "https://kubernetes.default.svc", + "namespace": "monitoring", + }, + }, + }, + }, + { + expectedSuccess: false, + argoApp: map[string]any{ + "metadata": map[string]any{ + "name": "notahelmapp", + }, "spec": map[string]any{ + "source": map[string]any{ + "repoURL": "https://github.qonto.co/devops/kubernetes-resources/alertmanager-webhook-logger.git", + "path": "data-staging", + "targetRevision": "master", + }, + "project": "monitoring", + "destination": map[string]any{ + "server": "https://kubernetes.default.svc", + "namespace": "monitoring", + }, + }, + }, + }, + } + for i, tc := range testCases { + _, err := rawToArgoApplication(tc.argoApp) + if err != nil && tc.expectedSuccess { + t.Fatalf("Case %d: failed to convert raw app but it should have succeeded, %s", i+1, err) + } + if err == nil && !tc.expectedSuccess { + t.Fatalf("Case %d: successfully converted raw app but it should not have succeeded, %s", i+1, err) + } + } +} + +func TestListArgoApplications(t *testing.T) { + log := zap.NewExample() + testCases := []map[string]any{ + { + "kind": "Application", + "apiVersion": "argoproj.io/v1alpha1", + "metadata": map[string]any{ + "name": "normalapp", + "namespace": "argocd", + }, "spec": map[string]any{ + "source": map[string]any{ + "helm": map[string]any{ + "releaseName": "alertmanager-webhook-logger", + }, + "repoURL": "https://github.qonto.co/devops/kubernetes-resources/alertmanager-webhook-logger.git", + "path": "data-staging", + "targetRevision": "master", + }, + "project": "monitoring", + "destination": map[string]any{ + "server": "https://kubernetes.default.svc", + "namespace": "monitoring", + }, + }, + "status": map[string]any{ + "history": []any{}, + "operationState": map[string]any{ + "syncResult": map[string]any{ + "revision": "1.0.0", + }, + }, + }, + }, + { + "kind": "Application", + "apiVersion": "argoproj.io/v1alpha1", + "metadata": map[string]any{ + "name": "normalapp2", + "namespace": "argocd", + }, "spec": map[string]any{ + "source": map[string]any{ + "helm": map[string]any{ + "releaseName": "alertmanager-webhook-logger", + }, + "repoURL": "https://github.qonto.co/devops/kubernetes-resources/alertmanager-webhook-logger.git", + "path": "data-staging", + "targetRevision": "master", + }, + "project": "monitoring", + "destination": map[string]any{ + "server": "https://kubernetes.default.svc", + "namespace": "monitoring", + }, + }, + "status": map[string]any{ + "history": []any{}, + "operationState": map[string]any{ + "syncResult": map[string]any{ + "revision": "2.0.0", + }, + }, + }, + }, + { + "kind": "Application", + "apiVersion": "argoproj.io/v1alpha1", + "metadata": map[string]any{ + "name": "nohistoryapp", + "namespace": "argocd", + }, "spec": map[string]any{ + "source": map[string]any{ + "helm": map[string]any{ + "releaseName": "alertmanager-webhook-logger", + }, + "repoURL": "https://github.qonto.co/devops/kubernetes-resources/alertmanager-webhook-logger.git", + "path": "data-staging", + "targetRevision": "master", + }, + "project": "monitoring", + "destination": map[string]any{ + "server": "https://kubernetes.default.svc", + "namespace": "monitoring", + }, + }, + "status": map[string]any{ + "operationState": map[string]any{ + "syncResult": map[string]any{ + "revision": "2.0.0", + }, + }, + }, + }, + } + client := fake.NewSimpleDynamicClient(&runtime.Scheme{}, + &unstructured.Unstructured{Object: testCases[0]}, + &unstructured.Unstructured{Object: testCases[1]}, + &unstructured.Unstructured{Object: testCases[2]}, + ) + s := Client{dynamicClient: client, logger: log} + apps, err := s.ListArgoApplications(context.Background(), "argocd") + if err != nil { + t.Error(err) + } + if expectedAppCount := 2; len(apps) != expectedAppCount { + t.Errorf("unexpected number of Argocd applications found. Expected: %d, got: %d", expectedAppCount, len(apps)) + } +} diff --git a/internal/infra/kubernetes/client.go b/internal/infra/kubernetes/client.go new file mode 100644 index 0000000..34a6bbd --- /dev/null +++ b/internal/infra/kubernetes/client.go @@ -0,0 +1,69 @@ +package kubernetes + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "go.uber.org/zap" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/util/homedir" +) + +type Client struct { + dynamicClient dynamic.Interface + kubernetesClient kubernetes.Interface + logger *zap.Logger +} + +type KubernetesClient interface { + ListArgoApplications(ctx context.Context, namespace string, filters ...ArgoCDAppFilter) ([]*ArgoCDApplication, error) + ListSecrets(ctx context.Context, namespace string) (*v1.SecretList, error) + ListDeployments(ctx context.Context, request ListRequest) (*appsv1.DeploymentList, error) +} + +func NewClient(logger *zap.Logger) (*Client, error) { + var config *rest.Config + var err error + + kubeconfig := filepath.Join(homedir.HomeDir(), ".kube", "config") + + if _, err := os.Stat(kubeconfig); err != nil { + kubeconfig = "" + } + if kubeconfig == "" { + if os.Getenv("KUBERNETES_SERVICE_HOST") == "" || os.Getenv("KUBERNETES_SERVICE_PORT") == "" { + return nil, fmt.Errorf("kubernetes environment variables not defined") + } + config, err = rest.InClusterConfig() + + if err != nil { + return nil, err + } + } else { + config, err = clientcmd.BuildConfigFromFlags("", kubeconfig) + if err != nil { + return nil, err + } + } + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + return nil, err + } + client, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, err + } + + return &Client{ + logger: logger, + dynamicClient: dynamicClient, + kubernetesClient: client, + }, nil +} diff --git a/internal/infra/kubernetes/deployment.go b/internal/infra/kubernetes/deployment.go new file mode 100644 index 0000000..22ec474 --- /dev/null +++ b/internal/infra/kubernetes/deployment.go @@ -0,0 +1,20 @@ +package kubernetes + +import ( + "context" + + v1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" +) + +type ListRequest struct { + Namespace string + LabelSelector map[string]string +} + +func (c *Client) ListDeployments(ctx context.Context, request ListRequest) (*v1.DeploymentList, error) { + labelSelector := metav1.LabelSelector{MatchLabels: request.LabelSelector} + + return c.kubernetesClient.AppsV1().Deployments(request.Namespace).List(ctx, metav1.ListOptions{LabelSelector: labels.Set(labelSelector.MatchLabels).String()}) +} diff --git a/internal/infra/kubernetes/filters.go b/internal/infra/kubernetes/filters.go new file mode 100644 index 0000000..7e56876 --- /dev/null +++ b/internal/infra/kubernetes/filters.go @@ -0,0 +1,71 @@ +package kubernetes + +import ( + "regexp" +) + +type ArgoCDAppFilter func(*ArgoCDApplication) bool + +type FiltersOptions struct { + // Destination namespace of Argocd Applications + DestinationNamespaceFilterOptions `yaml:"destination-namespace"` + // TODO: + // filter on app name, app labels, transform with annotations etc... +} + +type DestinationNamespaceFilterOptions struct { + NamespaceFilterOptions `yaml:",inline"` +} + +type NamespaceFilterOptions struct { + Include []string `yaml:"include"` + Exclude []string `yaml:"exclude"` +} + +func NewDestinationNamespaceFilter(opts FiltersOptions) (ArgoCDAppFilter, error) { + includeExpr, excludeExpr, err := opts.NamespaceFilterOptions.Compile() + if err != nil { + return nil, err + } + return func(app *ArgoCDApplication) bool { + return FilterNamespace(app.DestinationNamespace, includeExpr, excludeExpr) + }, nil +} + +// Returns a converted-to-*regexp.Regexp version of NamespaceFilterOptions.Include and NamespaceFilterOptions.Exclude +func (ds NamespaceFilterOptions) Compile() ([]*regexp.Regexp, []*regexp.Regexp, error) { + excludeExpr := make([]*regexp.Regexp, 0, len(ds.Exclude)) + includeExpr := make([]*regexp.Regexp, 0, len(ds.Include)) + + for _, ns := range ds.Exclude { + expr, err := regexp.Compile(ns) + if err != nil { + return nil, nil, err + } + excludeExpr = append(excludeExpr, expr) + } + for _, ns := range ds.Include { + expr, err := regexp.Compile(ns) + if err != nil { + return nil, nil, err + } + includeExpr = append(includeExpr, expr) + } + return includeExpr, excludeExpr, nil +} + +func FilterNamespace(namespace string, includeExpr []*regexp.Regexp, excludeExpr []*regexp.Regexp) bool { + for _, expr := range excludeExpr { + if expr.MatchString(namespace) { + return false + } + } + for _, expr := range includeExpr { + if expr.String() != "" { + if expr.MatchString(namespace) { + return true + } + } + } + return len(includeExpr) == 0 +} diff --git a/internal/infra/kubernetes/filters_test.go b/internal/infra/kubernetes/filters_test.go new file mode 100644 index 0000000..418609b --- /dev/null +++ b/internal/infra/kubernetes/filters_test.go @@ -0,0 +1,138 @@ +package kubernetes + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestWithDestinationNamespaceFilter(t *testing.T) { + testCases := []struct { + description string + opts FiltersOptions + expectedCount int + apps []*ArgoCDApplication + }{ + { + description: "Include some and remove some", + expectedCount: 1, + opts: FiltersOptions{ + DestinationNamespaceFilterOptions: DestinationNamespaceFilterOptions{ + NamespaceFilterOptions: NamespaceFilterOptions{ + Include: []string{"kyverno"}, + Exclude: []string{"master", "deps-*"}, + }, + }, + }, + apps: []*ArgoCDApplication{ + {Name: "App1", DestinationNamespace: "kyverno"}, + {Name: "App2", DestinationNamespace: "master"}, + {Name: "App3", DestinationNamespace: "deps-123"}, + {Name: "App4", DestinationNamespace: "deps-345"}, + }, + }, + { + description: "Include none and remove one", + expectedCount: 3, + opts: FiltersOptions{ + DestinationNamespaceFilterOptions: DestinationNamespaceFilterOptions{ + NamespaceFilterOptions: NamespaceFilterOptions{ + Include: nil, + Exclude: []string{"master", "ns2"}, + }, + }, + }, + apps: []*ArgoCDApplication{ + {Name: "App1", DestinationNamespace: "kyverno"}, + {Name: "App2", DestinationNamespace: "master"}, + {Name: "App3", DestinationNamespace: "deps-123"}, + {Name: "App4", DestinationNamespace: "deps-345"}, + }, + }, + { + description: "Include empty string, remove some", + expectedCount: 0, + opts: FiltersOptions{ + DestinationNamespaceFilterOptions: DestinationNamespaceFilterOptions{ + NamespaceFilterOptions: NamespaceFilterOptions{ + Include: []string{""}, + Exclude: []string{"master", "deps-*"}, + }, + }, + }, + apps: []*ArgoCDApplication{ + {Name: "App1", DestinationNamespace: "kyverno"}, + {Name: "App2", DestinationNamespace: "master"}, + {Name: "App3", DestinationNamespace: "deps-123"}, + {Name: "App4", DestinationNamespace: "deps-345"}, + }, + }, + { + description: "Include none and remove none", + expectedCount: 4, + opts: FiltersOptions{ + DestinationNamespaceFilterOptions: DestinationNamespaceFilterOptions{}, + }, + apps: []*ArgoCDApplication{ + {Name: "App1", DestinationNamespace: "kyverno"}, + {Name: "App2", DestinationNamespace: "master"}, + {Name: "App3", DestinationNamespace: "deps-123"}, + {Name: "App4", DestinationNamespace: "deps-345"}, + }, + }, + { + description: "Include some and remove same", + expectedCount: 0, + opts: FiltersOptions{ + DestinationNamespaceFilterOptions: DestinationNamespaceFilterOptions{ + NamespaceFilterOptions: NamespaceFilterOptions{ + Include: []string{"master"}, + Exclude: []string{"master"}, + }, + }, + }, + apps: []*ArgoCDApplication{ + {Name: "App1", DestinationNamespace: "kyverno"}, + {Name: "App2", DestinationNamespace: "master"}, + {Name: "App3", DestinationNamespace: "deps-123"}, + {Name: "App4", DestinationNamespace: "deps-345"}, + }, + }, + { + description: "Include some and remove none", + expectedCount: 1, + opts: FiltersOptions{ + DestinationNamespaceFilterOptions: DestinationNamespaceFilterOptions{ + NamespaceFilterOptions: NamespaceFilterOptions{ + Include: []string{"master"}, + }, + }, + }, + apps: []*ArgoCDApplication{ + {Name: "App1", DestinationNamespace: "kyverno"}, + {Name: "App2", DestinationNamespace: "master"}, + {Name: "App3", DestinationNamespace: "deps-123"}, + {Name: "App4", DestinationNamespace: "deps-345"}, + }, + }, + } + + // FilterNamespace(opts) + for _, tc := range testCases { + var argoFilters []ArgoCDAppFilter + filter, err := NewDestinationNamespaceFilter(tc.opts) + if err != nil { + t.Error(err) + } + argoFilters = append(argoFilters, filter) + kept := []*ArgoCDApplication{} + for _, app := range tc.apps { + for _, filter := range argoFilters { + if keep := filter(app); keep { + kept = append(kept, app) + } + } + } + assert.Equal(t, tc.expectedCount, len(kept)) + } +} diff --git a/internal/infra/kubernetes/mock.go b/internal/infra/kubernetes/mock.go new file mode 100644 index 0000000..a958ff2 --- /dev/null +++ b/internal/infra/kubernetes/mock.go @@ -0,0 +1,28 @@ +package kubernetes + +import ( + "context" + + "github.com/stretchr/testify/mock" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" +) + +type KubernetesClientMock struct { + mock.Mock +} + +func (m *KubernetesClientMock) ListArgoApplications(ctx context.Context, namespace string, filters ...ArgoCDAppFilter) ([]*ArgoCDApplication, error) { + return nil, nil +} + +func (m *KubernetesClientMock) ListDeployments(ctx context.Context, request ListRequest) (*appsv1.DeploymentList, error) { + args := m.Called(request.Namespace) + return args.Get(0).(*appsv1.DeploymentList), args.Error(1) //nolint +} + +func (m *KubernetesClientMock) ListSecrets(ctx context.Context, namespace string) (*v1.SecretList, error) { + args := m.Called(namespace) + secrets := args.Get(0).(*v1.SecretList) //nolint + return secrets, args.Error(1) +} diff --git a/internal/infra/kubernetes/secret.go b/internal/infra/kubernetes/secret.go new file mode 100644 index 0000000..0c6e4c3 --- /dev/null +++ b/internal/infra/kubernetes/secret.go @@ -0,0 +1,16 @@ +package kubernetes + +import ( + "context" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (c *Client) ListSecrets(ctx context.Context, namespace string) (*v1.SecretList, error) { + secrets, err := c.kubernetesClient.CoreV1().Secrets(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, err + } + return secrets, nil +} diff --git a/internal/infra/registry/client.go b/internal/infra/registry/client.go new file mode 100644 index 0000000..e6245ee --- /dev/null +++ b/internal/infra/registry/client.go @@ -0,0 +1,70 @@ +package registry + +import ( + "fmt" + + ecr "github.com/awslabs/amazon-ecr-credential-helper/ecr-login" + "github.com/awslabs/amazon-ecr-credential-helper/ecr-login/api" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +type Auth struct { + AWS bool `yaml:"aws"` +} + +type Config struct { + EnableDateRetrieval bool `yaml:"enable-date-retrieval"` + Auth Auth `yaml:"auth"` +} + +type Client struct { + config *Config + options []remote.Option +} + +func New(config *Config) (*Client, error) { + options := []remote.Option{} + if config.Auth.AWS { + ecrHelper := ecr.NewECRHelper(ecr.WithClientFactory(api.DefaultClientFactory{})) + auth := remote.WithAuthFromKeychain(authn.NewKeychainFromHelper(ecrHelper)) + options = append(options, auth) + } + return &Client{ + config: config, + options: options, + }, nil +} + +func (c *Client) Tags(repositoryString string) ([]string, error) { + repository, err := name.NewRepository(repositoryString) + if err != nil { + return nil, err + } + tags, err := remote.List(repository, c.options...) + if err != nil { + return nil, err + } + return tags, nil +} + +func (c *Client) ConfigFile(image string) (*v1.ConfigFile, error) { + if c.config.EnableDateRetrieval { + ref, err := name.ParseReference(image) + if err != nil { + return nil, err + } + remote, err := remote.Image(ref, c.options...) + if err != nil { + return nil, err + } + return remote.ConfigFile() + } + return nil, fmt.Errorf("Date retrieval is disabled for this registry (image %s)", image) +} + +func (c *Client) ReleaseDateRetrievalEnabled() bool { + return c.config.EnableDateRetrieval +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..8ae9a2f --- /dev/null +++ b/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "os" + + "github.com/qonto/upgrade-manager/cmd" +) + +func main() { + if err := cmd.InitAndRunCommand(); err != nil { + os.Exit(3) + } +}