Repository: estahn/k8s-image-swapper Branch: main Commit: 5ea715ab58ca Files: 62 Total size: 320.2 KB Directory structure: gitextract_aug9culg/ ├── .github/ │ ├── FUNDING.yml │ ├── dependabot.yml │ ├── release-drafter.yml │ ├── release.yml │ └── workflows/ │ ├── auto-approve.yml │ ├── auto-merge.yml │ ├── awaiting-reply.yml │ ├── codeql-analysis.yml │ ├── deploy.yml │ ├── docs.yml │ ├── pre-commit.yml │ ├── release-drafter.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .goreleaser.yml ├── .k8s-image-swapper.yml ├── .pre-commit-config.yaml ├── .releaserc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd/ │ └── root.go ├── docs/ │ ├── configuration.md │ ├── faq.md │ ├── getting-started.md │ ├── index.md │ └── overrides/ │ └── main.html ├── go.mod ├── go.sum ├── main.go ├── mkdocs.yml ├── package.json ├── pkg/ │ ├── config/ │ │ ├── config.go │ │ └── config_test.go │ ├── registry/ │ │ ├── client.go │ │ ├── ecr.go │ │ ├── ecr_test.go │ │ ├── gar.go │ │ ├── gar_test.go │ │ └── inmemory.go │ ├── secrets/ │ │ ├── dummy.go │ │ ├── dummy_test.go │ │ ├── kubernetes.go │ │ ├── kubernetes_test.go │ │ └── provider.go │ ├── types/ │ │ ├── types.go │ │ └── types_test.go │ └── webhook/ │ ├── image_copier.go │ ├── image_copier_test.go │ ├── image_swapper.go │ └── image_swapper_test.go └── test/ ├── curl.sh ├── e2e_test.go ├── kind-with-registry.sh ├── kind.yaml └── requests/ ├── admissionreview-imagepullsecrets.json └── admissionreview-simple.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [estahn] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username otechie: # Replace with a single Otechie username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" target-branch: "main" schedule: interval: "weekly" - package-ecosystem: "docker" directory: "/" target-branch: "main" schedule: interval: "daily" - package-ecosystem: "gomod" directory: "/" target-branch: "main" schedule: interval: "weekly" - package-ecosystem: "npm" directory: "/" target-branch: "main" schedule: interval: "weekly" versioning-strategy: increase ================================================ FILE: .github/release-drafter.yml ================================================ name-template: 'v$RESOLVED_VERSION' tag-template: 'v$RESOLVED_VERSION' categories: - title: '🚀 Features' labels: - 'feature' - 'enhancement' - title: '🐛 Bug Fixes' labels: - 'fix' - 'bugfix' - 'bug' - title: '📝 Documentation' label: 'docs' - title: '🧰 Maintenance' label: 'chore' - title: '⬆️ Dependencies' collapse-after: 3 labels: - 'dependencies' - title: '👷 Continuous Integration' collapse-after: 3 labels: - 'ci' exclude-labels: - 'ignore-for-release' replacers: - search: '/^(fix|feat|ci|build)(\(.+?\))?: /g' replace: '' template: | ## What's Changed $CHANGES version-resolver: major: labels: - 'type: breaking' minor: labels: - 'enhancement' patch: labels: - 'bugfix' - 'maintenance' - 'docs' - 'dependencies' - 'security' autolabeler: - label: 'bugfix' title: - '/fix:/i' - label: 'enhancement' title: - '/feat:/i' - label: 'docs' title: - '/docs:/i' - label: 'chore' title: - '/chore:/i' ================================================ FILE: .github/release.yml ================================================ changelog: exclude: labels: - ignore-for-release authors: - octocat categories: - title: 🛠 Breaking Changes labels: - breaking-change - title: '🚀 Features' labels: - 'feature' - 'enhancement' - title: '🐛 Bug Fixes' labels: - 'fix' - 'bugfix' - 'bug' - title: '📝 Documentation' label: 'docs' - title: '🧰 Maintenance' label: 'chore' - title: '⬆️ Dependencies' collapse-after: 3 labels: - 'dependencies' - title: '👷 Continuous Integration' collapse-after: 3 labels: - 'ci' - title: Other Changes labels: - "*" ================================================ FILE: .github/workflows/auto-approve.yml ================================================ name: Auto approve on: pull_request_target jobs: auto-approve: runs-on: ubuntu-latest steps: - uses: hmarr/auto-approve-action@v4 if: github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]' with: github-token: "${{ secrets.GITHUB_TOKEN }}" ================================================ FILE: .github/workflows/auto-merge.yml ================================================ name: Auto-Merge on: pull_request permissions: pull-requests: write contents: write jobs: automerge: runs-on: ubuntu-latest if: github.actor == 'dependabot[bot]' steps: - uses: peter-evans/enable-pull-request-automerge@v3 with: pull-request-number: ${{ github.event.pull_request.number }} merge-method: squash ================================================ FILE: .github/workflows/awaiting-reply.yml ================================================ on: issue_comment: types: [created] jobs: awaiting_reply: runs-on: ubuntu-latest name: Toggle label upon reply steps: - name: Toggle label uses: jd-0001/gh-action-toggle-awaiting-reply-label@v2.1.2 with: label: awaiting-reply exclude-members: estahn ================================================ FILE: .github/workflows/codeql-analysis.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ main ] paths-ignore: - 'docs/**' - 'mkdocs.yml' pull_request: # The branches below must be a subset of the branches above branches: [ main ] schedule: - cron: '42 5 * * 5' jobs: analyze: name: Analyze runs-on: ubuntu-latest strategy: fail-fast: false matrix: language: [ 'go' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - name: Checkout repository uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v6 with: go-version-file: 'go.mod' check-latest: true cache: true # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v4 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 ================================================ FILE: .github/workflows/deploy.yml ================================================ name: Deploy on: workflow_call: inputs: forRef: required: true type: string workflow_dispatch: inputs: forRef: description: 'Branch, SHA or Tag to release' required: false type: string permissions: contents: write packages: write env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: generate-artifacts: name: Generate artifacts runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: ref: ${{ inputs.forRef }} - name: Unshallow run: git fetch --prune --unshallow - name: Ensure release-notes exists run: touch /tmp/release-notes.md - name: Set up QEMU uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@v3 - name: Install dependencies run: sudo apt-get update && sudo apt-get install -y libdevmapper-dev libbtrfs-dev - name: Set up Go uses: actions/setup-go@v6 with: go-version-file: 'go.mod' check-latest: true cache: true - name: Login to github registry uses: docker/login-action@v4.1.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Run GoReleaser uses: goreleaser/goreleaser-action@v7.0.0 with: version: latest args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/docs.yml ================================================ #name: Publish docs #on: # workflow_dispatch: # push: # branches: # - main # paths: # - 'docs/**' # - mkdocs.yml # #jobs: # build: # name: Deploy docs # runs-on: ubuntu-latest # steps: # - name: Checkout main # uses: actions/checkout@v3 # with: # fetch-depth: 0 # # - uses: actions/setup-python@v4.5.0 # with: # python-version: '3.x' # # - name: Install mkdocs # run: pip install --upgrade pip && pip install mike mkdocs mkdocs-minify-plugin mkdocs-markdownextradata-plugin mkdocs-macros-plugin pymdown-extensions mkdocs-material # # - run: git config user.name 'github-actions[bot]' && git config user.email 'github-actions[bot]@users.noreply.github.com' # # - name: Publish docs # run: mkdocs gh-deploy ================================================ FILE: .github/workflows/pre-commit.yml ================================================ name: pre-commit on: pull_request: push: branches: [master] jobs: pre-commit: runs-on: ubuntu-latest # don't run this on the master branch if: github.ref != 'refs/heads/master' steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: actions/setup-python@v6.2.0 with: python-version: '3.x' - uses: actions/setup-go@v6 with: go-version-file: 'go.mod' check-latest: true cache: true - name: Install dependencies run: sudo apt-get update && sudo apt-get install -y libdevmapper-dev libbtrfs-dev - uses: pre-commit/action@v3.0.1 with: token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/release-drafter.yml ================================================ name: Release Drafter on: workflow_dispatch: push: # branches to consider in the event; optional, defaults to all branches: - main # pull_request event is required only for autolabeler pull_request: # Only following types are handled by the action, but one can default to all as well types: [opened, reopened, synchronize] # pull_request_target event is required for autolabeler to support PRs from forks pull_request_target: types: [opened, reopened, synchronize] permissions: contents: read jobs: update_release_draft: permissions: # write permission is required to create a github release contents: write # write permission is required for autolabeler # otherwise, read permission is required at least pull-requests: write runs-on: ubuntu-latest steps: # (Optional) GitHub Enterprise requires GHE_HOST variable set #- name: Set GHE_HOST # run: | # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV # Drafts your next Release notes as Pull Requests are merged into "master" - uses: release-drafter/release-drafter@v7 # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml # with: # config-name: my-config.yml # disable-autolabeler: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: workflow_dispatch: # Release patches and secruity updates on a schedule schedule: - cron: "0 0 1 * *" jobs: release: permissions: contents: write pull-requests: write runs-on: ubuntu-latest outputs: tag_name: ${{ steps.release-drafter.outputs.tag_name }} steps: - id: release-drafter uses: release-drafter/release-drafter@v7 with: publish: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} deploy: needs: [release] uses: ./.github/workflows/deploy.yml secrets: inherit permissions: packages: write contents: write with: forRef: ${{ needs.release.outputs.tag_name }} ================================================ FILE: .github/workflows/test.yml ================================================ name: Test on: pull_request: workflow_dispatch: push: branches: - main - 'releases/*' paths-ignore: - 'docs/**' - 'mkdocs.yml' concurrency: group: ${{ github.ref }} cancel-in-progress: true jobs: lint: name: Lint runs-on: ubuntu-latest steps: - name: Install dependencies run: sudo apt-get update && sudo apt-get install -y libdevmapper-dev libbtrfs-dev - name: Checkout uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v6 with: go-version-file: 'go.mod' check-latest: true cache: true - name: golangci-lint uses: golangci/golangci-lint-action@v9.2.0 with: version: latest args: --timeout=5m test: name: Test runs-on: ubuntu-latest steps: - name: Install dependencies run: sudo apt-get update && sudo apt-get install -y libdevmapper-dev libbtrfs-dev - name: Checkout uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v6 with: go-version-file: 'go.mod' check-latest: true cache: true - uses: actions/cache@v5.0.4 with: path: | ~/go/pkg/mod # Module download cache ~/.cache/go-build # Build cache (Linux) ~/Library/Caches/go-build # Build cache (Mac) '%LocalAppData%\go-build' # Build cache (Windows) key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Test run: go test -coverprofile cover.out ./... - uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} file: ./cover.out # optional fail_ci_if_error: true verbose: true image-scan: name: Image Scan runs-on: ubuntu-latest steps: - name: Install dependencies run: sudo apt-get update && sudo apt-get install -y libdevmapper-dev libbtrfs-dev - name: Checkout uses: actions/checkout@v6 - name: Set up QEMU uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@v3 - name: Unshallow run: git fetch --prune --unshallow - name: Set up Go uses: actions/setup-go@v6 with: go-version-file: 'go.mod' check-latest: true cache: true - uses: actions/cache@v5.0.4 with: path: | ~/go/pkg/mod # Module download cache ~/.cache/go-build # Build cache (Linux) ~/Library/Caches/go-build # Build cache (Mac) '%LocalAppData%\go-build' # Build cache (Windows) key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Run GoReleaser uses: goreleaser/goreleaser-action@v7.0.0 with: version: latest args: release --clean --skip=validate,publish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Scan image uses: anchore/scan-action@v7 id: scan with: image: "ghcr.io/estahn/k8s-image-swapper:latest" fail-build: false acs-report-enable: true - name: Upload Anchore scan SARIF report uses: github/codeql-action/upload-sarif@v4 with: sarif_file: ${{ steps.scan.outputs.sarif }} ================================================ FILE: .gitignore ================================================ # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ .idea/ coverage.txt k8s-image-swapper ================================================ FILE: .goreleaser.yml ================================================ env: - GO111MODULE=on gomod: proxy: true builds: - env: - CGO_ENABLED=0 goos: #- windows - darwin - linux goarch: - amd64 - arm64 mod_timestamp: '{{ .CommitTimestamp }}' flags: - -trimpath ldflags: - -s -w dockers: - image_templates: - "ghcr.io/estahn/k8s-image-swapper:latest-amd64" - "ghcr.io/estahn/k8s-image-swapper:{{ .Version }}-amd64" - "ghcr.io/estahn/k8s-image-swapper:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-amd64" - "ghcr.io/estahn/k8s-image-swapper:{{ .Major }}.{{ .Minor }}-amd64" - "ghcr.io/estahn/k8s-image-swapper:{{ .Major }}-amd64" use: buildx dockerfile: Dockerfile goarch: amd64 build_flag_templates: - "--pull" - "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--build-arg=VERSION={{.Version}}" - "--build-arg=BUILD_DATE={{.Date}}" - "--build-arg=VCS_REF={{.FullCommit}}" - "--platform=linux/amd64" - image_templates: - "ghcr.io/estahn/k8s-image-swapper:latest-arm64v8" - "ghcr.io/estahn/k8s-image-swapper:{{ .Version }}-arm64v8" - "ghcr.io/estahn/k8s-image-swapper:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-arm64v8" - "ghcr.io/estahn/k8s-image-swapper:{{ .Major }}.{{ .Minor }}-arm64v8" - "ghcr.io/estahn/k8s-image-swapper:{{ .Major }}-arm64v8" use: buildx dockerfile: Dockerfile goarch: arm64 build_flag_templates: - "--pull" - "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--build-arg=VERSION={{.Version}}" - "--build-arg=BUILD_DATE={{.Date}}" - "--build-arg=VCS_REF={{.FullCommit}}" - "--platform=linux/arm64/v8" docker_manifests: - name_template: ghcr.io/estahn/k8s-image-swapper:latest image_templates: - ghcr.io/estahn/k8s-image-swapper:latest-amd64 - ghcr.io/estahn/k8s-image-swapper:latest-arm64v8 - name_template: ghcr.io/estahn/k8s-image-swapper:{{ .Version }} image_templates: - ghcr.io/estahn/k8s-image-swapper:{{ .Version }}-amd64 - ghcr.io/estahn/k8s-image-swapper:{{ .Version }}-arm64v8 - name_template: ghcr.io/estahn/k8s-image-swapper:{{ .Major }}.{{ .Minor }}.{{ .Patch }} image_templates: - ghcr.io/estahn/k8s-image-swapper:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-amd64 - ghcr.io/estahn/k8s-image-swapper:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-arm64v8 - name_template: ghcr.io/estahn/k8s-image-swapper:{{ .Major }}.{{ .Minor }} image_templates: - ghcr.io/estahn/k8s-image-swapper:{{ .Major }}.{{ .Minor }}-amd64 - ghcr.io/estahn/k8s-image-swapper:{{ .Major }}.{{ .Minor }}-arm64v8 - name_template: ghcr.io/estahn/k8s-image-swapper:{{ .Major }} image_templates: - ghcr.io/estahn/k8s-image-swapper:{{ .Major }}-amd64 - ghcr.io/estahn/k8s-image-swapper:{{ .Major }}-arm64v8 release: prerelease: auto changelog: filters: exclude: - '^docs:' - '^chore:' archives: - format: binary ================================================ FILE: .k8s-image-swapper.yml ================================================ dryRun: true logLevel: trace logFormat: console # imageSwapPolicy defines the mutation strategy used by the webhook. # - always: Will always swap the image regardless of the image existence in the target registry. # This can result in pods ending in state ImagePullBack if images fail to be copied to the target registry. # - exists: Only swaps the image if it exits in the target registry. # This can result in pods pulling images from the source registry, e.g. the first pod pulls # from source registry, subsequent pods pull from target registry. imageSwapPolicy: exists # imageCopyPolicy defines the image copy strategy used by the webhook. # - delayed: Submits the copy job to a process queue and moves on. # - immediate: Submits the copy job to a process queue and waits for it to finish (deadline 8s). # - force: Attempts to immediately copy the image (deadline 8s). # - none: Do not copy the image. imageCopyPolicy: delayed source: # Filters provide control over what pods will be processed. # By default all pods will be processed. If a condition matches, the pod will NOT be processed. # For query language details see https://jmespath.org/ filters: # Do not process if namespace equals "kube-system" - jmespath: "obj.metadata.namespace == 'kube-system'" # Only process if namespace equals "playground" #- jmespath: "obj.metadata.namespace != 'playground'" # Only process if namespace ends with "-dev" #- jmespath: "ends_with(obj.metadata.namespace,'-dev')" # registries: # dockerio: # username: # password: target: type: aws aws: accountId: 123456789 region: ap-southeast-2 role: arn:aws:iam::123456789012:role/roleName ecrOptions: tags: - key: CreatedBy value: k8s-image-swapper - key: AnotherTag value: another-tag imageTagMutability: MUTABLE imageScanningConfiguration: imageScanOnPush: true encryptionConfiguration: encryptionType: AES256 kmsKey: string accessPolicy: | { "Version": "2008-10-17", "Statement": [ { "Sid": "AllowCrossAccountPull", "Effect": "Allow", "Principal": { "AWS": "*" }, "Action": [ "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", "ecr:BatchCheckLayerAvailability" ], "Condition": { "StringEquals": { "aws:PrincipalOrgID": [ "o-xxxxxxxx" ] } } } ] } lifecyclePolicy: | { "rules": [ { "rulePriority": 1, "description": "Rule 1", "selection": { "tagStatus": "any", "countType": "imageCountMoreThan", "countNumber": 1 }, "action": { "type": "expire" } } ] } # dockerio: # quayio: ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: 38b88246ccc552bffaaf54259d064beeee434539 # v4.0.1 hooks: - id: trailing-whitespace - id: check-added-large-files - id: check-json - id: pretty-format-json args: ['--autofix'] exclude: package-lock.json - id: check-merge-conflict - id: check-symlinks - id: check-yaml exclude: 'hack/charts/.*\.yaml' - id: detect-private-key - id: check-merge-conflict - id: check-executables-have-shebangs - id: end-of-file-fixer - id: mixed-line-ending #- repo: https://github.com/thlorenz/doctoc # rev: v2.0.0 # hooks: # - id: doctoc # args: ['--title', '## Table of Contents'] - repo: https://github.com/golangci/golangci-lint rev: v1.55.2 hooks: - id: golangci-lint args: ['--timeout', '5m'] - repo: https://github.com/dnephin/pre-commit-golang rev: ac0f6582d2484b3aa90b05d568e70f9f3c1374c7 # v0.0.17 hooks: - id: go-mod-tidy - id: go-fmt - id: go-imports ================================================ FILE: .releaserc ================================================ --- #verifyConditions: ['@semantic-release/github'] #prepare: [] #publish: ['@semantic-release/github'] #success: ['@semantic-release/github'] #fail: ['@semantic-release/github'] plugins: - "@semantic-release/commit-analyzer" - "@semantic-release/release-notes-generator" - "@semantic-release/changelog" - "@semantic-release/github" - "@semantic-release/git" - - "@semantic-release/exec" - generateNotesCmd: | echo "${nextRelease.notes}" > /tmp/release-notes.md verifyReleaseCmd: | echo "${nextRelease.version}" > /tmp/next-release-version.txt branch: main branches: - '+([0-9])?(.{+([0-9]),x}).x' - 'main' - 'next' - 'next-major' - {name: 'beta', prerelease: true} - {name: 'alpha', prerelease: true} analyzeCommits: - path: "@semantic-release/commit-analyzer" releaseRules: - type: "build" scope: "deps" release: "patch" generateNotes: - path: "@semantic-release/release-notes-generator" preset: "conventionalcommits" presetConfig: types: - { type: 'feat', section: ':tada: Features' } - { type: 'feature', section: ':tada: Features' } - { type: 'fix', section: ':bug: Bug Fixes' } - { type: 'perf', section: ':zap: Performance Improvements' } - { type: 'revert', section: ':rewind: Reverts' } - { type: 'docs', section: ':memo: Documentation', hidden: false } - { type: 'style', section: 'Styles', hidden: true } - { type: 'chore', section: 'Miscellaneous Chores', hidden: true } - { type: 'refactor', section: 'Code Refactoring', hidden: true } - { type: 'test', section: ':test_tube: Tests', hidden: true } - { type: 'build', scope: 'deps', section: ':arrow_up: Dependencies' } - { type: 'build', section: ':construction_worker: Build System' } - { type: 'ci', section: 'Continuous Integration', hidden: true } ================================================ FILE: CHANGELOG.md ================================================ ## [1.4.0](https://github.com/estahn/k8s-image-swapper/compare/v1.3.3...v1.4.0) (2023-01-01) ### :construction_worker: Build System * **deps-dev:** Bump @semantic-release/changelog from 6.0.1 to 6.0.2 ([#404](https://github.com/estahn/k8s-image-swapper/issues/404)) ([ee56dbc](https://github.com/estahn/k8s-image-swapper/commit/ee56dbc0e352a191e861d1f77839209e561cfd15)), closes [#276](https://github.com/estahn/k8s-image-swapper/issues/276) [#276](https://github.com/estahn/k8s-image-swapper/issues/276) [#272](https://github.com/estahn/k8s-image-swapper/issues/272) [#275](https://github.com/estahn/k8s-image-swapper/issues/275) [#273](https://github.com/estahn/k8s-image-swapper/issues/273) [#274](https://github.com/estahn/k8s-image-swapper/issues/274) [#271](https://github.com/estahn/k8s-image-swapper/issues/271) [#270](https://github.com/estahn/k8s-image-swapper/issues/270) [#269](https://github.com/estahn/k8s-image-swapper/issues/269) [#268](https://github.com/estahn/k8s-image-swapper/issues/268) [#267](https://github.com/estahn/k8s-image-swapper/issues/267) ### :tada: Features * add custom tags to created ECR repositories ([#191](https://github.com/estahn/k8s-image-swapper/issues/191)) ([9849df2](https://github.com/estahn/k8s-image-swapper/commit/9849df2e6f706ebd48c629b3bb1bf6ddb91faf32)) ### :memo: Documentation * fix indentation ([b00c57e](https://github.com/estahn/k8s-image-swapper/commit/b00c57ea13a7827458acaf5d2f8c5833d7dfc19d)) ### :arrow_up: Dependencies * **deps:** Bump actions/cache from 3.0.11 to 3.2.1 ([#417](https://github.com/estahn/k8s-image-swapper/issues/417)) ([7e7eb8f](https://github.com/estahn/k8s-image-swapper/commit/7e7eb8f9658fe5e890ec52dc3e30a2f2cf645fe9)), closes [actions/cache#1039](https://github.com/actions/cache/issues/1039) [actions/cache#1023](https://github.com/actions/cache/issues/1023) [actions/cache#959](https://github.com/actions/cache/issues/959) [actions/cache#960](https://github.com/actions/cache/issues/960) [actions/cache#963](https://github.com/actions/cache/issues/963) [actions/cache#961](https://github.com/actions/cache/issues/961) [actions/cache#976](https://github.com/actions/cache/issues/976) [actions/cache#971](https://github.com/actions/cache/issues/971) [actions/cache#979](https://github.com/actions/cache/issues/979) [actions/cache#986](https://github.com/actions/cache/issues/986) [actions/cache#981](https://github.com/actions/cache/issues/981) [actions/cache#997](https://github.com/actions/cache/issues/997) [actions/cache#998](https://github.com/actions/cache/issues/998) [actions/cache#1005](https://github.com/actions/cache/issues/1005) [actions/cache#1007](https://github.com/actions/cache/issues/1007) [actions/cache#1013](https://github.com/actions/cache/issues/1013) [actions/cache#1004](https://github.com/actions/cache/issues/1004) [actions/cache#1014](https://github.com/actions/cache/issues/1014) [actions/cache#1008](https://github.com/actions/cache/issues/1008) [actions/cache#1026](https://github.com/actions/cache/issues/1026) [actions/cache#929](https://github.com/actions/cache/issues/929) [actions/cache#1035](https://github.com/actions/cache/issues/1035) [actions/cache#959](https://github.com/actions/cache/issues/959) [actions/cache#979](https://github.com/actions/cache/issues/979) [actions/cache#1013](https://github.com/actions/cache/issues/1013) [actions/cache#1026](https://github.com/actions/cache/issues/1026) [actions/cache#929](https://github.com/actions/cache/issues/929) [actions/cache#1006](https://github.com/actions/cache/issues/1006) [#1023](https://github.com/estahn/k8s-image-swapper/issues/1023) [#1039](https://github.com/estahn/k8s-image-swapper/issues/1039) [#1035](https://github.com/estahn/k8s-image-swapper/issues/1035) [#929](https://github.com/estahn/k8s-image-swapper/issues/929) [#1026](https://github.com/estahn/k8s-image-swapper/issues/1026) [#1008](https://github.com/estahn/k8s-image-swapper/issues/1008) [#1014](https://github.com/estahn/k8s-image-swapper/issues/1014) [#1004](https://github.com/estahn/k8s-image-swapper/issues/1004) * **deps:** Bump actions/setup-python from 4.3.0 to 4.3.1 ([#406](https://github.com/estahn/k8s-image-swapper/issues/406)) ([16da762](https://github.com/estahn/k8s-image-swapper/commit/16da762a8223a7802e931a404b236dc18b19cc33)), closes [actions/setup-python#559](https://github.com/actions/setup-python/issues/559) [actions/setup-python#511](https://github.com/actions/setup-python/issues/511) [actions/setup-python#558](https://github.com/actions/setup-python/issues/558) [#559](https://github.com/estahn/k8s-image-swapper/issues/559) [#558](https://github.com/estahn/k8s-image-swapper/issues/558) [#549](https://github.com/estahn/k8s-image-swapper/issues/549) [#546](https://github.com/estahn/k8s-image-swapper/issues/546) [#545](https://github.com/estahn/k8s-image-swapper/issues/545) [#535](https://github.com/estahn/k8s-image-swapper/issues/535) [#510](https://github.com/estahn/k8s-image-swapper/issues/510) [#511](https://github.com/estahn/k8s-image-swapper/issues/511) [#520](https://github.com/estahn/k8s-image-swapper/issues/520) * **deps:** Bump actions/setup-python from 4.3.1 to 4.4.0 ([#418](https://github.com/estahn/k8s-image-swapper/issues/418)) ([77872f8](https://github.com/estahn/k8s-image-swapper/commit/77872f801aee9b15d1e878a927372dc5bcf49089)), closes [actions/setup-python#566](https://github.com/actions/setup-python/issues/566) [#567](https://github.com/estahn/k8s-image-swapper/issues/567) [#569](https://github.com/estahn/k8s-image-swapper/issues/569) [#566](https://github.com/estahn/k8s-image-swapper/issues/566) * **deps:** Bump github.com/aws/aws-sdk-go from 1.44.146 to 1.44.152 ([#403](https://github.com/estahn/k8s-image-swapper/issues/403)) ([9db51fd](https://github.com/estahn/k8s-image-swapper/commit/9db51fd866049c99d1a6d86cfb4f91570a10cad7)), closes [#4652](https://github.com/estahn/k8s-image-swapper/issues/4652) [#4650](https://github.com/estahn/k8s-image-swapper/issues/4650) [#4648](https://github.com/estahn/k8s-image-swapper/issues/4648) [#4647](https://github.com/estahn/k8s-image-swapper/issues/4647) [#4646](https://github.com/estahn/k8s-image-swapper/issues/4646) [#4644](https://github.com/estahn/k8s-image-swapper/issues/4644) [#4639](https://github.com/estahn/k8s-image-swapper/issues/4639) * **deps:** Bump github.com/aws/aws-sdk-go from 1.44.152 to 1.44.157 ([#411](https://github.com/estahn/k8s-image-swapper/issues/411)) ([2188432](https://github.com/estahn/k8s-image-swapper/commit/218843218c86f16f85546a037bd976841f220a82)), closes [#4658](https://github.com/estahn/k8s-image-swapper/issues/4658) [#4657](https://github.com/estahn/k8s-image-swapper/issues/4657) [#4656](https://github.com/estahn/k8s-image-swapper/issues/4656) [#4654](https://github.com/estahn/k8s-image-swapper/issues/4654) [#4653](https://github.com/estahn/k8s-image-swapper/issues/4653) * **deps:** Bump github.com/aws/aws-sdk-go from 1.44.157 to 1.44.162 ([#415](https://github.com/estahn/k8s-image-swapper/issues/415)) ([f70fcd9](https://github.com/estahn/k8s-image-swapper/commit/f70fcd98419f805c8314070fb73ca9a6122ad7e2)), closes [#4666](https://github.com/estahn/k8s-image-swapper/issues/4666) [#4665](https://github.com/estahn/k8s-image-swapper/issues/4665) [#4663](https://github.com/estahn/k8s-image-swapper/issues/4663) [#4661](https://github.com/estahn/k8s-image-swapper/issues/4661) [#4660](https://github.com/estahn/k8s-image-swapper/issues/4660) * **deps:** Bump github.com/aws/aws-sdk-go from 1.44.162 to 1.44.167 ([#419](https://github.com/estahn/k8s-image-swapper/issues/419)) ([f8b91fe](https://github.com/estahn/k8s-image-swapper/commit/f8b91fe6cbca3ff9a69a9c26e156eadbb8092336)), closes [#4671](https://github.com/estahn/k8s-image-swapper/issues/4671) [#4670](https://github.com/estahn/k8s-image-swapper/issues/4670) [#4669](https://github.com/estahn/k8s-image-swapper/issues/4669) [#4668](https://github.com/estahn/k8s-image-swapper/issues/4668) [#4667](https://github.com/estahn/k8s-image-swapper/issues/4667) * **deps:** Bump github.com/gruntwork-io/terratest from 0.41.3 to 0.41.4 ([#402](https://github.com/estahn/k8s-image-swapper/issues/402)) ([16dde07](https://github.com/estahn/k8s-image-swapper/commit/16dde0720dabfa966199f8cd4d57e2de65aaeee3)), closes [#1208](https://github.com/estahn/k8s-image-swapper/issues/1208) * **deps:** Bump github.com/gruntwork-io/terratest from 0.41.4 to 0.41.6 ([#409](https://github.com/estahn/k8s-image-swapper/issues/409)) ([9fc87df](https://github.com/estahn/k8s-image-swapper/commit/9fc87df1d6cd4f1979ef64346c88e1601e81855e)), closes [#1214](https://github.com/estahn/k8s-image-swapper/issues/1214) [#1198](https://github.com/estahn/k8s-image-swapper/issues/1198) * **deps:** Bump github.com/gruntwork-io/terratest from 0.41.6 to 0.41.7 ([#420](https://github.com/estahn/k8s-image-swapper/issues/420)) ([9ab97f2](https://github.com/estahn/k8s-image-swapper/commit/9ab97f2d3e00c1700efeaef20f3de74471d4dc4a)), closes [gruntwork-io/terratest#1217](https://github.com/gruntwork-io/terratest/issues/1217) [#1217](https://github.com/estahn/k8s-image-swapper/issues/1217) * **deps:** Bump goreleaser/goreleaser-action from 3.1.0 to 4.1.0 ([#414](https://github.com/estahn/k8s-image-swapper/issues/414)) ([e963ba1](https://github.com/estahn/k8s-image-swapper/commit/e963ba13406d43412962a025aece512846f85530)), closes [goreleaser/goreleaser-action#382](https://github.com/goreleaser/goreleaser-action/issues/382) [goreleaser/goreleaser-action#366](https://github.com/goreleaser/goreleaser-action/issues/366) [goreleaser/goreleaser-action#379](https://github.com/goreleaser/goreleaser-action/issues/379) [goreleaser/goreleaser-action#383](https://github.com/goreleaser/goreleaser-action/issues/383) [goreleaser/goreleaser-action#366](https://github.com/goreleaser/goreleaser-action/issues/366) [goreleaser/goreleaser-action#379](https://github.com/goreleaser/goreleaser-action/issues/379) [goreleaser/goreleaser-action#370](https://github.com/goreleaser/goreleaser-action/issues/370) [#374](https://github.com/estahn/k8s-image-swapper/issues/374) [#372](https://github.com/estahn/k8s-image-swapper/issues/372) [#373](https://github.com/estahn/k8s-image-swapper/issues/373) [#383](https://github.com/estahn/k8s-image-swapper/issues/383) [#366](https://github.com/estahn/k8s-image-swapper/issues/366) [#382](https://github.com/estahn/k8s-image-swapper/issues/382) [#370](https://github.com/estahn/k8s-image-swapper/issues/370) * **deps:** Bump k8s.io/api from 0.25.4 to 0.26.0 ([#407](https://github.com/estahn/k8s-image-swapper/issues/407)) ([d13bf5e](https://github.com/estahn/k8s-image-swapper/commit/d13bf5e751ed7638fb418add00ed40f1ceb04f07)), closes [#111023](https://github.com/estahn/k8s-image-swapper/issues/111023) [#113375](https://github.com/estahn/k8s-image-swapper/issues/113375) [#113186](https://github.com/estahn/k8s-image-swapper/issues/113186) * **deps:** Bump k8s.io/client-go from 0.25.4 to 0.26.0 ([#410](https://github.com/estahn/k8s-image-swapper/issues/410)) ([bcc56b5](https://github.com/estahn/k8s-image-swapper/commit/bcc56b57ac91c1e8312f4e558283c68684a38678)), closes [#113797](https://github.com/estahn/k8s-image-swapper/issues/113797) [#111023](https://github.com/estahn/k8s-image-swapper/issues/111023) [#113826](https://github.com/estahn/k8s-image-swapper/issues/113826) [#113375](https://github.com/estahn/k8s-image-swapper/issues/113375) ## [1.3.3](https://github.com/estahn/k8s-image-swapper/compare/v1.3.2...v1.3.3) (2022-12-01) ### :arrow_up: Dependencies * **deps:** Bump alpine from 3.16.2 to 3.16.3 ([#388](https://github.com/estahn/k8s-image-swapper/issues/388)) ([ffae497](https://github.com/estahn/k8s-image-swapper/commit/ffae497511dd6fb3adcac748f06684e50474446c)) * **deps:** Bump alpine from 3.16.3 to 3.17.0 ([#395](https://github.com/estahn/k8s-image-swapper/issues/395)) ([d32255d](https://github.com/estahn/k8s-image-swapper/commit/d32255d0b7f0685e837eb2906868b0b7c73bd022)) * **deps:** Bump github.com/aws/aws-sdk-go from 1.44.126 to 1.44.136 ([#391](https://github.com/estahn/k8s-image-swapper/issues/391)) ([61a6ae2](https://github.com/estahn/k8s-image-swapper/commit/61a6ae23f015d7ee0c1c4300a3a7e9a76e2acd09)), closes [#4620](https://github.com/estahn/k8s-image-swapper/issues/4620) [#4619](https://github.com/estahn/k8s-image-swapper/issues/4619) [#4617](https://github.com/estahn/k8s-image-swapper/issues/4617) [#4616](https://github.com/estahn/k8s-image-swapper/issues/4616) [#4615](https://github.com/estahn/k8s-image-swapper/issues/4615) [#4614](https://github.com/estahn/k8s-image-swapper/issues/4614) [#4613](https://github.com/estahn/k8s-image-swapper/issues/4613) [#4611](https://github.com/estahn/k8s-image-swapper/issues/4611) [#4608](https://github.com/estahn/k8s-image-swapper/issues/4608) [#4609](https://github.com/estahn/k8s-image-swapper/issues/4609) * **deps:** Bump github.com/aws/aws-sdk-go from 1.44.136 to 1.44.146 ([#397](https://github.com/estahn/k8s-image-swapper/issues/397)) ([d4a6136](https://github.com/estahn/k8s-image-swapper/commit/d4a61369b50b03dd56c1025557b3b65169beea7e)), closes [#4638](https://github.com/estahn/k8s-image-swapper/issues/4638) [#4636](https://github.com/estahn/k8s-image-swapper/issues/4636) [#4633](https://github.com/estahn/k8s-image-swapper/issues/4633) [#4632](https://github.com/estahn/k8s-image-swapper/issues/4632) [#4630](https://github.com/estahn/k8s-image-swapper/issues/4630) [#4628](https://github.com/estahn/k8s-image-swapper/issues/4628) [#4627](https://github.com/estahn/k8s-image-swapper/issues/4627) [#4626](https://github.com/estahn/k8s-image-swapper/issues/4626) [#4625](https://github.com/estahn/k8s-image-swapper/issues/4625) [#4624](https://github.com/estahn/k8s-image-swapper/issues/4624) * **deps:** Bump github.com/containers/image/v5 from 5.23.0 to 5.23.1 ([#393](https://github.com/estahn/k8s-image-swapper/issues/393)) ([84f4d18](https://github.com/estahn/k8s-image-swapper/commit/84f4d1800f0e0937560963b5bac6ed52ec824182)) * **deps:** Bump github.com/go-co-op/gocron from 1.17.1 to 1.18.0 ([#390](https://github.com/estahn/k8s-image-swapper/issues/390)) ([1750ee9](https://github.com/estahn/k8s-image-swapper/commit/1750ee9ebe3dd7e5455d8a8490b90bcccd019eb8)), closes [go-co-op/gocron#388](https://github.com/go-co-op/gocron/issues/388) [go-co-op/gocron#389](https://github.com/go-co-op/gocron/issues/389) [go-co-op/gocron#392](https://github.com/go-co-op/gocron/issues/392) [go-co-op/gocron#394](https://github.com/go-co-op/gocron/issues/394) [go-co-op/gocron#393](https://github.com/go-co-op/gocron/issues/393) [go-co-op/gocron#392](https://github.com/go-co-op/gocron/issues/392) [go-co-op/gocron#394](https://github.com/go-co-op/gocron/issues/394) [#393](https://github.com/estahn/k8s-image-swapper/issues/393) [#394](https://github.com/estahn/k8s-image-swapper/issues/394) [#392](https://github.com/estahn/k8s-image-swapper/issues/392) [#389](https://github.com/estahn/k8s-image-swapper/issues/389) * **deps:** Bump github.com/gruntwork-io/terratest from 0.40.24 to 0.41.3 ([#398](https://github.com/estahn/k8s-image-swapper/issues/398)) ([ab35b1a](https://github.com/estahn/k8s-image-swapper/commit/ab35b1a31d45e2c3f395a69f8fb1f11aad6485c0)), closes [gruntwork-io/terratest#1203](https://github.com/gruntwork-io/terratest/issues/1203) [gruntwork-io/terratest#1202](https://github.com/gruntwork-io/terratest/issues/1202) [gruntwork-io/terratest#1201](https://github.com/gruntwork-io/terratest/issues/1201) [gruntwork-io/terratest#1199](https://github.com/gruntwork-io/terratest/issues/1199) [gruntwork-io/terratest#1196](https://github.com/gruntwork-io/terratest/issues/1196) [#1202](https://github.com/estahn/k8s-image-swapper/issues/1202) [#1203](https://github.com/estahn/k8s-image-swapper/issues/1203) [#1201](https://github.com/estahn/k8s-image-swapper/issues/1201) [#1199](https://github.com/estahn/k8s-image-swapper/issues/1199) [#1196](https://github.com/estahn/k8s-image-swapper/issues/1196) * **deps:** Bump github.com/prometheus/client_golang from 1.13.0 to 1.13.1 ([#387](https://github.com/estahn/k8s-image-swapper/issues/387)) ([b155a16](https://github.com/estahn/k8s-image-swapper/commit/b155a160cf3f90e3bbbaa8c0a639496d5633072f)), closes [#1146](https://github.com/estahn/k8s-image-swapper/issues/1146) [#1148](https://github.com/estahn/k8s-image-swapper/issues/1148) [#1118](https://github.com/estahn/k8s-image-swapper/issues/1118) [#1146](https://github.com/estahn/k8s-image-swapper/issues/1146) [#1148](https://github.com/estahn/k8s-image-swapper/issues/1148) [#1118](https://github.com/estahn/k8s-image-swapper/issues/1118) [#1157](https://github.com/estahn/k8s-image-swapper/issues/1157) [#1146](https://github.com/estahn/k8s-image-swapper/issues/1146) [#1148](https://github.com/estahn/k8s-image-swapper/issues/1148) [#1118](https://github.com/estahn/k8s-image-swapper/issues/1118) * **deps:** Bump github.com/prometheus/client_golang from 1.13.1 to 1.14.0 ([#392](https://github.com/estahn/k8s-image-swapper/issues/392)) ([af00594](https://github.com/estahn/k8s-image-swapper/commit/af00594c9494182d1b6803efa44bc8d13ca7bad6)), closes [#1150](https://github.com/estahn/k8s-image-swapper/issues/1150) [#1103](https://github.com/estahn/k8s-image-swapper/issues/1103) [prometheus/client_golang#1118](https://github.com/prometheus/client_golang/issues/1118) [prometheus/client_golang#1103](https://github.com/prometheus/client_golang/issues/1103) [prometheus/client_golang#1125](https://github.com/prometheus/client_golang/issues/1125) [prometheus/client_golang#1130](https://github.com/prometheus/client_golang/issues/1130) [prometheus/client_golang#1148](https://github.com/prometheus/client_golang/issues/1148) [prometheus/client_golang#1146](https://github.com/prometheus/client_golang/issues/1146) [prometheus/client_golang#1152](https://github.com/prometheus/client_golang/issues/1152) [#1150](https://github.com/estahn/k8s-image-swapper/issues/1150) [#1103](https://github.com/estahn/k8s-image-swapper/issues/1103) [#1162](https://github.com/estahn/k8s-image-swapper/issues/1162) [#1161](https://github.com/estahn/k8s-image-swapper/issues/1161) [#1160](https://github.com/estahn/k8s-image-swapper/issues/1160) [#1136](https://github.com/estahn/k8s-image-swapper/issues/1136) [#1133](https://github.com/estahn/k8s-image-swapper/issues/1133) [#1150](https://github.com/estahn/k8s-image-swapper/issues/1150) [#1152](https://github.com/estahn/k8s-image-swapper/issues/1152) * **deps:** Bump github.com/spf13/viper from 1.13.0 to 1.14.0 ([#385](https://github.com/estahn/k8s-image-swapper/issues/385)) ([6f79498](https://github.com/estahn/k8s-image-swapper/commit/6f79498631d0382645ab9e1a031f80f130ea55a6)), closes [spf13/viper#1457](https://github.com/spf13/viper/issues/1457) [spf13/viper#1458](https://github.com/spf13/viper/issues/1458) [spf13/viper#1460](https://github.com/spf13/viper/issues/1460) [spf13/viper#1428](https://github.com/spf13/viper/issues/1428) [spf13/viper#1406](https://github.com/spf13/viper/issues/1406) [spf13/viper#1437](https://github.com/spf13/viper/issues/1437) [spf13/viper#1453](https://github.com/spf13/viper/issues/1453) [spf13/viper#1449](https://github.com/spf13/viper/issues/1449) [spf13/viper#1461](https://github.com/spf13/viper/issues/1461) * **deps:** Bump golangci/golangci-lint-action from 3.3.0 to 3.3.1 ([#389](https://github.com/estahn/k8s-image-swapper/issues/389)) ([0b50f7b](https://github.com/estahn/k8s-image-swapper/commit/0b50f7b725bd20f5a2fbe3d7bf74365250de8efd)), closes [golangci/golangci-lint-action#590](https://github.com/golangci/golangci-lint-action/issues/590) [golangci/golangci-lint-action#591](https://github.com/golangci/golangci-lint-action/issues/591) [golangci/golangci-lint-action#592](https://github.com/golangci/golangci-lint-action/issues/592) [golangci/golangci-lint-action#593](https://github.com/golangci/golangci-lint-action/issues/593) [golangci/golangci-lint-action#594](https://github.com/golangci/golangci-lint-action/issues/594) [golangci/golangci-lint-action#595](https://github.com/golangci/golangci-lint-action/issues/595) [golangci/golangci-lint-action#596](https://github.com/golangci/golangci-lint-action/issues/596) [golangci/golangci-lint-action#597](https://github.com/golangci/golangci-lint-action/issues/597) [golangci/golangci-lint-action#598](https://github.com/golangci/golangci-lint-action/issues/598) [golangci/golangci-lint-action#599](https://github.com/golangci/golangci-lint-action/issues/599) [#599](https://github.com/estahn/k8s-image-swapper/issues/599) [#598](https://github.com/estahn/k8s-image-swapper/issues/598) [#596](https://github.com/estahn/k8s-image-swapper/issues/596) [#595](https://github.com/estahn/k8s-image-swapper/issues/595) [#593](https://github.com/estahn/k8s-image-swapper/issues/593) [#591](https://github.com/estahn/k8s-image-swapper/issues/591) [#590](https://github.com/estahn/k8s-image-swapper/issues/590) * **deps:** Bump hmarr/auto-approve-action from 2 to 3 ([#396](https://github.com/estahn/k8s-image-swapper/issues/396)) ([0b982a2](https://github.com/estahn/k8s-image-swapper/commit/0b982a220226bad863a4aa4819a080a343c8b238)), closes [hmarr/auto-approve-action#205](https://github.com/hmarr/auto-approve-action/issues/205) [hmarr/auto-approve-action#202](https://github.com/hmarr/auto-approve-action/issues/202) [hmarr/auto-approve-action#202](https://github.com/hmarr/auto-approve-action/issues/202) [hmarr/auto-approve-action#200](https://github.com/hmarr/auto-approve-action/issues/200) [hmarr/auto-approve-action#200](https://github.com/hmarr/auto-approve-action/issues/200) [hmarr/auto-approve-action#186](https://github.com/hmarr/auto-approve-action/issues/186) [hmarr/auto-approve-action#191](https://github.com/hmarr/auto-approve-action/issues/191) [#210](https://github.com/estahn/k8s-image-swapper/issues/210) [#205](https://github.com/estahn/k8s-image-swapper/issues/205) * **deps:** Bump k8s.io/api from 0.25.3 to 0.25.4 ([#401](https://github.com/estahn/k8s-image-swapper/issues/401)) ([0f80b5d](https://github.com/estahn/k8s-image-swapper/commit/0f80b5d9802cfe9edf69b69e341b3cff5e22f918)) * **deps:** Bump k8s.io/apimachinery from 0.25.3 to 0.25.4 ([#399](https://github.com/estahn/k8s-image-swapper/issues/399)) ([1f0944f](https://github.com/estahn/k8s-image-swapper/commit/1f0944ff59bb3803d7e8e379c32838c937ccbed8)), closes [#112218](https://github.com/estahn/k8s-image-swapper/issues/112218) [haoruan/automated-cherry-pick-of-#111936](https://github.com/haoruan/automated-cherry-pick-of-/issues/111936) * **deps:** Bump k8s.io/client-go from 0.25.3 to 0.25.4 ([#400](https://github.com/estahn/k8s-image-swapper/issues/400)) ([ad036e0](https://github.com/estahn/k8s-image-swapper/commit/ad036e08999cf58a5c9bd3300d63222c4a1b48e1)) ## [1.3.2](https://github.com/estahn/k8s-image-swapper/compare/v1.3.1...v1.3.2) (2022-11-01) ### :arrow_up: Dependencies * **deps:** Bump actions/cache from 3.0.10 to 3.0.11 ([#365](https://github.com/estahn/k8s-image-swapper/issues/365)) ([4e88994](https://github.com/estahn/k8s-image-swapper/commit/4e88994fed8d51b8e21d8c0e5bdeef8edfcf6edb)), closes [actions/cache#946](https://github.com/actions/cache/issues/946) [actions/cache#950](https://github.com/actions/cache/issues/950) [actions/cache#956](https://github.com/actions/cache/issues/956) [actions/cache#950](https://github.com/actions/cache/issues/950) [#956](https://github.com/estahn/k8s-image-swapper/issues/956) [#950](https://github.com/estahn/k8s-image-swapper/issues/950) [#946](https://github.com/estahn/k8s-image-swapper/issues/946) * **deps:** bump actions/cache from 3.0.8 to 3.0.10 ([#358](https://github.com/estahn/k8s-image-swapper/issues/358)) ([921d9e2](https://github.com/estahn/k8s-image-swapper/commit/921d9e2df56e860ca2b09c87ed874bce4525260e)), closes [#809](https://github.com/estahn/k8s-image-swapper/issues/809) [#833](https://github.com/estahn/k8s-image-swapper/issues/833) [#810](https://github.com/estahn/k8s-image-swapper/issues/810) [#931](https://github.com/estahn/k8s-image-swapper/issues/931) [#942](https://github.com/estahn/k8s-image-swapper/issues/942) [#930](https://github.com/estahn/k8s-image-swapper/issues/930) [#920](https://github.com/estahn/k8s-image-swapper/issues/920) [#936](https://github.com/estahn/k8s-image-swapper/issues/936) [#925](https://github.com/estahn/k8s-image-swapper/issues/925) * **deps:** Bump actions/setup-python from 4.2.0 to 4.3.0 ([#362](https://github.com/estahn/k8s-image-swapper/issues/362)) ([05a7ab3](https://github.com/estahn/k8s-image-swapper/commit/05a7ab3cd2df647927fdd85a159dc21dbe181cc9)), closes [#517](https://github.com/estahn/k8s-image-swapper/issues/517) [#499](https://github.com/estahn/k8s-image-swapper/issues/499) [#443](https://github.com/estahn/k8s-image-swapper/issues/443) [#477](https://github.com/estahn/k8s-image-swapper/issues/477) [#479](https://github.com/estahn/k8s-image-swapper/issues/479) [#491](https://github.com/estahn/k8s-image-swapper/issues/491) [#492](https://github.com/estahn/k8s-image-swapper/issues/492) [#517](https://github.com/estahn/k8s-image-swapper/issues/517) [#503](https://github.com/estahn/k8s-image-swapper/issues/503) [#499](https://github.com/estahn/k8s-image-swapper/issues/499) [#495](https://github.com/estahn/k8s-image-swapper/issues/495) [#443](https://github.com/estahn/k8s-image-swapper/issues/443) [#492](https://github.com/estahn/k8s-image-swapper/issues/492) [#491](https://github.com/estahn/k8s-image-swapper/issues/491) * **deps:** Bump docker/login-action from 2.0.0 to 2.1.0 ([#364](https://github.com/estahn/k8s-image-swapper/issues/364)) ([ca6a535](https://github.com/estahn/k8s-image-swapper/commit/ca6a535e78ddc3bba389b1454c4ae6af7c41104b)), closes [#275](https://github.com/estahn/k8s-image-swapper/issues/275) [#252](https://github.com/estahn/k8s-image-swapper/issues/252) [#292](https://github.com/estahn/k8s-image-swapper/issues/292) [#298](https://github.com/estahn/k8s-image-swapper/issues/298) [#299](https://github.com/estahn/k8s-image-swapper/issues/299) [#299](https://github.com/estahn/k8s-image-swapper/issues/299) [#298](https://github.com/estahn/k8s-image-swapper/issues/298) [#292](https://github.com/estahn/k8s-image-swapper/issues/292) [#275](https://github.com/estahn/k8s-image-swapper/issues/275) * **deps:** Bump github.com/alitto/pond from 1.8.1 to 1.8.2 ([#371](https://github.com/estahn/k8s-image-swapper/issues/371)) ([417edd5](https://github.com/estahn/k8s-image-swapper/commit/417edd5f35b4194c78f0ff28ba26cb4930202f1f)), closes [alitto/pond#37](https://github.com/alitto/pond/issues/37) [#37](https://github.com/estahn/k8s-image-swapper/issues/37) * **deps:** bump github.com/aws/aws-sdk-go from 1.44.100 to 1.44.109 ([#359](https://github.com/estahn/k8s-image-swapper/issues/359)) ([b9acd7d](https://github.com/estahn/k8s-image-swapper/commit/b9acd7d6abaa0d893d084d39655fa6e9c7c6ee03)), closes [#4574](https://github.com/estahn/k8s-image-swapper/issues/4574) [#4573](https://github.com/estahn/k8s-image-swapper/issues/4573) [#4571](https://github.com/estahn/k8s-image-swapper/issues/4571) [#4568](https://github.com/estahn/k8s-image-swapper/issues/4568) [#4567](https://github.com/estahn/k8s-image-swapper/issues/4567) [#4566](https://github.com/estahn/k8s-image-swapper/issues/4566) [#4565](https://github.com/estahn/k8s-image-swapper/issues/4565) [#4562](https://github.com/estahn/k8s-image-swapper/issues/4562) [#4561](https://github.com/estahn/k8s-image-swapper/issues/4561) * **deps:** Bump github.com/aws/aws-sdk-go from 1.44.109 to 1.44.114 ([#363](https://github.com/estahn/k8s-image-swapper/issues/363)) ([832a21d](https://github.com/estahn/k8s-image-swapper/commit/832a21d5a4911b73adec3c3922691e64dabeb367)), closes [#4580](https://github.com/estahn/k8s-image-swapper/issues/4580) [#4579](https://github.com/estahn/k8s-image-swapper/issues/4579) [#4578](https://github.com/estahn/k8s-image-swapper/issues/4578) [#4576](https://github.com/estahn/k8s-image-swapper/issues/4576) [#4575](https://github.com/estahn/k8s-image-swapper/issues/4575) * **deps:** Bump github.com/aws/aws-sdk-go from 1.44.114 to 1.44.121 ([#377](https://github.com/estahn/k8s-image-swapper/issues/377)) ([26f4103](https://github.com/estahn/k8s-image-swapper/commit/26f410377e56ddd55a3b8f204422492ecb96d45c)), closes [#4596](https://github.com/estahn/k8s-image-swapper/issues/4596) [#4595](https://github.com/estahn/k8s-image-swapper/issues/4595) [#4593](https://github.com/estahn/k8s-image-swapper/issues/4593) [#4519](https://github.com/estahn/k8s-image-swapper/issues/4519) [#4590](https://github.com/estahn/k8s-image-swapper/issues/4590) [#4589](https://github.com/estahn/k8s-image-swapper/issues/4589) [#4587](https://github.com/estahn/k8s-image-swapper/issues/4587) [#4586](https://github.com/estahn/k8s-image-swapper/issues/4586) * **deps:** Bump github.com/aws/aws-sdk-go from 1.44.121 to 1.44.126 ([#383](https://github.com/estahn/k8s-image-swapper/issues/383)) ([b7e43d9](https://github.com/estahn/k8s-image-swapper/commit/b7e43d9312709eb9e81e1c941d0915776ca73c10)), closes [#4603](https://github.com/estahn/k8s-image-swapper/issues/4603) [#4602](https://github.com/estahn/k8s-image-swapper/issues/4602) [#4601](https://github.com/estahn/k8s-image-swapper/issues/4601) [#4600](https://github.com/estahn/k8s-image-swapper/issues/4600) [#4598](https://github.com/estahn/k8s-image-swapper/issues/4598) * **deps:** bump github.com/containers/image/v5 from 5.22.0 to 5.23.0 ([#360](https://github.com/estahn/k8s-image-swapper/issues/360)) ([250d9e4](https://github.com/estahn/k8s-image-swapper/commit/250d9e4c6c4af0ad80d185e3e28bc72f3bc2a9e2)), closes [#1665](https://github.com/estahn/k8s-image-swapper/issues/1665) [#1666](https://github.com/estahn/k8s-image-swapper/issues/1666) [#1664](https://github.com/estahn/k8s-image-swapper/issues/1664) [#1662](https://github.com/estahn/k8s-image-swapper/issues/1662) * **deps:** Bump github.com/dgraph-io/ristretto from 0.1.0 to 0.1.1 ([#368](https://github.com/estahn/k8s-image-swapper/issues/368)) ([0e9c9df](https://github.com/estahn/k8s-image-swapper/commit/0e9c9df74847dea68668137677391541df4a9977)), closes [#285](https://github.com/estahn/k8s-image-swapper/issues/285) [dgraph-io/ristretto#311](https://github.com/dgraph-io/ristretto/issues/311) [#304](https://github.com/estahn/k8s-image-swapper/issues/304) [dgraph-io/ristretto#304](https://github.com/dgraph-io/ristretto/issues/304) [#287](https://github.com/estahn/k8s-image-swapper/issues/287) [dgraph-io/ristretto#307](https://github.com/dgraph-io/ristretto/issues/307) [#285](https://github.com/estahn/k8s-image-swapper/issues/285) [dgraph-io/ristretto#311](https://github.com/dgraph-io/ristretto/issues/311) [#304](https://github.com/estahn/k8s-image-swapper/issues/304) [dgraph-io/ristretto#304](https://github.com/dgraph-io/ristretto/issues/304) [#287](https://github.com/estahn/k8s-image-swapper/issues/287) [dgraph-io/ristretto#307](https://github.com/dgraph-io/ristretto/issues/307) [#312](https://github.com/estahn/k8s-image-swapper/issues/312) [#285](https://github.com/estahn/k8s-image-swapper/issues/285) [#310](https://github.com/estahn/k8s-image-swapper/issues/310) [#306](https://github.com/estahn/k8s-image-swapper/issues/306) [#309](https://github.com/estahn/k8s-image-swapper/issues/309) [#308](https://github.com/estahn/k8s-image-swapper/issues/308) [#287](https://github.com/estahn/k8s-image-swapper/issues/287) [#307](https://github.com/estahn/k8s-image-swapper/issues/307) [#304](https://github.com/estahn/k8s-image-swapper/issues/304) * **deps:** Bump github.com/go-co-op/gocron from 1.17.0 to 1.17.1 ([#380](https://github.com/estahn/k8s-image-swapper/issues/380)) ([8c4cef8](https://github.com/estahn/k8s-image-swapper/commit/8c4cef891249bc5e9b699e4df492eb343ee005b6)), closes [go-co-op/gocron#382](https://github.com/go-co-op/gocron/issues/382) [go-co-op/gocron#386](https://github.com/go-co-op/gocron/issues/386) [go-co-op/gocron#386](https://github.com/go-co-op/gocron/issues/386) [#386](https://github.com/estahn/k8s-image-swapper/issues/386) [#382](https://github.com/estahn/k8s-image-swapper/issues/382) * **deps:** Bump github.com/gruntwork-io/terratest from 0.40.22 to 0.40.23 ([#367](https://github.com/estahn/k8s-image-swapper/issues/367)) ([a07149b](https://github.com/estahn/k8s-image-swapper/commit/a07149b44a5814d06121de06cf117c8cb1baedc2)), closes [gruntwork-io/terratest#1186](https://github.com/gruntwork-io/terratest/issues/1186) [gruntwork-io/terratest#1189](https://github.com/gruntwork-io/terratest/issues/1189) [#1189](https://github.com/estahn/k8s-image-swapper/issues/1189) [#1186](https://github.com/estahn/k8s-image-swapper/issues/1186) * **deps:** Bump github.com/gruntwork-io/terratest from 0.40.23 to 0.40.24 ([#378](https://github.com/estahn/k8s-image-swapper/issues/378)) ([e57939d](https://github.com/estahn/k8s-image-swapper/commit/e57939daf1efa9d3c9e879c5468a7e6451393c69)), closes [gruntwork-io/terratest#1191](https://github.com/gruntwork-io/terratest/issues/1191) [#1191](https://github.com/estahn/k8s-image-swapper/issues/1191) * **deps:** Bump github.com/slok/kubewebhook/v2 from 2.3.0 to 2.5.0 ([#372](https://github.com/estahn/k8s-image-swapper/issues/372)) ([249a996](https://github.com/estahn/k8s-image-swapper/commit/249a9961d438d4057e74cca8f5358640a836d521)), closes [#218](https://github.com/estahn/k8s-image-swapper/issues/218) [#217](https://github.com/estahn/k8s-image-swapper/issues/217) [#187](https://github.com/estahn/k8s-image-swapper/issues/187) * **deps:** Bump github.com/spf13/cobra from 1.5.0 to 1.6.0 ([#373](https://github.com/estahn/k8s-image-swapper/issues/373)) ([39cfd45](https://github.com/estahn/k8s-image-swapper/commit/39cfd45c188b19fc48ccb06b68542089da2440ca)), closes [#1003](https://github.com/estahn/k8s-image-swapper/issues/1003) [#1802](https://github.com/estahn/k8s-image-swapper/issues/1802) [#1760](https://github.com/estahn/k8s-image-swapper/issues/1760) [#1707](https://github.com/estahn/k8s-image-swapper/issues/1707) [#1813](https://github.com/estahn/k8s-image-swapper/issues/1813) [#1788](https://github.com/estahn/k8s-image-swapper/issues/1788) [#1621](https://github.com/estahn/k8s-image-swapper/issues/1621) [#1467](https://github.com/estahn/k8s-image-swapper/issues/1467) [#1643](https://github.com/estahn/k8s-image-swapper/issues/1643) [#1643](https://github.com/estahn/k8s-image-swapper/issues/1643) [#1762](https://github.com/estahn/k8s-image-swapper/issues/1762) [#1771](https://github.com/estahn/k8s-image-swapper/issues/1771) [#1776](https://github.com/estahn/k8s-image-swapper/issues/1776) [#1766](https://github.com/estahn/k8s-image-swapper/issues/1766) [#1782](https://github.com/estahn/k8s-image-swapper/issues/1782) [#1803](https://github.com/estahn/k8s-image-swapper/issues/1803) [#1783](https://github.com/estahn/k8s-image-swapper/issues/1783) [#1387](https://github.com/estahn/k8s-image-swapper/issues/1387) [#1792](https://github.com/estahn/k8s-image-swapper/issues/1792) [#1744](https://github.com/estahn/k8s-image-swapper/issues/1744) [#1748](https://github.com/estahn/k8s-image-swapper/issues/1748) [#1726](https://github.com/estahn/k8s-image-swapper/issues/1726) [#1656](https://github.com/estahn/k8s-image-swapper/issues/1656) [#1779](https://github.com/estahn/k8s-image-swapper/issues/1779) [#1741](https://github.com/estahn/k8s-image-swapper/issues/1741) [#1742](https://github.com/estahn/k8s-image-swapper/issues/1742) [#1745](https://github.com/estahn/k8s-image-swapper/issues/1745) [#1759](https://github.com/estahn/k8s-image-swapper/issues/1759) [#1772](https://github.com/estahn/k8s-image-swapper/issues/1772) [#1819](https://github.com/estahn/k8s-image-swapper/issues/1819) [#1800](https://github.com/estahn/k8s-image-swapper/issues/1800) [#1809](https://github.com/estahn/k8s-image-swapper/issues/1809) [#1804](https://github.com/estahn/k8s-image-swapper/issues/1804) [#1467](https://github.com/estahn/k8s-image-swapper/issues/1467) [#1003](https://github.com/estahn/k8s-image-swapper/issues/1003) [#1813](https://github.com/estahn/k8s-image-swapper/issues/1813) [#1621](https://github.com/estahn/k8s-image-swapper/issues/1621) [#1792](https://github.com/estahn/k8s-image-swapper/issues/1792) [#1788](https://github.com/estahn/k8s-image-swapper/issues/1788) [#1815](https://github.com/estahn/k8s-image-swapper/issues/1815) [#1819](https://github.com/estahn/k8s-image-swapper/issues/1819) [#1707](https://github.com/estahn/k8s-image-swapper/issues/1707) [#1760](https://github.com/estahn/k8s-image-swapper/issues/1760) * **deps:** Bump github.com/spf13/cobra from 1.6.0 to 1.6.1 ([#384](https://github.com/estahn/k8s-image-swapper/issues/384)) ([ffe3ef6](https://github.com/estahn/k8s-image-swapper/commit/ffe3ef6a3ff0f789234b4627ae472047477518dc)), closes [#1839](https://github.com/estahn/k8s-image-swapper/issues/1839) [#1841](https://github.com/estahn/k8s-image-swapper/issues/1841) * **deps:** Bump github.com/stretchr/testify from 1.8.0 to 1.8.1 ([#379](https://github.com/estahn/k8s-image-swapper/issues/379)) ([a0e1429](https://github.com/estahn/k8s-image-swapper/commit/a0e1429c3c74ff5c7a69d1949de7f220c04feac0)), closes [#1283](https://github.com/estahn/k8s-image-swapper/issues/1283) * **deps:** Bump golangci/golangci-lint-action from 3.2.0 to 3.3.0 ([#375](https://github.com/estahn/k8s-image-swapper/issues/375)) ([abcf765](https://github.com/estahn/k8s-image-swapper/commit/abcf765c74573a98f3c6956c8ce6b087f8a6b8d0)), closes [#586](https://github.com/estahn/k8s-image-swapper/issues/586) [#584](https://github.com/estahn/k8s-image-swapper/issues/584) [#582](https://github.com/estahn/k8s-image-swapper/issues/582) [#580](https://github.com/estahn/k8s-image-swapper/issues/580) [#578](https://github.com/estahn/k8s-image-swapper/issues/578) [#576](https://github.com/estahn/k8s-image-swapper/issues/576) [#577](https://github.com/estahn/k8s-image-swapper/issues/577) [#575](https://github.com/estahn/k8s-image-swapper/issues/575) * **deps:** Bump k8s.io/api from 0.25.1 to 0.25.3 ([#366](https://github.com/estahn/k8s-image-swapper/issues/366)) ([c233527](https://github.com/estahn/k8s-image-swapper/commit/c23352795c55ed3d9897015c0d127163648302b8)), closes [#112808](https://github.com/estahn/k8s-image-swapper/issues/112808) [cheftako/automated-cherry-pick-of-#112689](https://github.com/cheftako/automated-cherry-pick-of-/issues/112689) ## [1.3.1](https://github.com/estahn/k8s-image-swapper/compare/v1.3.0...v1.3.1) (2022-10-01) ### :bug: Bug Fixes * set verbose level & use structured logging ([#346](https://github.com/estahn/k8s-image-swapper/issues/346)) ([9b21320](https://github.com/estahn/k8s-image-swapper/commit/9b21320a52d3f74ae4a6e8233cc3e310d2f5136b)) ### :arrow_up: Dependencies * **deps:** bump github.com/aws/aws-sdk-go from 1.44.92 to 1.44.95 ([#349](https://github.com/estahn/k8s-image-swapper/issues/349)) ([609e915](https://github.com/estahn/k8s-image-swapper/commit/609e91566628b2c89ee0f3a6f582993cb7df8154)), closes [#4553](https://github.com/estahn/k8s-image-swapper/issues/4553) [#4551](https://github.com/estahn/k8s-image-swapper/issues/4551) [#4550](https://github.com/estahn/k8s-image-swapper/issues/4550) * **deps:** bump github.com/aws/aws-sdk-go from 1.44.95 to 1.44.100 ([#351](https://github.com/estahn/k8s-image-swapper/issues/351)) ([c4aba7d](https://github.com/estahn/k8s-image-swapper/commit/c4aba7dd91b6128c4b6b70b52a3587d81a1b439f)), closes [#4560](https://github.com/estahn/k8s-image-swapper/issues/4560) [#4559](https://github.com/estahn/k8s-image-swapper/issues/4559) [#4558](https://github.com/estahn/k8s-image-swapper/issues/4558) [#4556](https://github.com/estahn/k8s-image-swapper/issues/4556) [#4555](https://github.com/estahn/k8s-image-swapper/issues/4555) * **deps:** bump github.com/gruntwork-io/terratest from 0.40.21 to 0.40.22 ([#348](https://github.com/estahn/k8s-image-swapper/issues/348)) ([b3fa94d](https://github.com/estahn/k8s-image-swapper/commit/b3fa94df956a05796d8fd396462d0bb6987c8f11)), closes [#1169](https://github.com/estahn/k8s-image-swapper/issues/1169) * **deps:** bump k8s.io/api from 0.25.0 to 0.25.1 ([#350](https://github.com/estahn/k8s-image-swapper/issues/350)) ([e1b358a](https://github.com/estahn/k8s-image-swapper/commit/e1b358aa28abacbf4e2c125032871d9db6fab401)), closes [#112161](https://github.com/estahn/k8s-image-swapper/issues/112161) [pohly/automated-cherry-pick-of-#112129](https://github.com/pohly/automated-cherry-pick-of-/issues/112129) * **deps:** bump k8s.io/apimachinery from 0.25.0 to 0.25.1 ([#352](https://github.com/estahn/k8s-image-swapper/issues/352)) ([046ad1e](https://github.com/estahn/k8s-image-swapper/commit/046ad1e07924a4b4e797e5984262ef09872e5e50)), closes [#112330](https://github.com/estahn/k8s-image-swapper/issues/112330) [enj/automated-cherry-pick-of-#112193](https://github.com/enj/automated-cherry-pick-of-/issues/112193) [#112161](https://github.com/estahn/k8s-image-swapper/issues/112161) [pohly/automated-cherry-pick-of-#112129](https://github.com/pohly/automated-cherry-pick-of-/issues/112129) * **deps:** bump k8s.io/client-go from 0.25.0 to 0.25.1 ([#353](https://github.com/estahn/k8s-image-swapper/issues/353)) ([4525ad4](https://github.com/estahn/k8s-image-swapper/commit/4525ad4a667fda8e86d4c19d3c57f9d2fe9ab7c3)), closes [#112161](https://github.com/estahn/k8s-image-swapper/issues/112161) [pohly/automated-cherry-pick-of-#112129](https://github.com/pohly/automated-cherry-pick-of-/issues/112129) [#112336](https://github.com/estahn/k8s-image-swapper/issues/112336) [enj/automated-cherry-pick-of-#112017](https://github.com/enj/automated-cherry-pick-of-/issues/112017) [#112055](https://github.com/estahn/k8s-image-swapper/issues/112055) [aanm/automated-cherry-pick-of-#111752](https://github.com/aanm/automated-cherry-pick-of-/issues/111752) ## [1.3.0](https://github.com/estahn/k8s-image-swapper/compare/v1.2.3...v1.3.0) (2022-09-07) ### :tada: Features * cross account caching with role ([#336](https://github.com/estahn/k8s-image-swapper/issues/336)) ([98d138e](https://github.com/estahn/k8s-image-swapper/commit/98d138ece9dc27acf20266994e25bef4d43c3d7b)) ### :arrow_up: Dependencies * **deps:** bump actions/cache from 3.0.6 to 3.0.8 ([#319](https://github.com/estahn/k8s-image-swapper/issues/319)) ([245ab30](https://github.com/estahn/k8s-image-swapper/commit/245ab30bec7155caaad2ee95689ca71574f69252)), closes [#809](https://github.com/estahn/k8s-image-swapper/issues/809) [#833](https://github.com/estahn/k8s-image-swapper/issues/833) [#810](https://github.com/estahn/k8s-image-swapper/issues/810) [#888](https://github.com/estahn/k8s-image-swapper/issues/888) [#891](https://github.com/estahn/k8s-image-swapper/issues/891) [#899](https://github.com/estahn/k8s-image-swapper/issues/899) [#894](https://github.com/estahn/k8s-image-swapper/issues/894) * **deps:** bump alpine from 3.16.1 to 3.16.2 ([da05fdd](https://github.com/estahn/k8s-image-swapper/commit/da05fdd19e9b2540a1a57b30aadabd00ea260f9e)) * **deps:** bump github.com/alitto/pond from 1.8.0 to 1.8.1 ([#342](https://github.com/estahn/k8s-image-swapper/issues/342)) ([4e50c28](https://github.com/estahn/k8s-image-swapper/commit/4e50c28818fb7db5f2d9b3431a346036109a8f44)), closes [alitto/pond#33](https://github.com/alitto/pond/issues/33) [#34](https://github.com/estahn/k8s-image-swapper/issues/34) [#32](https://github.com/estahn/k8s-image-swapper/issues/32) * **deps:** bump github.com/aws/aws-sdk-go from 1.44.70 to 1.44.92 ([0f396c5](https://github.com/estahn/k8s-image-swapper/commit/0f396c57a16e97a5ed01dd310cd7fe808cb0c8b1)) * **deps:** bump github.com/aws/aws-sdk-go from 1.44.70 to 1.44.92 ([#338](https://github.com/estahn/k8s-image-swapper/issues/338)) ([fa795ae](https://github.com/estahn/k8s-image-swapper/commit/fa795aef3e847fb0f1526dca9efc6cd44ddd9fd9)), closes [#4548](https://github.com/estahn/k8s-image-swapper/issues/4548) [#4546](https://github.com/estahn/k8s-image-swapper/issues/4546) [#4545](https://github.com/estahn/k8s-image-swapper/issues/4545) [#4544](https://github.com/estahn/k8s-image-swapper/issues/4544) [#4543](https://github.com/estahn/k8s-image-swapper/issues/4543) [#4542](https://github.com/estahn/k8s-image-swapper/issues/4542) [#4539](https://github.com/estahn/k8s-image-swapper/issues/4539) [#4536](https://github.com/estahn/k8s-image-swapper/issues/4536) [#4534](https://github.com/estahn/k8s-image-swapper/issues/4534) [#4533](https://github.com/estahn/k8s-image-swapper/issues/4533) * **deps:** bump github.com/go-co-op/gocron from 1.16.2 to 1.17.0 ([#340](https://github.com/estahn/k8s-image-swapper/issues/340)) ([645bef3](https://github.com/estahn/k8s-image-swapper/commit/645bef3b6b2ab2c936b0192dd24fd083f64e2034)), closes [go-co-op/gocron#380](https://github.com/go-co-op/gocron/issues/380) [go-co-op/gocron#381](https://github.com/go-co-op/gocron/issues/381) [go-co-op/gocron#375](https://github.com/go-co-op/gocron/issues/375) [#381](https://github.com/estahn/k8s-image-swapper/issues/381) [#380](https://github.com/estahn/k8s-image-swapper/issues/380) [#375](https://github.com/estahn/k8s-image-swapper/issues/375) * **deps:** bump github.com/gruntwork-io/terratest from 0.40.19 to 0.40.21 ([#334](https://github.com/estahn/k8s-image-swapper/issues/334)) ([d0f6c39](https://github.com/estahn/k8s-image-swapper/commit/d0f6c39c30c6c47c502b036de3687c73912ecec9)), closes [#1166](https://github.com/estahn/k8s-image-swapper/issues/1166) [#1159](https://github.com/estahn/k8s-image-swapper/issues/1159) * **deps:** bump github.com/rs/zerolog from 1.27.0 to 1.28.0 ([#339](https://github.com/estahn/k8s-image-swapper/issues/339)) ([7fb4ff5](https://github.com/estahn/k8s-image-swapper/commit/7fb4ff588ca7f0d177cc9f5bb36066367f9ca84d)), closes [#457](https://github.com/estahn/k8s-image-swapper/issues/457) [#416](https://github.com/estahn/k8s-image-swapper/issues/416) [#454](https://github.com/estahn/k8s-image-swapper/issues/454) [#453](https://github.com/estahn/k8s-image-swapper/issues/453) [#383](https://github.com/estahn/k8s-image-swapper/issues/383) [#396](https://github.com/estahn/k8s-image-swapper/issues/396) [#414](https://github.com/estahn/k8s-image-swapper/issues/414) [#415](https://github.com/estahn/k8s-image-swapper/issues/415) [#430](https://github.com/estahn/k8s-image-swapper/issues/430) [#432](https://github.com/estahn/k8s-image-swapper/issues/432) * **deps:** bump github.com/spf13/viper from 1.12.0 to 1.13.0 ([#341](https://github.com/estahn/k8s-image-swapper/issues/341)) ([9b59bd4](https://github.com/estahn/k8s-image-swapper/commit/9b59bd4f308916d207fcfb5c7f3c70eedda1c615)), closes [spf13/viper#1371](https://github.com/spf13/viper/issues/1371) [spf13/viper#1373](https://github.com/spf13/viper/issues/1373) [spf13/viper#1393](https://github.com/spf13/viper/issues/1393) [spf13/viper#1424](https://github.com/spf13/viper/issues/1424) [spf13/viper#1405](https://github.com/spf13/viper/issues/1405) [spf13/viper#1414](https://github.com/spf13/viper/issues/1414) [spf13/viper#1387](https://github.com/spf13/viper/issues/1387) [spf13/viper#1374](https://github.com/spf13/viper/issues/1374) [spf13/viper#1375](https://github.com/spf13/viper/issues/1375) [spf13/viper#1378](https://github.com/spf13/viper/issues/1378) [spf13/viper#1360](https://github.com/spf13/viper/issues/1360) [spf13/viper#1381](https://github.com/spf13/viper/issues/1381) [spf13/viper#1384](https://github.com/spf13/viper/issues/1384) [spf13/viper#1383](https://github.com/spf13/viper/issues/1383) [spf13/viper#1395](https://github.com/spf13/viper/issues/1395) [spf13/viper#1420](https://github.com/spf13/viper/issues/1420) [spf13/viper#1422](https://github.com/spf13/viper/issues/1422) [spf13/viper#1412](https://github.com/spf13/viper/issues/1412) [spf13/viper#1373](https://github.com/spf13/viper/issues/1373) [spf13/viper#1393](https://github.com/spf13/viper/issues/1393) [spf13/viper#1371](https://github.com/spf13/viper/issues/1371) [spf13/viper#1387](https://github.com/spf13/viper/issues/1387) [spf13/viper#1405](https://github.com/spf13/viper/issues/1405) [spf13/viper#1414](https://github.com/spf13/viper/issues/1414) * **deps:** bump goreleaser/goreleaser-action from 3.0.0 to 3.1.0 ([#328](https://github.com/estahn/k8s-image-swapper/issues/328)) ([a8d2dd1](https://github.com/estahn/k8s-image-swapper/commit/a8d2dd1916be3b7e686cb2e6814710ab73c5f953)), closes [#369](https://github.com/estahn/k8s-image-swapper/issues/369) [#357](https://github.com/estahn/k8s-image-swapper/issues/357) [#356](https://github.com/estahn/k8s-image-swapper/issues/356) [#360](https://github.com/estahn/k8s-image-swapper/issues/360) [#359](https://github.com/estahn/k8s-image-swapper/issues/359) [#358](https://github.com/estahn/k8s-image-swapper/issues/358) [#367](https://github.com/estahn/k8s-image-swapper/issues/367) [#369](https://github.com/estahn/k8s-image-swapper/issues/369) [#367](https://github.com/estahn/k8s-image-swapper/issues/367) [#358](https://github.com/estahn/k8s-image-swapper/issues/358) [#359](https://github.com/estahn/k8s-image-swapper/issues/359) [#360](https://github.com/estahn/k8s-image-swapper/issues/360) [#357](https://github.com/estahn/k8s-image-swapper/issues/357) [#356](https://github.com/estahn/k8s-image-swapper/issues/356) * **deps:** bump k8s.io/api from 0.24.3 to 0.25.0 ([#325](https://github.com/estahn/k8s-image-swapper/issues/325)) ([ce10907](https://github.com/estahn/k8s-image-swapper/commit/ce10907f31431c641269032b823beaff4932f224)), closes [#111657](https://github.com/estahn/k8s-image-swapper/issues/111657) [#109090](https://github.com/estahn/k8s-image-swapper/issues/109090) [#111258](https://github.com/estahn/k8s-image-swapper/issues/111258) [#111113](https://github.com/estahn/k8s-image-swapper/issues/111113) [#111696](https://github.com/estahn/k8s-image-swapper/issues/111696) [#108692](https://github.com/estahn/k8s-image-swapper/issues/108692) * **deps:** bump k8s.io/client-go from 0.24.3 to 0.25.0 ([#324](https://github.com/estahn/k8s-image-swapper/issues/324)) ([f7c889f](https://github.com/estahn/k8s-image-swapper/commit/f7c889f4880f0d543c05f70759e8cbfef5c3d7ac)) ## [1.2.3](https://github.com/estahn/k8s-image-swapper/compare/v1.2.2...v1.2.3) (2022-09-01) ## [1.2.2](https://github.com/estahn/k8s-image-swapper/compare/v1.2.1...v1.2.2) (2022-08-01) ## [1.2.1](https://github.com/estahn/k8s-image-swapper/compare/v1.2.0...v1.2.1) (2022-07-26) # [1.2.0](https://github.com/estahn/k8s-image-swapper/compare/v1.1.0...v1.2.0) (2022-07-03) ### Bug Fixes * add missing dash ([228749d](https://github.com/estahn/k8s-image-swapper/commit/228749d98e32a7f90608b37b39d74a108f619f37)) * bump alpine to 3.16 due to security reports ([f7d6564](https://github.com/estahn/k8s-image-swapper/commit/f7d6564e1d607fa53a44e73f8b495a859c31aac1)) * docker references with both tag and digest ([5a17075](https://github.com/estahn/k8s-image-swapper/commit/5a170758a58b0244e6001a3aa5911c3be3d076f8)), closes [#48](https://github.com/estahn/k8s-image-swapper/issues/48) * failed to solve: executor failed running ([af7df18](https://github.com/estahn/k8s-image-swapper/commit/af7df18a02d6455a4ff8ef1495741ad59cbb4856)) * setup buildx and qemu for image-scan ([c435048](https://github.com/estahn/k8s-image-swapper/commit/c43504873af1c5fd9c2551f8b77f3220f491ab6a)) * standard_init_linux.go:228: exec user process caused: exec format error ([b7d0c89](https://github.com/estahn/k8s-image-swapper/commit/b7d0c89d162ed0d71e01620cb074be68b8612ab2)) * **deps:** update module github.com/aws/aws-sdk-go to v1.40.54 ([7f9dbf5](https://github.com/estahn/k8s-image-swapper/commit/7f9dbf5cf5ddae16e252adc8ce21bb4039cd208d)) ### Features * add arm docker build ([be81815](https://github.com/estahn/k8s-image-swapper/commit/be8181590fb899f1515b78fbc02bf02986d72e9c)) * add full arm support to image copying ([6f14156](https://github.com/estahn/k8s-image-swapper/commit/6f14156acb610541d54d16e85171529de39af6ab)) # [1.1.0](https://github.com/estahn/k8s-image-swapper/compare/v1.0.0...v1.1.0) (2021-10-02) ### Bug Fixes * provide log record for ImageSwapPolicyExists ([179da70](https://github.com/estahn/k8s-image-swapper/commit/179da706fd43c880d71063b786164f9d2cc862e4)) * timeout for ECR client ([26bdc10](https://github.com/estahn/k8s-image-swapper/commit/26bdc10c3eb21b1dfbea9a659e6b650cb25b335e)) * **deps:** update module github.com/alitto/pond to v1.5.1 ([504e2dd](https://github.com/estahn/k8s-image-swapper/commit/504e2dde58abf1312dab523cb43073a5cc7bc1b1)) * **deps:** update module github.com/aws/aws-sdk-go to v1.38.47 ([#70](https://github.com/estahn/k8s-image-swapper/issues/70)) ([4f30053](https://github.com/estahn/k8s-image-swapper/commit/4f300530ac9a6f8250672b272c24168601f42e62)) * **deps:** update module github.com/aws/aws-sdk-go to v1.40.43 ([266ef01](https://github.com/estahn/k8s-image-swapper/commit/266ef01da6d3caad97dac0f4d0a882dbd75502cc)) * **deps:** update module github.com/containers/image/v5 to v5.11.0 ([#61](https://github.com/estahn/k8s-image-swapper/issues/61)) ([11d6d28](https://github.com/estahn/k8s-image-swapper/commit/11d6d2843dbaa392a418e2a57fdab27fb5249077)) * **deps:** update module github.com/containers/image/v5 to v5.16.0 ([5230b91](https://github.com/estahn/k8s-image-swapper/commit/5230b91a7f37e0f4c6d6370d7c1a9231bf13b983)) * **deps:** update module github.com/dgraph-io/ristretto to v0.1.0 ([#82](https://github.com/estahn/k8s-image-swapper/issues/82)) ([dff1cb1](https://github.com/estahn/k8s-image-swapper/commit/dff1cb186ab1301836f978da1ead02b9ea75bb09)) * **deps:** update module github.com/go-co-op/gocron to v1.9.0 ([c0e9f11](https://github.com/estahn/k8s-image-swapper/commit/c0e9f111eb6b07d54732cc85464bab06dbfdf5e6)) * **deps:** update module github.com/rs/zerolog to v1.22.0 ([#76](https://github.com/estahn/k8s-image-swapper/issues/76)) ([c098326](https://github.com/estahn/k8s-image-swapper/commit/c098326273ab31dbd31869c4749164fde7544b67)) * **deps:** update module github.com/rs/zerolog to v1.23.0 ([#84](https://github.com/estahn/k8s-image-swapper/issues/84)) ([607d5bb](https://github.com/estahn/k8s-image-swapper/commit/607d5bb53a1d7396ae5d504ce49508ceac5e26d6)) * **deps:** update module github.com/rs/zerolog to v1.25.0 ([72822f4](https://github.com/estahn/k8s-image-swapper/commit/72822f42c762455a1a6932631e36418dc3b92d2a)) * **deps:** update module github.com/slok/kubewebhook to v2 ([8bd73d4](https://github.com/estahn/k8s-image-swapper/commit/8bd73d47772c0524c552577805d9f01ae365e77f)) * **deps:** update module github.com/spf13/cobra to v1.2.1 ([ea1e787](https://github.com/estahn/k8s-image-swapper/commit/ea1e7874cdaaa09dea34dd1d4a6f02a7ccb6925c)) * **deps:** update module github.com/spf13/viper to v1.8.1 ([8a055a2](https://github.com/estahn/k8s-image-swapper/commit/8a055a28343d8dbe780f74f99a275a311549576d)) * **deps:** update module k8s.io/api to v0.22.1 ([ab6d898](https://github.com/estahn/k8s-image-swapper/commit/ab6d898a2f9faa49b3c4f61f1443eb55bf79d93b)) * **deps:** update module k8s.io/apimachinery to v0.21.1 ([#79](https://github.com/estahn/k8s-image-swapper/issues/79)) ([aeeeffb](https://github.com/estahn/k8s-image-swapper/commit/aeeeffb4e20c50ecb0e3c0cb46654c3c41f62de0)) * **deps:** update module k8s.io/apimachinery to v0.22.2 ([ef72c66](https://github.com/estahn/k8s-image-swapper/commit/ef72c665f00d6d1fb454cd596c98b3a72cd7614c)) ### Features * Support for imagePullSecrets ([#112](https://github.com/estahn/k8s-image-swapper/issues/112)) ([2d8cf77](https://github.com/estahn/k8s-image-swapper/commit/2d8cf777d32053b8af622cb677d86ac21f526ba8)), closes [#92](https://github.com/estahn/k8s-image-swapper/issues/92) [#19](https://github.com/estahn/k8s-image-swapper/issues/19) * Support for pod.spec.initContainers ([#118](https://github.com/estahn/k8s-image-swapper/issues/118)) ([725ff2c](https://github.com/estahn/k8s-image-swapper/commit/725ff2cdc45a13d1a31c3694231482ee09ab2cbd)), closes [#73](https://github.com/estahn/k8s-image-swapper/issues/73) [#96](https://github.com/estahn/k8s-image-swapper/issues/96) # [1.1.0-alpha.1](https://github.com/estahn/k8s-image-swapper/compare/v1.0.0...v1.1.0-alpha.1) (2021-09-30) ### Bug Fixes * provide log record for ImageSwapPolicyExists ([179da70](https://github.com/estahn/k8s-image-swapper/commit/179da706fd43c880d71063b786164f9d2cc862e4)) * timeout for ECR client ([26bdc10](https://github.com/estahn/k8s-image-swapper/commit/26bdc10c3eb21b1dfbea9a659e6b650cb25b335e)) * **deps:** update module github.com/alitto/pond to v1.5.1 ([504e2dd](https://github.com/estahn/k8s-image-swapper/commit/504e2dde58abf1312dab523cb43073a5cc7bc1b1)) * **deps:** update module github.com/aws/aws-sdk-go to v1.38.47 ([#70](https://github.com/estahn/k8s-image-swapper/issues/70)) ([4f30053](https://github.com/estahn/k8s-image-swapper/commit/4f300530ac9a6f8250672b272c24168601f42e62)) * **deps:** update module github.com/aws/aws-sdk-go to v1.40.43 ([266ef01](https://github.com/estahn/k8s-image-swapper/commit/266ef01da6d3caad97dac0f4d0a882dbd75502cc)) * **deps:** update module github.com/containers/image/v5 to v5.11.0 ([#61](https://github.com/estahn/k8s-image-swapper/issues/61)) ([11d6d28](https://github.com/estahn/k8s-image-swapper/commit/11d6d2843dbaa392a418e2a57fdab27fb5249077)) * **deps:** update module github.com/containers/image/v5 to v5.16.0 ([5230b91](https://github.com/estahn/k8s-image-swapper/commit/5230b91a7f37e0f4c6d6370d7c1a9231bf13b983)) * **deps:** update module github.com/dgraph-io/ristretto to v0.1.0 ([#82](https://github.com/estahn/k8s-image-swapper/issues/82)) ([dff1cb1](https://github.com/estahn/k8s-image-swapper/commit/dff1cb186ab1301836f978da1ead02b9ea75bb09)) * **deps:** update module github.com/go-co-op/gocron to v1.9.0 ([c0e9f11](https://github.com/estahn/k8s-image-swapper/commit/c0e9f111eb6b07d54732cc85464bab06dbfdf5e6)) * **deps:** update module github.com/rs/zerolog to v1.22.0 ([#76](https://github.com/estahn/k8s-image-swapper/issues/76)) ([c098326](https://github.com/estahn/k8s-image-swapper/commit/c098326273ab31dbd31869c4749164fde7544b67)) * **deps:** update module github.com/rs/zerolog to v1.23.0 ([#84](https://github.com/estahn/k8s-image-swapper/issues/84)) ([607d5bb](https://github.com/estahn/k8s-image-swapper/commit/607d5bb53a1d7396ae5d504ce49508ceac5e26d6)) * **deps:** update module github.com/rs/zerolog to v1.25.0 ([72822f4](https://github.com/estahn/k8s-image-swapper/commit/72822f42c762455a1a6932631e36418dc3b92d2a)) * **deps:** update module github.com/slok/kubewebhook to v2 ([8bd73d4](https://github.com/estahn/k8s-image-swapper/commit/8bd73d47772c0524c552577805d9f01ae365e77f)) * **deps:** update module github.com/spf13/cobra to v1.2.1 ([ea1e787](https://github.com/estahn/k8s-image-swapper/commit/ea1e7874cdaaa09dea34dd1d4a6f02a7ccb6925c)) * **deps:** update module github.com/spf13/viper to v1.8.1 ([8a055a2](https://github.com/estahn/k8s-image-swapper/commit/8a055a28343d8dbe780f74f99a275a311549576d)) * **deps:** update module k8s.io/api to v0.22.1 ([ab6d898](https://github.com/estahn/k8s-image-swapper/commit/ab6d898a2f9faa49b3c4f61f1443eb55bf79d93b)) * **deps:** update module k8s.io/apimachinery to v0.21.1 ([#79](https://github.com/estahn/k8s-image-swapper/issues/79)) ([aeeeffb](https://github.com/estahn/k8s-image-swapper/commit/aeeeffb4e20c50ecb0e3c0cb46654c3c41f62de0)) * **deps:** update module k8s.io/apimachinery to v0.22.2 ([ef72c66](https://github.com/estahn/k8s-image-swapper/commit/ef72c665f00d6d1fb454cd596c98b3a72cd7614c)) ### Features * Support for imagePullSecrets ([#112](https://github.com/estahn/k8s-image-swapper/issues/112)) ([2d8cf77](https://github.com/estahn/k8s-image-swapper/commit/2d8cf777d32053b8af622cb677d86ac21f526ba8)), closes [#92](https://github.com/estahn/k8s-image-swapper/issues/92) [#19](https://github.com/estahn/k8s-image-swapper/issues/19) # 1.0.0 (2020-12-25) ### Bug Fixes * bump skopeo from 0.2.0 to 1.2.0 ([84025aa](https://github.com/estahn/k8s-image-swapper/commit/84025aaf06d287a306fba98f848e272a19ff8aa0)) * hardcoded AWS region ([3cc0d49](https://github.com/estahn/k8s-image-swapper/commit/3cc0d492bc17a6ad022cb2794786079759f7bc41)), closes [#20](https://github.com/estahn/k8s-image-swapper/issues/20) [#17](https://github.com/estahn/k8s-image-swapper/issues/17) * **chart:** serviceaccount missing annotation tag ([#21](https://github.com/estahn/k8s-image-swapper/issues/21)) ([7164626](https://github.com/estahn/k8s-image-swapper/commit/71646266e54d043f3bba2ee59975e7f9d11f8f13)) * trace for verbose logs and improve context ([58e05dc](https://github.com/estahn/k8s-image-swapper/commit/58e05dc66644de22183e39dcdc85cf8ce139d8db)), closes [#15](https://github.com/estahn/k8s-image-swapper/issues/15) ### Features * allow filters for container context ([37d0a4d](https://github.com/estahn/k8s-image-swapper/commit/37d0a4d9ac3bd37128c92ede0bff3f4071483b1d)), closes [#32](https://github.com/estahn/k8s-image-swapper/issues/32) * automatic token renewal before expiry ([a7c45b8](https://github.com/estahn/k8s-image-swapper/commit/a7c45b8b093efa00e7a04f89a57d5909b4ce068a)), closes [#31](https://github.com/estahn/k8s-image-swapper/issues/31) * helm chart ([00f6b74](https://github.com/estahn/k8s-image-swapper/commit/00f6b7409c1f0ab59ea227f5d3b995d532beb623)) * ImageSwapPolicy defines the mutation strategy used by the webhook. ([9d61659](https://github.com/estahn/k8s-image-swapper/commit/9d616596013d7b1cbb121b0cf137273867bdb19f)) * POC ([fedcb22](https://github.com/estahn/k8s-image-swapper/commit/fedcb22c2fef26a76bd0fd9dacff70d0d952c077)) # [1.0.0-beta.4](https://github.com/estahn/k8s-image-swapper/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2020-12-23) ### Bug Fixes * bump skopeo from 0.2.0 to 1.2.0 ([09fdb6e](https://github.com/estahn/k8s-image-swapper/commit/09fdb6eb2383c30a45d1a5a7fb3d10a4c6b891e0)) # [1.0.0-beta.3](https://github.com/estahn/k8s-image-swapper/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2020-12-23) ### Features * ImageSwapPolicy defines the mutation strategy used by the webhook. ([e64bc6d](https://github.com/estahn/k8s-image-swapper/commit/e64bc6d120bea925a06cf06f3b22c8184a24fb35)) # [1.0.0-beta.2](https://github.com/estahn/k8s-image-swapper/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2020-12-22) ### Features * allow filters for container context ([c7e4c51](https://github.com/estahn/k8s-image-swapper/commit/c7e4c51a5a04ef9ae8689ffe73ff7d1411f43450)), closes [#32](https://github.com/estahn/k8s-image-swapper/issues/32) * automatic token renewal before expiry ([d557c23](https://github.com/estahn/k8s-image-swapper/commit/d557c23e798f4cae61cd412d99f482ec4d310b9f)), closes [#31](https://github.com/estahn/k8s-image-swapper/issues/31) # 1.0.0-beta.1 (2020-12-21) ### Bug Fixes * hardcoded AWS region ([3cc0d49](https://github.com/estahn/k8s-image-swapper/commit/3cc0d492bc17a6ad022cb2794786079759f7bc41)), closes [#20](https://github.com/estahn/k8s-image-swapper/issues/20) [#17](https://github.com/estahn/k8s-image-swapper/issues/17) * **chart:** serviceaccount missing annotation tag ([#21](https://github.com/estahn/k8s-image-swapper/issues/21)) ([7164626](https://github.com/estahn/k8s-image-swapper/commit/71646266e54d043f3bba2ee59975e7f9d11f8f13)) * trace for verbose logs and improve context ([58e05dc](https://github.com/estahn/k8s-image-swapper/commit/58e05dc66644de22183e39dcdc85cf8ce139d8db)), closes [#15](https://github.com/estahn/k8s-image-swapper/issues/15) ### Features * helm chart ([00f6b74](https://github.com/estahn/k8s-image-swapper/commit/00f6b7409c1f0ab59ea227f5d3b995d532beb623)) * POC ([fedcb22](https://github.com/estahn/k8s-image-swapper/commit/fedcb22c2fef26a76bd0fd9dacff70d0d952c077)) # 1.0.0-alpha.1 (2020-12-18) ### Bug Fixes * **chart:** serviceaccount missing annotation tag ([#21](https://github.com/estahn/k8s-image-swapper/issues/21)) ([7164626](https://github.com/estahn/k8s-image-swapper/commit/71646266e54d043f3bba2ee59975e7f9d11f8f13)) * trace for verbose logs and improve context ([58e05dc](https://github.com/estahn/k8s-image-swapper/commit/58e05dc66644de22183e39dcdc85cf8ce139d8db)), closes [#15](https://github.com/estahn/k8s-image-swapper/issues/15) ### Features * helm chart ([00f6b74](https://github.com/estahn/k8s-image-swapper/commit/00f6b7409c1f0ab59ea227f5d3b995d532beb623)) * POC ([fedcb22](https://github.com/estahn/k8s-image-swapper/commit/fedcb22c2fef26a76bd0fd9dacff70d0d952c077)) ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [enrico.stahn@gmail.com](mailto:enrico.stahn@gmail.com). All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html [Mozilla CoC]: https://github.com/mozilla/diversity [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing By participating to this project, you agree to abide our [code of conduct](/CODE_OF_CONDUCT.md). ## Setup your machine `k8s-image-swapper` is written in [Go](https://golang.org/). Prerequisites: - `make` - [Go 1.16+](https://golang.org/doc/install) - [golangci-lint](https://golangci-lint.run/usage/install/#local-installation) - [Docker](https://www.docker.com/) (or [Podman](https://podman.io/)) - [kind](https://kind.sigs.k8s.io/) - [pre-commit](https://pre-commit.com/) (optional) - [ngrok](https://ngrok.com/) (optional) Clone `k8s-image-swapper` anywhere: ```sh git clone git@github.com:estahn/k8s-image-swapper.git ``` Install the build and lint dependencies: ```sh make setup ``` A good way of making sure everything is all right is running the test suite: ```sh make test ``` ## Test your change You can create a branch for your changes and try to build from the source as you go: ```sh make test ``` When you are satisfied with the changes, we suggest you run: ```sh make fmt lint test ``` Which runs all the linters and tests. ## Create a commit Commit messages should be well formatted, and to make that "standardized", we are using Conventional Commits. You can follow the documentation on [their website](https://www.conventionalcommits.org). ## Submit a pull request Push your branch to your `k8s-image-swapper` fork and open a pull request against the main branch. ================================================ FILE: Dockerfile ================================================ #FROM quay.io/skopeo/stable:v1.2.0 AS skopeo #FROM gcr.io/distroless/base-debian10 #FROM debian:10 #COPY --from=skopeo /usr/bin/skopeo /skopeo # TODO: Using alpine for now due to easier installation of skopeo # Will use distroless after incorporating skopeo into the webhook directly FROM alpine:3.23.3 RUN ["apk", "add", "--no-cache", "--repository=http://dl-cdn.alpinelinux.org/alpine/edge/community", "skopeo>=1.2.0"] COPY k8s-image-swapper / ENTRYPOINT ["/k8s-image-swapper"] ARG BUILD_DATE ARG VCS_REF LABEL maintainer="k8s-image-swapper " \ org.opencontainers.image.title="k8s-image-swapper" \ org.opencontainers.image.description="Mirror images into your own registry and swap image references automatically." \ org.opencontainers.image.url="https://github.com/estahn/k8s-image-swapper" \ org.opencontainers.image.source="https://github.com/estahn/k8s-image-swapper" \ org.opencontainers.image.vendor="estahn" \ org.label-schema.schema-version="1.0" \ org.label-schema.name="k8s-image-swapper" \ org.label-schema.description="Mirror images into your own registry and swap image references automatically." \ org.label-schema.url="https://github.com/estahn/k8s-image-swapper" \ org.label-schema.vcs-url="git@github.com:estahn/k8s-image-swapper.git" \ org.label-schema.vendor="estahn" \ org.opencontainers.image.revision="$VCS_REF" \ org.opencontainers.image.created="$BUILD_DATE" \ org.label-schema.vcs-ref="$VCS_REF" \ org.label-schema.build-date="$BUILD_DATE" ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Enrico Stahn Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ SOURCE_FILES?=./... TEST_PATTERN?=. TEST_OPTIONS?= .PHONY: help $(MAKECMDGOALS) .DEFAULT_GOAL := help export GO111MODULE := on export GOPROXY = https://proxy.golang.org,direct help: ## List targets & descriptions @cat Makefile* | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' setup: ## Install dependencies go mod download go mod tidy test: ## Run tests LC_ALL=C go test $(TEST_OPTIONS) -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=5m cover: test ## Run tests and open coverage report go tool cover -html=coverage.txt fmt: ## gofmt and goimports all go files gofmt -l -w . goimports -l -w . lint: ## Run linters golangci-lint run e2e: ## Run end-to-end tests go test -v -run TestHelmDeployment ./test ================================================ FILE: README.md ================================================

Raiders of the Lost Ark

k8s-image-swapper

Mirror images into your own registry and swap image references automatically.

--- `k8s-image-swapper` is a mutating webhook for Kubernetes, downloading images into your own registry and pointing the images to that new location. It is an alternative to a [docker pull-through proxy](https://docs.docker.com/registry/recipes/mirror/). **Amazon ECR** and **Google Container Registry** are currently supported. ## :zap: Benefits Using `k8s-image-swapper` will improve the overall availability, reliability, durability and resiliency of your Kubernetes cluster by keeping 3rd-party images mirrored into your own registry. `k8s-image-swapper` will transparently consolidate all images into a single registry without the need to adjust manifests therefore reducing the impact of external registry failures, rate limiting, network issues, change or removal of images while reducing data traffic and therefore cost. **TL;DR:** * Protect against: * external registry failure ([quay.io outage](https://www.reddit.com/r/devops/comments/f9kiej/quayio_is_experiencing_an_outage/)) * image pull rate limiting ([docker.io rate limits](https://www.docker.com/blog/scaling-docker-to-serve-millions-more-developers-network-egress/)) * accidental image changes * removal of images * Use in air-gaped environments without the need to change manifests * Reduce NAT ingress traffic/cost ## :book: Documentation A comprehensive guide on getting started and a list of configuration options can be found in the documentation. [![Documentation](https://img.shields.io/badge/Documentation-2FA4E7?style=for-the-badge&logo=ReadMe&logoColor=white)](https://estahn.github.io/k8s-image-swapper/index.html) ## :question: Community You have questions, need support and or just want to talk about `k8s-image-swapper`? Here are ways to get in touch with the community: [![Slack channel](https://img.shields.io/badge/Slack_Channel-4A154B?style=for-the-badge&logo=slack&logoColor=white)](http://slack.kubernetes.io/) [![GitHub Discussions](https://img.shields.io/badge/GITHUB_DISCUSSION-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/estahn/k8s-image-swapper/discussions) ## :heart_decoration: Sponsor Does your company use `k8s-image-swapper`? Help keep the project bug-free and feature rich by [sponsoring the project](https://github.com/sponsors/estahn). ## :office: Commercial Support Does your company require individual support or addition of features within a guaranteed timeframe? Contact me via [email](mailto:enrico.stahn@gmail.com) to discuss. ## :octocat: Badges [![Release](https://img.shields.io/github/release/estahn/k8s-image-swapper.svg?style=for-the-badge)](https://github.com/estahn/k8s-image-swapper/releases/latest) [![Artifact Hub](https://img.shields.io/badge/Artifact_Hub-417598?style=for-the-badge&logo=artifacthub&logoColor=white)](https://artifacthub.io/packages/helm/estahn/k8s-image-swapper) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=for-the-badge)](/LICENSE.md) [![Codecov branch](https://img.shields.io/codecov/c/github/estahn/k8s-image-swapper/main.svg?style=for-the-badge)](https://codecov.io/gh/estahn/k8s-image-swapper) [![Go Doc](https://img.shields.io/badge/godoc-reference-blue.svg?style=for-the-badge)](http://godoc.org/github.com/estahn/k8s-image-swapper) ## :star2: Stargazers over time [![Stargazers over time](https://starchart.cc/estahn/k8s-image-swapper.svg)](https://starchart.cc/estahn/k8s-image-swapper) ================================================ FILE: cmd/root.go ================================================ /* Copyright © 2020 Enrico Stahn Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package cmd import ( "context" "fmt" "net/http" "os" "os/signal" "syscall" "time" "github.com/estahn/k8s-image-swapper/pkg/config" "github.com/estahn/k8s-image-swapper/pkg/registry" "github.com/estahn/k8s-image-swapper/pkg/secrets" "github.com/estahn/k8s-image-swapper/pkg/types" "github.com/estahn/k8s-image-swapper/pkg/webhook" homedir "github.com/mitchellh/go-homedir" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/rs/zerolog" "github.com/rs/zerolog/log" kwhhttp "github.com/slok/kubewebhook/v2/pkg/http" "github.com/spf13/cobra" "github.com/spf13/viper" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" ) var cfgFile string var cfg *config.Config = &config.Config{} // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "k8s-image-swapper", Short: "Mirror images into your own registry and swap image references automatically.", Long: `Mirror images into your own registry and swap image references automatically. A mutating webhook for Kubernetes, pointing the images to a new location.`, // Uncomment the following line if your bare application // has an action associated with it: Run: func(cmd *cobra.Command, args []string) { //promReg := prometheus.NewRegistry() //metricsRec := metrics.NewPrometheus(promReg) log.Trace().Interface("config", cfg).Msg("config") // Create registry clients for source registries sourceRegistryClients := []registry.Client{} for _, reg := range cfg.Source.Registries { sourceRegistryClient, err := registry.NewClient(reg) if err != nil { log.Err(err).Msgf("error connecting to source registry at %s", reg.Domain()) os.Exit(1) } sourceRegistryClients = append(sourceRegistryClients, sourceRegistryClient) } // Create a registry client for private target registry targetRegistryClient, err := registry.NewClient(cfg.Target) if err != nil { log.Err(err).Msgf("error connecting to target registry at %s", cfg.Target.Domain()) os.Exit(1) } imageSwapPolicy, err := types.ParseImageSwapPolicy(cfg.ImageSwapPolicy) if err != nil { log.Err(err).Str("policy", cfg.ImageSwapPolicy).Msg("parsing image swap policy failed") } imageCopyPolicy, err := types.ParseImageCopyPolicy(cfg.ImageCopyPolicy) if err != nil { log.Err(err).Str("policy", cfg.ImageCopyPolicy).Msg("parsing image copy policy failed") } imageCopyDeadline := config.DefaultImageCopyDeadline if cfg.ImageCopyDeadline != 0 { imageCopyDeadline = cfg.ImageCopyDeadline } imagePullSecretProvider := setupImagePullSecretsProvider() // Inform secret provider about managed private source registries imagePullSecretProvider.SetAuthenticatedRegistries(sourceRegistryClients) wh, err := webhook.NewImageSwapperWebhookWithOpts( targetRegistryClient, webhook.Filters(cfg.Source.Filters), webhook.ImagePullSecretsProvider(imagePullSecretProvider), webhook.ImageSwapPolicy(imageSwapPolicy), webhook.ImageCopyPolicy(imageCopyPolicy), webhook.ImageCopyDeadline(imageCopyDeadline), ) if err != nil { log.Err(err).Msg("error creating webhook") os.Exit(1) } // Get the handler for our webhook. whHandler, err := kwhhttp.HandlerFor(kwhhttp.HandlerConfig{Webhook: wh}) if err != nil { log.Err(err).Msg("error creating webhook handler") os.Exit(1) } handler := http.NewServeMux() handler.Handle("/webhook", whHandler) handler.Handle("/metrics", promhttp.Handler()) handler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { _, err := w.Write([]byte(` k8s-image-webhook

k8s-image-webhook

`)) if err != nil { log.Error() } }) srv := &http.Server{ Addr: cfg.ListenAddress, // Good practice to set timeouts to avoid Slowloris attacks. WriteTimeout: time.Second * 15, ReadTimeout: time.Second * 15, IdleTimeout: time.Second * 60, Handler: handler, } go func() { log.Info().Msgf("Listening on %v", cfg.ListenAddress) //err = http.ListenAndServeTLS(":8080", cfg.certFile, cfg.keyFile, whHandler) if cfg.TLSCertFile != "" && cfg.TLSKeyFile != "" { if err := srv.ListenAndServeTLS(cfg.TLSCertFile, cfg.TLSKeyFile); err != nil { log.Err(err).Msg("error serving webhook") os.Exit(1) } } else { if err := srv.ListenAndServe(); err != nil { log.Err(err).Msg("error serving webhook") os.Exit(1) } } }() c := make(chan os.Signal, 1) // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C) or SIGTERM // SIGKILL, SIGQUIT will not be caught. signal.Notify(c, os.Interrupt, syscall.SIGTERM) // Block until we receive our signal. <-c // Create a deadline to wait for. var wait time.Duration ctx, cancel := context.WithTimeout(context.Background(), wait) defer cancel() // Doesn't block if no connections, but will otherwise wait // until the timeout deadline. if err := srv.Shutdown(ctx); err != nil { log.Err(err).Msg("Error during shutdown") } // Optionally, you could run srv.Shutdown in a goroutine and block on // <-ctx.Done() if your application should wait for other services // to finalize based on context cancellation. log.Info().Msg("Shutting down") os.Exit(0) }, } // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) } } func init() { cobra.OnInitialize(initConfig, initLogger) // Here you will define your flags and configuration settings. // Cobra supports persistent flags, which, if defined here, // will be global for your application. rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.k8s-image-swapper.yaml)") rootCmd.PersistentFlags().StringVar(&cfg.LogLevel, "log-level", "info", "Only log messages with the given severity or above. Valid levels: [debug, info, warn, error, fatal]") rootCmd.PersistentFlags().StringVar(&cfg.LogFormat, "log-format", "json", "Format of the log messages. Valid levels: [json, console]") // Cobra also supports local flags, which will only run // when this action is called directly. //rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") rootCmd.Flags().StringVar(&cfg.ListenAddress, "listen-address", ":8443", "Address on which to expose the webhook") rootCmd.Flags().StringVar(&cfg.TLSCertFile, "tls-cert-file", "", "File containing the TLS certificate") rootCmd.Flags().StringVar(&cfg.TLSKeyFile, "tls-key-file", "", "File containing the TLS private key") rootCmd.Flags().BoolVar(&cfg.DryRun, "dry-run", true, "If true, print the action taken without taking it") } // initConfig reads in config file and ENV variables if set. func initConfig() { // Default to aws target registry type if none are defined config.SetViperDefaults(viper.GetViper()) if cfgFile != "" { // Use config file from the flag. viper.SetConfigFile(cfgFile) } else { // Find home directory. home, err := homedir.Dir() if err != nil { fmt.Println(err) os.Exit(1) } // Search config in home directory with name ".k8s-image-swapper" (without extension). viper.AddConfigPath(home) viper.AddConfigPath(".") viper.SetConfigType("yaml") viper.SetConfigName(".k8s-image-swapper") } viper.AutomaticEnv() // read in environment variables that match // If a config file is found, read it in. if err := viper.ReadInConfig(); err == nil { log.Info().Str("file", viper.ConfigFileUsed()).Msg("using config file") } if err := viper.Unmarshal(&cfg); err != nil { log.Err(err).Msg("failed to unmarshal the config file") } //validate := validator.New() //if err := validate.Struct(cfg); err != nil { // validationErrors := err.(validator.ValidationErrors) // log.Err(validationErrors).Msg("validation errors for config file") //} } // initLogger configures the log level func initLogger() { if cfg.LogFormat == "console" { log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) } lvl, err := zerolog.ParseLevel(cfg.LogLevel) if err != nil { lvl = zerolog.InfoLevel log.Err(err).Msgf("could not set log level to '%v'.", cfg.LogLevel) } zerolog.SetGlobalLevel(lvl) // add file and line number to log if level is trace if lvl == zerolog.TraceLevel { log.Logger = log.With().Caller().Logger() } } // setupImagePullSecretsProvider configures the provider handling secrets func setupImagePullSecretsProvider() secrets.ImagePullSecretsProvider { config, err := rest.InClusterConfig() if err != nil { log.Warn().Err(err).Msg("failed to configure Kubernetes client, will continue without reading secrets") return secrets.NewDummyImagePullSecretsProvider() } clientset, err := kubernetes.NewForConfig(config) if err != nil { log.Warn().Err(err).Msg("failed to configure Kubernetes client, will continue without reading secrets") return secrets.NewDummyImagePullSecretsProvider() } return secrets.NewKubernetesImagePullSecretsProvider(clientset) } ================================================ FILE: docs/configuration.md ================================================ # Configuration The configuration is managed via the config file `.k8s-image-swapper.yaml`. Some options can be overridden via parameters, e.g. `--dry-run`. ## Dry Run The option `dryRun` allows to run the webhook without executing the actions, e.g. repository creation, image download and manifest mutation. !!! example ```yaml dryRun: true ``` ## Log Level & Format The option `logLevel` & `logFormat` allow to adjust the verbosity and format (e.g. `json`, `console`). !!! example ```yaml logLevel: debug logFormat: console ``` ## ImageSwapPolicy The option `imageSwapPolicy` (default: `exists`) defines the mutation strategy used. * `always`: Will always swap the image regardless of the image existence in the target registry. This can result in pods ending in state ImagePullBack if images fail to be copied to the target registry. * `exists`: Only swaps the image if it exits in the target registry. This can result in pods pulling images from the source registry, e.g. the first pod pulls from source registry, subsequent pods pull from target registry. ## ImageCopyPolicy The option `imageCopyPolicy` (default: `delayed`) defines the image copy strategy used. * `delayed`: Submits the copy job to a process queue and moves on. * `immediate`: Submits the copy job to a process queue and waits for it to finish (deadline defined by `imageCopyDeadline`). * `force`: Attempts to immediately copy the image (deadline defined by `imageCopyDeadline`). * `none`: Do not copy the image. ## ImageCopyDeadline The option `imageCopyDeadline` (default: `8s`) defines the duration after which the image copy if aborted. This option only applies for `immediate` and `force` image copy strategies. ## Source This section configures details about the image source. ### Registries The option `source.registries` describes a list of registries to pull images from, using a specific configuration. #### AWS By providing configuration on AWS registries you can ask `k8s-image-swapper` to handle the authentication using the same credentials as for the target AWS registry. This authentication method is the default way to get authorized by a private registry if the targeted Pod does not provide an `imagePullSecret`. Registries are described with an AWS account ID and region, mostly to construct the ECR domain `[ACCOUNT_ID].dkr.ecr.[REGION].amazonaws.com`. !!! example ```yaml source: registries: - type: aws aws: accountId: 123456789 region: ap-southeast-2 - type: aws aws: accountId: 234567890 region: us-east-1 ``` ### Filters Filters provide control over what pods will be processed. By default, all pods will be processed. If a condition matches, the pod will **NOT** be processed. [JMESPath](https://jmespath.org/) is used as query language and allows flexible rules for most use-cases. !!! info The data structure used for JMESPath is as follows: === "Structure" ```yaml obj: container: ``` === "Example" ```yaml obj: metadata: name: static-web labels: role: myrole spec: containers: - name: web image: nginx ports: - name: web containerPort: 80 protocol: TCP container: name: web image: nginx ports: - name: web containerPort: 80 protocol: TCP ``` Below you will find a list of common queries and/or ideas: !!! tip "List of common queries/ideas" * Do not process if namespace equals `kube-system` (_Helm chart default_) ```yaml source: filters: - jmespath: "obj.metadata.namespace == 'kube-system'" ``` * Only process if namespace equals `playground` ```yaml source: filters: - jmespath: "obj.metadata.namespace != 'playground'" ``` * Only process if namespace ends with `-dev` ```yaml source: filters: - jmespath: "ends_with(obj.metadata.namespace,'-dev')" ``` * Do not process AWS ECR images ```yaml source: filters: - jmespath: "contains(container.image, '.dkr.ecr.') && contains(container.image, '.amazonaws.com')" ``` `k8s-image-swapper` will log the filter data and result in `debug` mode. This can be used in conjunction with [JMESPath.org](https://jmespath.org/) which has a live editor that can be used as a playground to experiment with more complex queries. ## Target This section configures details about the image target. The option `target` allows to specify which type of registry you set as your target (AWS, GCP...). At the moment, `aws` and `gcp` are the only supported values. ### AWS The option `target.aws` holds details about the target registry storing the images. The AWS Account ID and Region is primarily used to construct the ECR domain `[ACCOUNTID].dkr.ecr.[REGION].amazonaws.com`. !!! example ```yaml target: type: aws aws: accountId: 123456789 region: ap-southeast-2 prefix: /cache ``` #### ECR Options ##### Tags This provides a way to add custom tags to newly created repositories. This may be useful while looking at AWS costs. It's a slice of `Key` and `Value`. !!! example ```yaml target: type: aws aws: ecrOptions: tags: - key: cluster value: myCluster ``` ### GCP The option `target.gcp` holds details about the target registry storing the images. The GCP location, projectId, and repositoryId are used to constrct the GCP Artifact Registry domain `[LOCATION]-docker.pkg.dev/[PROJECT_ID]/[REPOSITORY_ID]`. !!! example ```yaml target: type: gcp gcp: location: us-central1 projectId: gcp-project-123 repositoryId: main ``` ================================================ FILE: docs/faq.md ================================================ # FAQ ### Is pulling from private registries supported? Yes, `imagePullSecrets` on `Pod` and `ServiceAccount` level in the hooked pod definition are supported. It is also possible to provide a list of ECRs to which authentication is handled by `k8s-image-swapper` using the same credentials as for the target registry. Please see [Configuration > Source - AWS](configuration.md#Private-registries). ### Are config changes reloaded gracefully? Not yet, they require a pod rotation. ### What happens if the image is not found in the target registry? Please see [Configuration > ImageCopyPolicy](configuration.md#imagecopypolicy). ### What level of registry outage does this handle? If the source image registry is not reachable it will replace the reference with the target registry reference. If the target registry is down it will do the same. It has no notion of the target registry being up or down. ### What happens if `k8s-image-swapper` is unavailable? Kubernetes will continue to work as if `k8s-image-swapper` was not installed. The webhook [failure policy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) is set to `Ignore`. !!! tip Environments with strict compliance requirements (or air-gapped) may overwrite this with `Fail` to avoid falling back to the public images. ### Why are sidecar images not being replaced? A Kubernetes cluster can have multiple mutating webhooks. Mutating webhooks execute sequentiatlly and each can change a submitted object. Changes may be applied after `k8s-image-swapper` was executed, e.g. Istio injecting a sidecar. ``` ... -> k8s-image-swapper -> Istio sidecar injection --> ... ``` Kubernetes 1.15+ allows to re-run webhooks if a mutating webhook modifies an object. The behaviour is controlled by the [Reinvocation policy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy). > reinvocationPolicy may be set to `Never` or `IfNeeded`. It defaults to Never. > > * `Never`: the webhook must not be called more than once in a single admission evaluation > * `IfNeeded`: the webhook may be called again as part of the admission evaluation if the object being admitted is modified by other admission plugins after the initial webhook call. The reinvocation policy can be set in the helm chart as follows: !!! example "Helm Chart" ```yaml webhook: reinvocationPolicy: IfNeeded ``` ================================================ FILE: docs/getting-started.md ================================================ # Getting started This document will provide guidance for installing `k8s-image-swapper`. ## Prerequisites `k8s-image-swapper` will automatically create image repositories and mirror images into them. This requires certain permissions for your target registry (_only AWS ECR and GCP ArtifactRegistry are supported atm_). Before you get started choose a namespace to install `k8s-image-swapper` in, e.g. `operations` or `k8s-image-swapper`. Ensure the namespace exists and is configured as your current context[^1]. All examples below will omit the namespace. ### AWS ECR as a target registry AWS supports a variety of authentication strategies. `k8s-image-swapper` uses the official Amazon AWS SDK and therefore supports [all available authentication strategies](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html). Choose from one of the strategies below or an alternative if needed. #### IAM credentials 1. Create an IAM user (e.g. `k8s-image-swapper`) with permissions[^2] to create ECR repositories and upload container images. An IAM policy example can be found in the footnotes[^2]. 2. Create a Kubernetes secret (e.g. `k8s-image-swapper-aws`) containing the IAM credentials you just obtained, e.g. ```bash kubectl create secret generic k8s-image-swapper-aws \ --from-literal=aws_access_key_id=<...> \ --from-literal=aws_secret_access_key=<...> ``` #### Using ECR registries cross-account Although ECR allows creating registry policy that allows reposistories creation from different account, there's no way to push anything to these repositories. ECR resource-level policy can not be applied during creation, and to apply it afterwards we need ecr:SetRepositoryPolicy permission, which foreign account doesn't have. One way out of this conundrum is to assume the role in target account ```yaml title=".k8s-image-swapper.yml" target: type: aws aws: accountId: 123456789 region: ap-southeast-2 role: arn:aws:iam::123456789012:role/roleName ``` !!! note Make sure that target role has proper trust permissions that allow to assume it cross-account !!! note In order te be able to pull images from outside accounts, you will have to apply proper access policy #### Access policy You can specify the access policy that will be applied to the created repos in config. Policy should be raw json string. For example: ```yaml title=".k8s-image-swapper.yml" target: type: aws aws: accountId: 123456789 region: ap-southeast-2 role: arn:aws:iam::123456789012:role/roleName ecrOptions: accessPolicy: | { "Statement": [ { "Sid": "AllowCrossAccountPull", "Effect": "Allow", "Principal": { "AWS": "*" }, "Action": [ "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", "ecr:BatchCheckLayerAvailability" ], "Condition": { "StringEquals": { "aws:PrincipalOrgID": "o-xxxxxxxxxx" } } } ], "Version": "2008-10-17" } ``` #### Lifecycle policy Similarly to access policy, lifecycle policy can be specified, for example: ```yaml title=".k8s-image-swapper.yml" target: type: aws aws: accountId: 123456789 region: ap-southeast-2 role: arn:aws:iam::123456789012:role/roleName ecrOptions: lifecyclePolicy: | { "rules": [ { "rulePriority": 1, "description": "Rule 1", "selection": { "tagStatus": "any", "countType": "imageCountMoreThan", "countNumber": 1000 }, "action": { "type": "expire" } } ] } ``` #### Service Account 1. Create an Webidentity IAM role (e.g. `k8s-image-swapper`) with the following trust policy, e.g ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::${your_aws_account_id}:oidc-provider/${oidc_image_swapper_role_arn}" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "${oidc_image_swapper_role_arn}:sub": "system:serviceaccount:${k8s_image_swapper_namespace}:${k8s_image_swapper_serviceaccount_name}" } } } ] } ``` 2. Create and attach permission policy[^2] to the role from Step 1.. Note: You can see a complete example below in [Terraform](Terraform) ### GCP Artifact Registry as a target registry To target a GCP Artifact Registry set the `target.type` to `gcp` and provide additional metadata in the configuration. ```yaml title=".k8s-image-swapper.yml" target: type: gcp gcp: location: us-central1 projectId: gcp-project-123 repositoryId: main ``` !!! note This is fundamentally different from the AWS ECR implementation since all images will be stored under *one* GCP Artifact Registry repository.
![GCP Console - Artifact Registry](img/gcp_artifact_registry.png){ loading=lazy }
#### Create Repository Create and configure a single GCP Artifact Registry repository to store Docker images for `k8s-image-swapper`. === "Terraform" ```terraform resource "google_artifact_registry_repository" "repo" { project = var.project_id location = var.region repository_id = "main" description = "main docker repository" format = "DOCKER" } ``` #### IAM for GKE / Nodes / Compute Give the compute service account that the nodes use, permissions to pull images from Artifact Registry. === "Terraform" ```terraform resource "google_project_iam_member" "compute_artifactregistry_reader" { project = var.project_id role = "roles/artifactregistry.reader" member = "serviceAccount:${var.compute_sa_email}" } ``` Allow GKE node pools to access Artifact Registry API via OAuth scope `https://www.googleapis.com/auth/devstorage.read_only` === "Terraform" ```terraform resource "google_container_node_pool" "primary_nodes_v1" { project = var.project_id name = "${google_container_cluster.primary.name}-node-pool-v1" location = var.region cluster = google_container_cluster.primary.name ... node_config { oauth_scopes = [ ... "https://www.googleapis.com/auth/devstorage.read_only", ] ... } ... } ``` #### IAM for `k8s-image-swapper` On GKE, leverage Workload Identity for the `k8s-image-swapper` K8s service account. 1. Enable Workload Identity on the GKE cluster[^3]. === "Terraform" ```terraform resource "google_container_cluster" "primary" { ... workload_identity_config { workload_pool = "${var.project_id}.svc.id.goog" } ... } ``` 2. Setup a Google Service Account (GSA) for `k8s-image-swapper`. === "Terraform" ```terraform resource "google_service_account" "k8s_image_swapper_service_account" { project = var.project_id account_id = k8s-image-swapper display_name = "Workload identity for kube-system/k8s-image-swapper" } ``` 3. Setup Workload Identity for the GSA !!! note This example assumes `k8s-image-swapper` is deployed to the `kube-system` namespace and uses `k8s-image-swapper` as the K8s service account name. === "Terraform" ```terraform resource "google_service_account_iam_member" "k8s_image_swapper_workload_identity_binding" { service_account_id = google_service_account.k8s_image_swapper_service_account.name role = "roles/iam.workloadIdentityUser" member = "serviceAccount:${var.project_id}.svc.id.goog[kube-system/k8s-image-swapper]" depends_on = [ google_container_cluster.primary, ] } ``` 4. Bind permissions for GSA to access Artifact Registry Setup the `roles/artifactregistry.writer` role in order for `k8s-image-swapper` to be able to read/write images to the Artifact Repository. === "Terraform" ```terraform resource "google_project_iam_member" "k8s_image_swapper_service_account_binding" { project = var.project_id role = "roles/artifactregistry.writer" member = "serviceAccount:${google_service_account.k8s_image_swapper_service_account.email}" } ``` 5. (Optional) Bind additional permissions for GSA to read from other GCP Artifact Registries 6. Set Workload Identity annotation on `k8s-iamge-swapper` service account ```yaml serviceAccount: annotations: iam.gke.io/gcp-service-account: k8s-image-swapper@gcp-project-123.iam.gserviceaccount.com ``` #### Firewall If running `k8s-image-swapper` on a private GKE cluster you must have a firewall rule enabled to allow the GKE control plane to talk to `k8s-image-swapper` on port `8443`. See the following Terraform example for the firewall configuration. === "Terraform" ```terraform resource "google_compute_firewall" "k8s_image_swapper_webhook" { project = var.project_id name = "gke-${google_container_cluster.primary.name}-k8s-image-swapper-webhook" network = google_compute_network.vpc.name direction = "INGRESS" source_ranges = [google_container_cluster.primary.private_cluster_config[0].master_ipv4_cidr_block] target_tags = [google_container_cluster.primary.name] allow { ports = ["8443"] protocol = "tcp" } } ``` For more details see https://cloud.google.com/kubernetes-engine/docs/how-to/private-clusters#add_firewall_rules ## Helm 1. Add the Helm chart repository: ```bash helm repo add estahn https://estahn.github.io/charts/ ``` 2. Update the local chart information: ```bash helm repo update ``` 3. Install `k8s-image-swapper` ``` helm install k8s-image-swapper estahn/k8s-image-swapper \ --set config.target.aws.accountId=$AWS_ACCOUNT_ID \ --set config.target.aws.region=$AWS_DEFAULT_REGION \ --set awsSecretName=k8s-image-swapper-aws ``` !!! note `awsSecretName` is not required for the Service Account method and instead the service account is annotated: ```yaml serviceAccount: create: true annotations: eks.amazonaws.com/role-arn: ${oidc_image_swapper_role_arn} ``` ## Terraform Full example of helm chart deployment with AWS service account setup in Terraform. ```terraform data "aws_caller_identity" "current" { } variable "cluster_oidc_provider" { default = "oidc.eks.ap-southeast-1.amazonaws.com/id/ABCDEFGHIJKLMNOPQRSTUVWXYZ012345" description = "example oidc endpoint that is created during eks deployment" } variable "cluster_name" { default = "test" description = "name of the eks cluster being deployed to" } variable "region" { default = "ap-southeast-1" description = "name of the eks cluster being deployed to" } variable "k8s_image_swapper_namespace" { default = "kube-system" description = "namespace to install k8s-image-swapper" } variable "k8s_image_swapper_name" { default = "k8s-image-swapper" description = "name for k8s-image-swapper release and service account" } #k8s-image-swapper helm chart resource "helm_release" "k8s_image_swapper" { name = var.k8s_image_swapper_name namespace = "kube-system" repository = "https://estahn.github.io/charts/" chart = "k8s-image-swapper" keyring = "" version = "1.0.1" values = [ < `docker.io/library/nginx:latest`. [^3]: [Google Kubernetes Engine (GKE) > Documentation > Guides > Use Workload Identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) [^4]: [Google Kubernetes Engine (GKE) > Documentation > Guides > Creating a private cluster > Adding firewall rules for specific use cases](https://cloud.google.com/kubernetes-engine/docs/how-to/private-clusters#add_firewall_rules) ================================================ FILE: docs/index.md ================================================

Raiders of the Lost Ark

k8s-image-swapper

Mirror images into your own registry and swap image references automatically.

`k8s-image-swapper` is a mutating webhook for Kubernetes, downloading images into your own registry and pointing the images to that new location. It is an alternative to a [docker pull-through proxy](https://docs.docker.com/registry/recipes/mirror/). The feature set was primarily designed with Amazon ECR in mind but may work with other registries. ## Benefits Using `k8s-image-swapper` will improve the overall availability, reliability, durability and resiliency of your Kubernetes cluster by keeping 3rd-party images mirrored into your own registry. `k8s-image-swapper` will transparently consolidate all images into a single registry without the need to adjust manifests therefore reducing the impact of external registry failures, rate limiting, network issues, change or removal of images while reducing data traffic and therefore cost. **TL;DR:** * Protect against: * external registry failure ([quay.io outage](https://www.reddit.com/r/devops/comments/f9kiej/quayio_is_experiencing_an_outage/)) * image pull rate limiting ([docker.io rate limits](https://www.docker.com/blog/scaling-docker-to-serve-millions-more-developers-network-egress/)) * accidental image changes * removal of images * Use in air-gaped environments without the need to change manifests * Reduce NAT ingress traffic/cost ## How it works ![Explainer](img/k8s-image-swapper_explainer.gif) ================================================ FILE: docs/overrides/main.html ================================================ {% extends "base.html" %} {% block outdated %} You're not viewing the latest version. Click here to go to latest. {% endblock %} {% block extrahead %} {% endblock %} ================================================ FILE: go.mod ================================================ module github.com/estahn/k8s-image-swapper go 1.24.0 require ( cloud.google.com/go/artifactregistry v1.17.1 github.com/alitto/pond v1.9.2 github.com/aws/aws-sdk-go v1.55.7 github.com/containers/image/v5 v5.36.2 github.com/dgraph-io/ristretto v0.2.0 github.com/evanphx/json-patch v5.9.11+incompatible github.com/go-co-op/gocron v1.37.0 github.com/gruntwork-io/terratest v0.50.0 github.com/jmespath/go-jmespath v0.4.0 github.com/mitchellh/go-homedir v1.1.0 github.com/prometheus/client_golang v1.23.2 github.com/rs/zerolog v1.34.0 github.com/slok/kubewebhook/v2 v2.5.0 github.com/spf13/cobra v1.10.1 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 google.golang.org/api v0.250.0 gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/api v0.33.4 k8s.io/apimachinery v0.33.4 k8s.io/client-go v0.33.4 sigs.k8s.io/yaml v1.4.0 // indirect ) require ( cloud.google.com/go v0.120.0 // indirect cloud.google.com/go/auth v0.16.5 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect cloud.google.com/go/longrunning v0.6.7 // indirect dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/BurntSushi/toml v1.5.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/hcsshim v0.13.0 // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect github.com/aws/aws-sdk-go-v2/config v1.28.5 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.46 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 // indirect github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.41 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect github.com/aws/aws-sdk-go-v2/service/acm v1.30.6 // indirect github.com/aws/aws-sdk-go-v2/service/autoscaling v1.51.0 // indirect github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.65.0 // indirect github.com/aws/aws-sdk-go-v2/service/dynamodb v1.37.1 // indirect github.com/aws/aws-sdk-go-v2/service/ec2 v1.193.0 // indirect github.com/aws/aws-sdk-go-v2/service/ecr v1.36.6 // indirect github.com/aws/aws-sdk-go-v2/service/ecs v1.52.0 // indirect github.com/aws/aws-sdk-go-v2/service/iam v1.38.1 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.5 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect github.com/aws/aws-sdk-go-v2/service/kms v1.37.6 // indirect github.com/aws/aws-sdk-go-v2/service/lambda v1.88.5 // indirect github.com/aws/aws-sdk-go-v2/service/rds v1.91.0 // indirect github.com/aws/aws-sdk-go-v2/service/route53 v1.46.2 // indirect github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 // indirect github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.6 // indirect github.com/aws/aws-sdk-go-v2/service/sns v1.33.6 // indirect github.com/aws/aws-sdk-go-v2/service/sqs v1.37.1 // indirect github.com/aws/aws-sdk-go-v2/service/ssm v1.56.0 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 // indirect github.com/aws/smithy-go v1.24.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/containerd/cgroups/v3 v3.0.5 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect github.com/containers/ocicrypt v1.2.1 // indirect github.com/containers/storage v1.59.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker v28.3.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.1 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/gonvenience/bunt v1.3.5 // indirect github.com/gonvenience/neat v1.3.12 // indirect github.com/gonvenience/term v1.0.2 // indirect github.com/gonvenience/text v1.0.7 // indirect github.com/gonvenience/wrap v1.1.2 // indirect github.com/gonvenience/ytbx v1.4.4 // indirect github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-containerregistry v0.20.3 // indirect github.com/google/go-intervals v0.0.2 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/gruntwork-io/go-commons v0.8.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-getter/v2 v2.2.3 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/hcl/v2 v2.22.0 // indirect github.com/hashicorp/terraform-json v0.23.0 // indirect github.com/homeport/dyff v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.7.1 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326 // indirect github.com/mistifyio/go-zfs/v3 v3.0.1 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/hashstructure v1.1.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/spdystream v0.5.0 // indirect github.com/moby/sys/capability v0.4.0 // indirect github.com/moby/sys/mountinfo v0.7.2 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opencontainers/runtime-spec v1.2.1 // indirect github.com/opencontainers/selinux v1.12.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pquerna/otp v1.4.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sergi/go-diff v1.3.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/sylabs/sif/v2 v2.21.1 // indirect github.com/tchap/go-patricia/v2 v2.3.3 // indirect github.com/texttheater/golang-levenshtein v1.0.1 // indirect github.com/tmccombs/hcl2json v0.6.4 // indirect github.com/ulikunitz/xz v0.5.14 // indirect github.com/urfave/cli v1.22.16 // indirect github.com/vbatts/tar-split v0.12.1 // indirect github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zclconf/go-cty v1.15.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect go.opentelemetry.io/otel v1.39.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect go.opentelemetry.io/otel/metric v1.39.0 // indirect go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.7.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.46.0 // indirect golang.org/x/mod v0.30.0 // indirect golang.org/x/net v0.48.0 // indirect golang.org/x/oauth2 v0.34.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.39.0 // indirect golang.org/x/term v0.38.0 // indirect golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.13.0 // indirect golang.org/x/tools v0.39.0 // indirect gomodules.xyz/jsonpatch/v3 v3.0.1 // indirect gomodules.xyz/orderedmap v0.1.0 // indirect google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect ) ================================================ FILE: go.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= cloud.google.com/go/artifactregistry v1.17.1 h1:A20kj2S2HO9vlyBVyVFHPxArjxkXvLP5LjcdE7NhaPc= cloud.google.com/go/artifactregistry v1.17.1/go.mod h1:06gLv5QwQPWtaudI2fWO37gfwwRUHwxm3gA8Fe568Hc= cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI= cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA= github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/alitto/pond v1.9.2 h1:9Qb75z/scEZVCoSU+osVmQ0I0JOeLfdTDafrbcJ8CLs= github.com/alitto/pond v1.9.2/go.mod h1:xQn3P/sHTYcU/1BR3i86IGIrilcrGC2LiS+E2+CJWsI= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= github.com/aws/aws-sdk-go-v2/config v1.28.5 h1:Za41twdCXbuyyWv9LndXxZZv3QhTG1DinqlFsSuvtI0= github.com/aws/aws-sdk-go-v2/config v1.28.5/go.mod h1:4VsPbHP8JdcdUDmbTVgNL/8w9SqOkM5jyY8ljIxLO3o= github.com/aws/aws-sdk-go-v2/credentials v1.17.46 h1:AU7RcriIo2lXjUfHFnFKYsLCwgbz1E7Mm95ieIRDNUg= github.com/aws/aws-sdk-go-v2/credentials v1.17.46/go.mod h1:1FmYyLGL08KQXQ6mcTlifyFXfJVCNJTVGuQP4m0d/UA= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 h1:sDSXIrlsFSFJtWKLQS4PUWRvrT580rrnuLydJrCQ/yA= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20/go.mod h1:WZ/c+w0ofps+/OUqMwWgnfrgzZH1DZO1RIkktICsqnY= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.41 h1:hqcxMc2g/MwwnRMod9n6Bd+t+9Nf7d5qRg7RaXKPd6o= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.41/go.mod h1:d1eH0VrttvPmrCraU68LOyNdu26zFxQFjrVSb5vdhog= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y= github.com/aws/aws-sdk-go-v2/service/acm v1.30.6 h1:fDg0RlN30Xf/yYzEUL/WXqhmgFsjVb/I3230oCfyI5w= github.com/aws/aws-sdk-go-v2/service/acm v1.30.6/go.mod h1:zRR6jE3v/TcbfO8C2P+H0Z+kShiKKVaVyoIl8NQRjyg= github.com/aws/aws-sdk-go-v2/service/autoscaling v1.51.0 h1:1KzQVZi7OTixxaVJ8fWaJAUBjme+iQ3zBOCZhE4RgxQ= github.com/aws/aws-sdk-go-v2/service/autoscaling v1.51.0/go.mod h1:I1+/2m+IhnK5qEbhS3CrzjeiVloo9sItE/2K+so0fkU= github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.65.0 h1:3yaFbUbuLfN8n1q01wZtQtHRzUDc/jm0VvniMY0IPE8= github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.65.0/go.mod h1:PobeppEnIjw4pcgjFryNDZCTH7AiqZw0yb5r98Gvf9c= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.37.1 h1:vucMirlM6D+RDU8ncKaSZ/5dGrXNajozVwpmWNPn2gQ= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.37.1/go.mod h1:fceORfs010mNxZbQhfqUjUeHlTwANmIT4mvHamuUaUg= github.com/aws/aws-sdk-go-v2/service/ec2 v1.193.0 h1:RhSoBFT5/8tTmIseJUXM6INTXTQDF8+0oyxWBnozIms= github.com/aws/aws-sdk-go-v2/service/ec2 v1.193.0/go.mod h1:mzj8EEjIHSN2oZRXiw1Dd+uB4HZTl7hC8nBzX9IZMWw= github.com/aws/aws-sdk-go-v2/service/ecr v1.36.6 h1:zg+3FGHA0PBs0KM25qE/rOf2o5zsjNa1g/Qq83+SDI0= github.com/aws/aws-sdk-go-v2/service/ecr v1.36.6/go.mod h1:ZSq54Z9SIsOTf1Efwgw1msilSs4XVEfVQiP9nYVnKpM= github.com/aws/aws-sdk-go-v2/service/ecs v1.52.0 h1:7/vgFWplkusJN/m+3QOa+W9FNRqa8ujMPNmdufRaJpg= github.com/aws/aws-sdk-go-v2/service/ecs v1.52.0/go.mod h1:dPTOvmjJQ1T7Q+2+Xs2KSPrMvx+p0rpyV+HsQVnUK4o= github.com/aws/aws-sdk-go-v2/service/iam v1.38.1 h1:hfkzDZHBp9jAT4zcd5mtqckpU4E3Ax0LQaEWWk1VgN8= github.com/aws/aws-sdk-go-v2/service/iam v1.38.1/go.mod h1:u36ahDtZcQHGmVm/r+0L1sfKX4fzLEMdCqiKRKkUMVM= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM= github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.5 h1:3Y457U2eGukmjYjeHG6kanZpDzJADa2m0ADqnuePYVQ= github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.5/go.mod h1:CfwEHGkTjYZpkQ/5PvcbEtT7AJlG68KkEvmtwU8z3/U= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ= github.com/aws/aws-sdk-go-v2/service/kms v1.37.6 h1:CZImQdb1QbU9sGgJ9IswhVkxAcjkkD1eQTMA1KHWk+E= github.com/aws/aws-sdk-go-v2/service/kms v1.37.6/go.mod h1:YJDdlK0zsyxVBxGU48AR/Mi8DMrGdc1E3Yij4fNrONA= github.com/aws/aws-sdk-go-v2/service/lambda v1.88.5 h1:HWN7xwaV7Zwrn3Jlauio4u4aTMFgRzG2fblHWQeir/k= github.com/aws/aws-sdk-go-v2/service/lambda v1.88.5/go.mod h1:6HBXRyFFqOw+ALkJ6YGHfrr20/YXYv6X9pcZErXRvCA= github.com/aws/aws-sdk-go-v2/service/rds v1.91.0 h1:eqHz3Uih+gb0vLE5Cc4Xf733vOxsxDp6GFUUVQU4d7w= github.com/aws/aws-sdk-go-v2/service/rds v1.91.0/go.mod h1:h2jc7IleH3xHY7y+h8FH7WAZcz3IVLOB6/jXotIQ/qU= github.com/aws/aws-sdk-go-v2/service/route53 v1.46.2 h1:wmt05tPp/CaRZpPV5B4SaJ5TwkHKom07/BzHoLdkY1o= github.com/aws/aws-sdk-go-v2/service/route53 v1.46.2/go.mod h1:d+K9HESMpGb1EU9/UmmpInbGIUcAkwmcY6ZO/A3zZsw= github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 h1:HwxWTbTrIHm5qY+CAEur0s/figc3qwvLWsNkF4RPToo= github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.6 h1:1KDMKvOKNrpD667ORbZ/+4OgvUoaok1gg/MLzrHF9fw= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.6/go.mod h1:DmtyfCfONhOyVAJ6ZMTrDSFIeyCBlEO93Qkfhxwbxu0= github.com/aws/aws-sdk-go-v2/service/sns v1.33.6 h1:lEUtRHICiXsd7VRwRjXaY7MApT2X4Ue0Mrwe6XbyBro= github.com/aws/aws-sdk-go-v2/service/sns v1.33.6/go.mod h1:SODr0Lu3lFdT0SGsGX1TzFTapwveBrT5wztVoYtppm8= github.com/aws/aws-sdk-go-v2/service/sqs v1.37.1 h1:39WvSrVq9DD6UHkD+fx5x19P5KpRQfNdtgReDVNbelc= github.com/aws/aws-sdk-go-v2/service/sqs v1.37.1/go.mod h1:3gwPzC9LER/BTQdQZ3r6dUktb1rSjABF1D3Sr6nS7VU= github.com/aws/aws-sdk-go-v2/service/ssm v1.56.0 h1:mADKqoZaodipGgiZfuAjtlcr4IVBtXPZKVjkzUZCCYM= github.com/aws/aws-sdk-go-v2/service/ssm v1.56.0/go.mod h1:l9qF25TzH95FhcIak6e4vt79KE4I7M2Nf59eMUVjj6c= github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 h1:3zu537oLmsPfDMyjnUS2g+F2vITgy5pB74tHI+JBNoM= github.com/aws/aws-sdk-go-v2/service/sso v1.24.6/go.mod h1:WJSZH2ZvepM6t6jwu4w/Z45Eoi75lPN7DcydSRtJg6Y= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 h1:K0OQAsDywb0ltlFrZm0JHPY3yZp/S9OaoLU33S7vPS8= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5/go.mod h1:ORITg+fyuMoeiQFiVGoqB3OydVTLkClw/ljbblMq6Cc= github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 h1:6SZUVRQNvExYlMLbHdlKB48x0fLbc2iVROyaNEwBHbU= github.com/aws/aws-sdk-go-v2/service/sts v1.33.1/go.mod h1:GqWyYCwLXnlUB1lOAXQyNSPqPLQJvmo8J0DWBzp9mtg= github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= 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/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= github.com/containers/image/v5 v5.36.2 h1:GcxYQyAHRF/pLqR4p4RpvKllnNL8mOBn0eZnqJbfTwk= github.com/containers/image/v5 v5.36.2/go.mod h1:b4GMKH2z/5t6/09utbse2ZiLK/c72GuGLFdp7K69eA4= github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA= github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY= github.com/containers/ocicrypt v1.2.1 h1:0qIOTT9DoYwcKmxSt8QJt+VzMY18onl9jUXsxpVhSmM= github.com/containers/ocicrypt v1.2.1/go.mod h1:aD0AAqfMp0MtwqWgHM1bUwe1anx0VazI108CRrSKINQ= github.com/containers/storage v1.59.1 h1:11Zu68MXsEQGBBd+GadPrHPpWeqjKS8hJDGiAHgIqDs= github.com/containers/storage v1.59.1/go.mod h1:KoAYHnAjP3/cTsRS+mmWZGkufSY2GACiKQ4V3ZLQnR0= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE= github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/cli v28.3.2+incompatible h1:mOt9fcLE7zaACbxW1GeS65RI67wIJrTnqS3hP2huFsY= github.com/docker/cli v28.3.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0 h1:skJKxRtNmevLqnayafdLe2AsenqRupVmzZSqrvb5caU= github.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M= github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 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-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 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.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 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.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/gonvenience/bunt v1.3.5 h1:wSQquifvwEWtzn27k1ngLfeLaStyt0k1b/K6TrlCNAs= github.com/gonvenience/bunt v1.3.5/go.mod h1:7ApqkVBEWvX04oJ28Q2WeI/BvJM6VtukaJAU/q/pTs8= github.com/gonvenience/neat v1.3.12 h1:xwIyRbJcG9LgcDYys+HHLH9DqqHeQsUpS5CfBUeskbs= github.com/gonvenience/neat v1.3.12/go.mod h1:8OljAIgPelN0uPPO94VBqxK+Kz98d6ZFwHDg5o/PfkE= github.com/gonvenience/term v1.0.2 h1:qKa2RydbWIrabGjR/fegJwpW5m+JvUwFL8mLhHzDXn0= github.com/gonvenience/term v1.0.2/go.mod h1:wThTR+3MzWtWn7XGVW6qQ65uaVf8GHED98KmwpuEQeo= github.com/gonvenience/text v1.0.7 h1:YmIqmgTwxnACYCG59DykgMbomwteYyNhAmEUEJtPl14= github.com/gonvenience/text v1.0.7/go.mod h1:OAjH+mohRszffLY6OjgQcUXiSkbrIavooFpfIt1ZwAs= github.com/gonvenience/wrap v1.1.2 h1:xPKxNwL1HCguwyM+HlP/1CIuc9LRd7k8RodLwe9YTZA= github.com/gonvenience/wrap v1.1.2/go.mod h1:GiryBSXoI3BAAhbWD1cZVj7RZmtiu0ERi/6R6eJfslI= github.com/gonvenience/ytbx v1.4.4 h1:jQopwyaLsVGuwdxSiN4WkXjsEaFNPJ3V4lUj7eyEpzo= github.com/gonvenience/ytbx v1.4.4/go.mod h1:w37+MKCPcCMY/jpPNmEklD4xKqrOAVBO6kIWW2+uI6M= github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= 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.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI= github.com/google/go-intervals v0.0.2 h1:FGrVEiUnTRKR8yE04qzXYaJMtnIYqobR5QbblK3ixcM= github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/gruntwork-io/go-commons v0.8.0 h1:k/yypwrPqSeYHevLlEDmvmgQzcyTwrlZGRaxEM6G0ro= github.com/gruntwork-io/go-commons v0.8.0/go.mod h1:gtp0yTtIBExIZp7vyIV9I0XQkVwiQZze678hvDXof78= github.com/gruntwork-io/terratest v0.50.0 h1:AbBJ7IRCpLZ9H4HBrjeoWESITv8nLjN6/f1riMNcAsw= github.com/gruntwork-io/terratest v0.50.0/go.mod h1:see0lbKvAqz6rvzvN2wyfuFQQG4PWcAb2yHulF6B2q4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-getter/v2 v2.2.3 h1:6CVzhT0KJQHqd9b0pK3xSP0CM/Cv+bVhk+jcaRJ2pGk= github.com/hashicorp/go-getter/v2 v2.2.3/go.mod h1:hp5Yy0GMQvwWVUmwLs3ygivz1JSLI323hdIE9J9m7TY= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M= github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/terraform-json v0.23.0 h1:sniCkExU4iKtTADReHzACkk8fnpQXrdD2xoR+lppBkI= github.com/hashicorp/terraform-json v0.23.0/go.mod h1:MHdXbBAbSg0GvzuWazEGKAn/cyNfIB7mN6y7KJN6y2c= github.com/homeport/dyff v1.6.0 h1:AN+ikld0Fy+qx34YE7655b/bpWuxS6cL9k852pE2GUc= github.com/homeport/dyff v1.6.0/go.mod h1:FlAOFYzeKvxmU5nTrnG+qrlJVWpsFew7pt8L99p5q8k= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a h1:zPPuIq2jAWWPTrGt70eK/BSch+gFAGrNzecsoENgu2o= github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s= 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.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 h1:BXxTozrOU8zgC5dkpn3J6NTRdoP+hjok/e+ACr4Hibk= github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3/go.mod h1:x1uk6vxTiVuNt6S5R2UYgdhpj3oKojXvOXauHZ7dEnI= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= github.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326 h1:ofNAzWCcyTALn2Zv40+8XitdzCgXY6e9qvXwN9W0YXg= github.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= github.com/mistifyio/go-zfs/v3 v3.0.1 h1:YaoXgBePoMA12+S1u/ddkv+QqxcfiZK4prI6HPnkFiU= github.com/mistifyio/go-zfs/v3 v3.0.1/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9kmUZPXP+H0= github.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/6d8ulp4AwfLKrmA= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk= github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/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/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 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.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww= github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8= github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/sebdah/goldie v1.0.0 h1:9GNhIat69MSlz/ndaBg48vl9dF5fI+NBB6kfOxgfkMc= github.com/sebdah/goldie/v2 v2.5.5 h1:rx1mwF95RxZ3/83sdS4Yp7t2C5TCokvWP4TBRbAyEWY= github.com/sebdah/goldie/v2 v2.5.5/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= 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/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/slok/kubewebhook/v2 v2.5.0 h1:CwMxLbTEcha3+SxSXc4pc9iIbREdhgLurAs+/uRzxIw= github.com/slok/kubewebhook/v2 v2.5.0/go.mod h1:TcQS+Ae0TDiiwm9glxum6AFvtumR33qdAenUeiQ/TWs= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= 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/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/sylabs/sif/v2 v2.21.1 h1:GZ0b5//AFAqJEChd8wHV/uSKx/l1iuGYwjR8nx+4wPI= github.com/sylabs/sif/v2 v2.21.1/go.mod h1:YoqEGQnb5x/ItV653bawXHZJOXQaEWpGwHsSD3YePJI= github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc= github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U= github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8= github.com/tmccombs/hcl2json v0.6.4 h1:/FWnzS9JCuyZ4MNwrG4vMrFrzRgsWEOVi+1AyYUVLGw= github.com/tmccombs/hcl2json v0.6.4/go.mod h1:+ppKlIW3H5nsAsZddXPy2iMyvld3SHxyjswOZhavRDk= github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg= github.com/ulikunitz/xz v0.5.14/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 h1:JwtAtbp7r/7QSyGz8mKUbYJBg2+6Cd7OjM8o/GNOcVo= github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74/go.mod h1:RmMWU37GKR2s6pgrIEB4ixgpVCt/cf7dnJv3fuH1J1c= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ= github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= 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.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 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-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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 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-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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= 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= gomodules.xyz/jsonpatch/v3 v3.0.1 h1:Te7hKxV52TKCbNYq3t84tzKav3xhThdvSsSp/W89IyI= gomodules.xyz/jsonpatch/v3 v3.0.1/go.mod h1:CBhndykehEwTOlEfnsfJwvkFQbSN8YZFr9M+cIHAJto= gomodules.xyz/orderedmap v0.1.0 h1:fM/+TGh/O1KkqGR5xjTKg6bU8OKBkg7p0Y+x/J9m8Os= gomodules.xyz/orderedmap v0.1.0/go.mod h1:g9/TPUCm1t2gwD3j3zfV8uylyYhVdCNSi+xCEIu7yTU= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.250.0 h1:qvkwrf/raASj82UegU2RSDGWi/89WkLckn4LuO4lVXM= google.golang.org/api v0.250.0/go.mod h1:Y9Uup8bDLJJtMzJyQnu+rLRJLA0wn+wTtc6vTlOvfXo= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= 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.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= 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.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= 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.33.4 h1:oTzrFVNPXBjMu0IlpA2eDDIU49jsuEorGHB4cvKupkk= k8s.io/api v0.33.4/go.mod h1:VHQZ4cuxQ9sCUMESJV5+Fe8bGnqAARZ08tSTdHWfeAc= k8s.io/apimachinery v0.33.4 h1:SOf/JW33TP0eppJMkIgQ+L6atlDiP/090oaX0y9pd9s= k8s.io/apimachinery v0.33.4/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= k8s.io/client-go v0.33.4 h1:TNH+CSu8EmXfitntjUPwaKVPN0AYMbc9F1bBS8/ABpw= k8s.io/client-go v0.33.4/go.mod h1:LsA0+hBG2DPwovjd931L/AoaezMPX9CmBgyVyBZmbCY= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= ================================================ FILE: main.go ================================================ /* Copyright © 2020 Enrico Stahn Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package main import "github.com/estahn/k8s-image-swapper/cmd" func main() { cmd.Execute() } ================================================ FILE: mkdocs.yml ================================================ # Project information site_name: k8s-image-swapper site_url: https://estahn.github.io/k8s-image-swapper/ site_author: Enrico Stahn site_description: >- Mirror images into your own registry and swap image references automatically. # Repository repo_name: estahn/k8s-image-swapper repo_url: https://github.com/estahn/k8s-image-swapper edit_uri: "blob/main/docs/" # Copyright copyright: Copyright © 2020 Enrico Stahn use_directory_urls: false theme: name: material custom_dir: docs/overrides palette: # Palette toggle for automatic mode - media: "(prefers-color-scheme)" toggle: icon: material/brightness-auto name: Switch to light mode # Palette toggle for light mode - media: "(prefers-color-scheme: light)" scheme: default toggle: icon: material/brightness-7 name: Switch to dark mode # Palette toggle for dark mode - media: "(prefers-color-scheme: dark)" scheme: slate toggle: icon: material/brightness-4 name: Switch to system preference # Don't include MkDocs' JavaScript include_search_page: false search_index_only: true # Default values, taken from mkdocs_theme.yml language: en features: - tabs - content.action.edit - content.code.copy - navigation.footer # Plugins plugins: - search - minify: minify_html: true - markdownextradata: {} - social # Extensions markdown_extensions: - admonition - attr_list - md_in_html - codehilite: guess_lang: false - def_list - footnotes - meta - toc: permalink: true - pymdownx.arithmatex - pymdownx.betterem: smart_enable: all - pymdownx.caret - pymdownx.critic - pymdownx.details - pymdownx.emoji - pymdownx.highlight: use_pygments: true linenums_style: pymdownx-inline anchor_linenums: true - pymdownx.inlinehilite - pymdownx.keys - pymdownx.magiclink: repo_url_shorthand: true user: squidfunk repo: mkdocs-material - pymdownx.mark - pymdownx.smartsymbols - pymdownx.snippets: check_paths: true - pymdownx.superfences - pymdownx.tabbed: alternate_style: true - pymdownx.tasklist: custom_checkbox: true - pymdownx.tilde nav: - Home: index.md - Getting started: getting-started.md - Configuration: configuration.md - FAQ: faq.md # - Releases: # - 1.3.0: releases/1.3.0-NOTES.md # - Operations: # - Production considerations: foo # - Contributing: # - Testing: testing.md # - Contributors: constributors.md extra: version: provider: mike default: latest social: - icon: fontawesome/brands/github link: https://github.com/estahn/k8s-image-swapper - icon: fontawesome/brands/docker link: https://github.com/estahn/k8s-image-swapper/pkgs/container/k8s-image-swapper - icon: fontawesome/brands/slack link: https://kubernetes.slack.com/archives/C04LETF7KEC - icon: fontawesome/brands/twitter link: https://twitter.com/estahn - icon: fontawesome/brands/linkedin link: https://www.linkedin.com/in/enricostahn analytics: provider: google property: G-BK225DNZVM feedback: title: Was this page helpful? ratings: - icon: material/emoticon-happy-outline name: This page was helpful data: 1 note: >- Thanks for your feedback! - icon: material/emoticon-sad-outline name: This page could be improved data: 0 note: >- Thanks for your feedback! Help us improve this page by using our feedback form. ================================================ FILE: package.json ================================================ { "devDependencies": { "@semantic-release/changelog": "^6.0.3", "@semantic-release/exec": "^7.1.0", "@semantic-release/git": "^10.0.1", "conventional-changelog-conventionalcommits": "^9.3.1", "semantic-release": "^25.0.3" } } ================================================ FILE: pkg/config/config.go ================================================ /* Copyright © 2020 Enrico Stahn Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package config import ( "fmt" "strings" "time" "github.com/spf13/viper" "github.com/estahn/k8s-image-swapper/pkg/types" ) const DefaultImageCopyDeadline = 8 * time.Second type Config struct { LogLevel string `yaml:"logLevel" validate:"oneof=trace debug info warn error fatal"` LogFormat string `yaml:"logFormat" validate:"oneof=json console"` ListenAddress string DryRun bool `yaml:"dryRun"` ImageSwapPolicy string `yaml:"imageSwapPolicy" validate:"oneof=always exists"` ImageCopyPolicy string `yaml:"imageCopyPolicy" validate:"oneof=delayed immediate force none"` ImageCopyDeadline time.Duration `yaml:"imageCopyDeadline"` Source Source `yaml:"source"` Target Registry `yaml:"target"` TLSCertFile string TLSKeyFile string } type JMESPathFilter struct { JMESPath string `yaml:"jmespath"` } type Source struct { Registries []Registry `yaml:"registries"` Filters []JMESPathFilter `yaml:"filters"` } type Registry struct { Type string `yaml:"type"` AWS AWS `yaml:"aws"` GCP GCP `yaml:"gcp"` } type AWS struct { AccountID string `yaml:"accountId"` Region string `yaml:"region"` Role string `yaml:"role"` Prefix string `yaml:"prefix"` ECROptions ECROptions `yaml:"ecrOptions"` } type GCP struct { Location string `yaml:"location"` ProjectID string `yaml:"projectId"` RepositoryID string `yaml:"repositoryId"` } type ECROptions struct { AccessPolicy string `yaml:"accessPolicy"` LifecyclePolicy string `yaml:"lifecyclePolicy"` Tags []Tag `yaml:"tags"` ImageTagMutability string `yaml:"imageTagMutability" validate:"oneof=MUTABLE IMMUTABLE"` ImageScanningConfiguration ImageScanningConfiguration `yaml:"imageScanningConfiguration"` EncryptionConfiguration EncryptionConfiguration `yaml:"encryptionConfiguration"` } type Tag struct { Key string `yaml:"key"` Value string `yaml:"value"` } type ImageScanningConfiguration struct { ImageScanOnPush bool `yaml:"imageScanOnPush"` } type EncryptionConfiguration struct { EncryptionType string `yaml:"encryptionType" validate:"oneof=KMS AES256"` KmsKey string `yaml:"kmsKey"` } func (a *AWS) EcrDomain() string { domain := "amazonaws.com" if strings.HasPrefix(a.Region, "cn-") { domain = "amazonaws.com.cn" } return fmt.Sprintf("%s.dkr.ecr.%s.%s%s", a.AccountID, a.Region, domain, a.Prefix) } func (g *GCP) GarDomain() string { return fmt.Sprintf("%s-docker.pkg.dev/%s/%s", g.Location, g.ProjectID, g.RepositoryID) } func (r Registry) Domain() string { registry, _ := types.ParseRegistry(r.Type) switch registry { case types.RegistryAWS: return r.AWS.EcrDomain() case types.RegistryGCP: return r.GCP.GarDomain() default: return "" } } // provides detailed information about wrongly provided configuration func CheckRegistryConfiguration(r Registry) error { if r.Type == "" { return fmt.Errorf("a registry requires a type") } errorWithType := func(info string) error { return fmt.Errorf(`registry of type "%s" %s`, r.Type, info) } registry, _ := types.ParseRegistry(r.Type) switch registry { case types.RegistryAWS: if r.AWS.Region == "" { return errorWithType(`requires a field "region"`) } if r.AWS.AccountID == "" { return errorWithType(`requires a field "accountdId"`) } if r.AWS.ECROptions.EncryptionConfiguration.EncryptionType == "KMS" && r.AWS.ECROptions.EncryptionConfiguration.KmsKey == "" { return errorWithType(`requires a field "kmsKey" if encryptionType is set to "KMS"`) } case types.RegistryGCP: if r.GCP.Location == "" { return errorWithType(`requires a field "location"`) } if r.GCP.ProjectID == "" { return errorWithType(`requires a field "projectId"`) } if r.GCP.RepositoryID == "" { return errorWithType(`requires a field "repositoryId"`) } } return nil } // SetViperDefaults configures default values for config items that are not set. func SetViperDefaults(v *viper.Viper) { v.SetDefault("Target.Type", "aws") v.SetDefault("Target.AWS.ECROptions.ImageScanningConfiguration.ImageScanOnPush", true) v.SetDefault("Target.AWS.ECROptions.ImageTagMutability", "MUTABLE") v.SetDefault("Target.AWS.ECROptions.EncryptionConfiguration.EncryptionType", "AES256") } ================================================ FILE: pkg/config/config_test.go ================================================ package config import ( "strings" "testing" "github.com/spf13/viper" "github.com/stretchr/testify/assert" ) // TestConfigParses validates if yaml annotation do not overlap func TestConfigParses(t *testing.T) { tests := []struct { name string cfg string expCfg Config expErr bool }{ { name: "should render empty config with defaults", cfg: "", expCfg: Config{ Target: Registry{ Type: "aws", AWS: AWS{ ECROptions: ECROptions{ ImageTagMutability: "MUTABLE", ImageScanningConfiguration: ImageScanningConfiguration{ ImageScanOnPush: true, }, EncryptionConfiguration: EncryptionConfiguration{ EncryptionType: "AES256", }, }, }, }, }, }, { name: "should render multiple filters", cfg: ` source: filters: - jmespath: "obj.metadata.namespace == 'kube-system'" - jmespath: "obj.metadata.namespace != 'playground'" `, expCfg: Config{ Target: Registry{ Type: "aws", AWS: AWS{ ECROptions: ECROptions{ ImageTagMutability: "MUTABLE", ImageScanningConfiguration: ImageScanningConfiguration{ ImageScanOnPush: true, }, EncryptionConfiguration: EncryptionConfiguration{ EncryptionType: "AES256", }, }, }, }, Source: Source{ Filters: []JMESPathFilter{ {JMESPath: "obj.metadata.namespace == 'kube-system'"}, {JMESPath: "obj.metadata.namespace != 'playground'"}, }, }, }, }, { name: "should render tags config", cfg: ` target: type: aws aws: accountId: 123456789 region: ap-southeast-2 role: arn:aws:iam::123456789012:role/roleName ecrOptions: tags: - key: CreatedBy value: k8s-image-swapper - key: A value: B `, expCfg: Config{ Target: Registry{ Type: "aws", AWS: AWS{ AccountID: "123456789", Region: "ap-southeast-2", Role: "arn:aws:iam::123456789012:role/roleName", ECROptions: ECROptions{ ImageTagMutability: "MUTABLE", ImageScanningConfiguration: ImageScanningConfiguration{ ImageScanOnPush: true, }, EncryptionConfiguration: EncryptionConfiguration{ EncryptionType: "AES256", }, Tags: []Tag{ { Key: "CreatedBy", Value: "k8s-image-swapper", }, { Key: "A", Value: "B", }, }, }, }, }, }, }, { name: "should render multiple source registries", cfg: ` source: registries: - type: "aws" aws: accountId: "12345678912" region: "us-west-1" - type: "aws" aws: accountId: "12345678912" region: "us-east-1" `, expCfg: Config{ Target: Registry{ Type: "aws", AWS: AWS{ ECROptions: ECROptions{ ImageTagMutability: "MUTABLE", ImageScanningConfiguration: ImageScanningConfiguration{ ImageScanOnPush: true, }, EncryptionConfiguration: EncryptionConfiguration{ EncryptionType: "AES256", }, }, }, }, Source: Source{ Registries: []Registry{ { Type: "aws", AWS: AWS{ AccountID: "12345678912", Region: "us-west-1", }}, { Type: "aws", AWS: AWS{ AccountID: "12345678912", Region: "us-east-1", }}, }, }, }, }, { name: "should use previous defaults", cfg: ` target: type: aws aws: accountId: 123456789 region: ap-southeast-2 role: arn:aws:iam::123456789012:role/roleName ecrOptions: tags: - key: CreatedBy value: k8s-image-swapper - key: A value: B `, expCfg: Config{ Target: Registry{ Type: "aws", AWS: AWS{ AccountID: "123456789", Region: "ap-southeast-2", Role: "arn:aws:iam::123456789012:role/roleName", ECROptions: ECROptions{ ImageScanningConfiguration: ImageScanningConfiguration{ ImageScanOnPush: true, }, EncryptionConfiguration: EncryptionConfiguration{ EncryptionType: "AES256", }, ImageTagMutability: "MUTABLE", Tags: []Tag{ { Key: "CreatedBy", Value: "k8s-image-swapper", }, { Key: "A", Value: "B", }, }, }, }, }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { assert := assert.New(t) v := viper.New() v.SetConfigType("yaml") SetViperDefaults(v) readConfigError := v.ReadConfig(strings.NewReader(test.cfg)) assert.NoError(readConfigError) gotCfg := Config{} err := v.Unmarshal(&gotCfg) if test.expErr { assert.Error(err) } else if assert.NoError(err) { assert.Equal(test.expCfg, gotCfg) } }) } } func TestEcrDomain(t *testing.T) { tests := []struct { name string cfg Config domain string }{ { name: "commercial aws", cfg: Config{ Target: Registry{ Type: "aws", AWS: AWS{ AccountID: "123456789", Region: "ap-southeast-2", }, }, }, domain: "123456789.dkr.ecr.ap-southeast-2.amazonaws.com", }, { name: "aws in china", cfg: Config{ Target: Registry{ Type: "aws", AWS: AWS{ AccountID: "123456789", Region: "cn-north-1", }, }, }, domain: "123456789.dkr.ecr.cn-north-1.amazonaws.com.cn", }, { name: "aws with prefix", cfg: Config{ Target: Registry{ Type: "aws", AWS: AWS{ AccountID: "123456789", Region: "ap-southeast-2", Prefix: "/prefix", }, }, }, domain: "123456789.dkr.ecr.ap-southeast-2.amazonaws.com/prefix", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { assert := assert.New(t) assert.Equal(test.cfg.Target.AWS.EcrDomain(), test.domain) }) } } ================================================ FILE: pkg/registry/client.go ================================================ package registry import ( "context" "encoding/base64" "encoding/json" "fmt" "github.com/estahn/k8s-image-swapper/pkg/config" "github.com/estahn/k8s-image-swapper/pkg/types" ctypes "github.com/containers/image/v5/types" ) // Client provides methods required to be implemented by the various target registry clients, e.g. ECR, Docker, Quay. type Client interface { CreateRepository(ctx context.Context, name string) error RepositoryExists() bool CopyImage(ctx context.Context, src ctypes.ImageReference, srcCreds string, dest ctypes.ImageReference, destCreds string) error PullImage() error PutImage() error ImageExists(ctx context.Context, ref ctypes.ImageReference) bool // Endpoint returns the domain of the registry Endpoint() string Credentials() string // IsOrigin returns true if the imageRef originates from this registry IsOrigin(imageRef ctypes.ImageReference) bool } type DockerConfig struct { AuthConfigs map[string]AuthConfig `json:"auths"` } type AuthConfig struct { Auth string `json:"auth,omitempty"` } // NewClient returns a registry client ready for use without the need to specify an implementation func NewClient(r config.Registry) (Client, error) { if err := config.CheckRegistryConfiguration(r); err != nil { return nil, err } registry, err := types.ParseRegistry(r.Type) if err != nil { return nil, err } switch registry { case types.RegistryAWS: return NewECRClient(r.AWS) case types.RegistryGCP: return NewGARClient(r.GCP) default: return nil, fmt.Errorf(`registry of type "%s" is not supported`, r.Type) } } func GenerateDockerConfig(c Client) ([]byte, error) { dockerConfig := DockerConfig{ AuthConfigs: map[string]AuthConfig{ c.Endpoint(): { Auth: base64.StdEncoding.EncodeToString([]byte(c.Credentials())), }, }, } dockerConfigJson, err := json.Marshal(dockerConfig) if err != nil { return []byte{}, err } return dockerConfigJson, nil } ================================================ FILE: pkg/registry/ecr.go ================================================ package registry import ( "context" "encoding/base64" "fmt" "math/rand" "net/http" "os/exec" "time" "github.com/containers/image/v5/docker/reference" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/credentials/stscreds" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/ecr" "github.com/aws/aws-sdk-go/service/ecr/ecriface" ctypes "github.com/containers/image/v5/types" "github.com/dgraph-io/ristretto" "github.com/estahn/k8s-image-swapper/pkg/config" "github.com/go-co-op/gocron" "github.com/rs/zerolog/log" ) type ECRClient struct { client ecriface.ECRAPI ecrDomain string authToken []byte cache *ristretto.Cache scheduler *gocron.Scheduler targetAccount string options config.ECROptions } func NewECRClient(clientConfig config.AWS) (*ECRClient, error) { ecrDomain := clientConfig.EcrDomain() var sess *session.Session var cfg *aws.Config if clientConfig.Role != "" { log.Info().Str("assumedRole", clientConfig.Role).Msg("assuming specified role") stsSession, _ := session.NewSession(cfg) creds := stscreds.NewCredentials(stsSession, clientConfig.Role) cfg = aws.NewConfig(). WithRegion(clientConfig.Region). WithCredentialsChainVerboseErrors(true). WithHTTPClient(&http.Client{ Timeout: 3 * time.Second, }). WithCredentials(creds) } else { cfg = aws.NewConfig(). WithRegion(clientConfig.Region). WithCredentialsChainVerboseErrors(true). WithHTTPClient(&http.Client{ Timeout: 3 * time.Second, }) } sess = session.Must(session.NewSessionWithOptions(session.Options{ SharedConfigState: session.SharedConfigEnable, Config: *cfg, })) ecrClient := ecr.New(sess, cfg) cache, err := ristretto.NewCache(&ristretto.Config{ NumCounters: 1e7, // number of keys to track frequency of (10M). MaxCost: 1 << 30, // maximum cost of cache (1GB). BufferItems: 64, // number of keys per Get buffer. }) if err != nil { panic(err) } scheduler := gocron.NewScheduler(time.UTC) scheduler.StartAsync() client := &ECRClient{ client: ecrClient, ecrDomain: ecrDomain, cache: cache, scheduler: scheduler, targetAccount: clientConfig.AccountID, options: clientConfig.ECROptions, } if err := client.scheduleTokenRenewal(); err != nil { return nil, err } return client, nil } func (e *ECRClient) Credentials() string { return string(e.authToken) } func (e *ECRClient) CreateRepository(ctx context.Context, name string) error { if _, found := e.cache.Get(name); found { return nil } log.Ctx(ctx).Debug().Str("repository", name).Msg("create repository") encryptionConfiguration := &ecr.EncryptionConfiguration{ EncryptionType: aws.String(e.options.EncryptionConfiguration.EncryptionType), } if e.options.EncryptionConfiguration.EncryptionType == "KMS" { encryptionConfiguration.KmsKey = aws.String(e.options.EncryptionConfiguration.KmsKey) } _, err := e.client.CreateRepositoryWithContext(ctx, &ecr.CreateRepositoryInput{ RepositoryName: aws.String(name), EncryptionConfiguration: encryptionConfiguration, ImageScanningConfiguration: &ecr.ImageScanningConfiguration{ ScanOnPush: aws.Bool(e.options.ImageScanningConfiguration.ImageScanOnPush), }, ImageTagMutability: aws.String(e.options.ImageTagMutability), RegistryId: &e.targetAccount, Tags: e.buildEcrTags(), }) if err != nil { if aerr, ok := err.(awserr.Error); ok { switch aerr.Code() { case ecr.ErrCodeRepositoryAlreadyExistsException: // We ignore this case as it is valid. default: return err } } else { // Print the error, cast err to awserr.Error to get the Code and // Message from an error. return err } } if len(e.options.AccessPolicy) > 0 { log.Ctx(ctx).Debug().Str("repo", name).Str("accessPolicy", e.options.AccessPolicy).Msg("setting access policy on repo") _, err := e.client.SetRepositoryPolicyWithContext(ctx, &ecr.SetRepositoryPolicyInput{ PolicyText: &e.options.AccessPolicy, RegistryId: &e.targetAccount, RepositoryName: aws.String(name), }) if err != nil { log.Err(err).Msg(err.Error()) return err } } if len(e.options.LifecyclePolicy) > 0 { log.Ctx(ctx).Debug().Str("repo", name).Str("lifecyclePolicy", e.options.LifecyclePolicy).Msg("setting lifecycle policy on repo") _, err := e.client.PutLifecyclePolicyWithContext(ctx, &ecr.PutLifecyclePolicyInput{ LifecyclePolicyText: &e.options.LifecyclePolicy, RegistryId: &e.targetAccount, RepositoryName: aws.String(name), }) if err != nil { log.Err(err).Msg(err.Error()) return err } } e.cache.SetWithTTL(name, "", 1, time.Duration(24*time.Hour)) return nil } func (e *ECRClient) buildEcrTags() []*ecr.Tag { ecrTags := []*ecr.Tag{} for _, t := range e.options.Tags { tag := ecr.Tag{Key: aws.String(t.Key), Value: aws.String(t.Value)} ecrTags = append(ecrTags, &tag) } return ecrTags } func (e *ECRClient) RepositoryExists() bool { panic("implement me") } func (e *ECRClient) CopyImage(ctx context.Context, srcRef ctypes.ImageReference, srcCreds string, destRef ctypes.ImageReference, destCreds string) error { src := srcRef.DockerReference().String() dest := destRef.DockerReference().String() app := "skopeo" args := []string{ "--override-os", "linux", "copy", "--multi-arch", "all", "--retry-times", "3", "docker://" + src, "docker://" + dest, } if len(srcCreds) > 0 { args = append(args, "--src-authfile", srcCreds) } else { args = append(args, "--src-no-creds") } if len(destCreds) > 0 { args = append(args, "--dest-creds", destCreds) } else { args = append(args, "--dest-no-creds") } log.Ctx(ctx). Trace(). Str("app", app). Strs("args", args). Msg("execute command to copy image") output, cmdErr := exec.CommandContext(ctx, app, args...).CombinedOutput() // check if the command timed out during execution for proper logging if err := ctx.Err(); err != nil { return err } // enrich error with output from the command which may contain the actual reason if cmdErr != nil { return fmt.Errorf("command error, stderr: %s, stdout: %s", cmdErr.Error(), string(output)) } return nil } func (e *ECRClient) PullImage() error { panic("implement me") } func (e *ECRClient) PutImage() error { panic("implement me") } func (e *ECRClient) ImageExists(ctx context.Context, imageRef ctypes.ImageReference) bool { ref := imageRef.DockerReference().String() if _, found := e.cache.Get(ref); found { log.Ctx(ctx).Trace().Str("ref", ref).Msg("found in cache") return true } app := "skopeo" args := []string{ "inspect", "--retry-times", "3", "docker://" + ref, "--creds", e.Credentials(), } log.Ctx(ctx).Trace().Str("app", app).Strs("args", args).Msg("executing command to inspect image") if err := exec.CommandContext(ctx, app, args...).Run(); err != nil { log.Ctx(ctx).Trace().Str("ref", ref).Msg("not found in target repository") return false } log.Ctx(ctx).Trace().Str("ref", ref).Msg("found in target repository") e.cache.SetWithTTL(ref, "", 1, 24*time.Hour+time.Duration(rand.Intn(180))*time.Minute) return true } func (e *ECRClient) Endpoint() string { return e.ecrDomain } // IsOrigin returns true if the references origin is from this registry func (e *ECRClient) IsOrigin(imageRef ctypes.ImageReference) bool { domain := reference.Domain(imageRef.DockerReference()) return domain == e.Endpoint() } // requestAuthToken requests and returns an authentication token from ECR with its expiration date func (e *ECRClient) requestAuthToken() ([]byte, time.Time, error) { getAuthTokenOutput, err := e.client.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{ RegistryIds: []*string{&e.targetAccount}, }) if err != nil { return []byte(""), time.Time{}, err } authToken, err := base64.StdEncoding.DecodeString(*getAuthTokenOutput.AuthorizationData[0].AuthorizationToken) if err != nil { return []byte(""), time.Time{}, err } return authToken, *getAuthTokenOutput.AuthorizationData[0].ExpiresAt, nil } // scheduleTokenRenewal sets a scheduler to execute token renewal before the token expires func (e *ECRClient) scheduleTokenRenewal() error { token, expiryAt, err := e.requestAuthToken() if err != nil { return err } renewalAt := expiryAt.Add(-2 * time.Minute) e.authToken = token log.Debug().Time("expiryAt", expiryAt).Time("renewalAt", renewalAt).Msg("auth token set, schedule next token renewal") j, _ := e.scheduler.Every(1).StartAt(renewalAt).Do(e.scheduleTokenRenewal) j.LimitRunsTo(1) return nil } // For testing purposes func NewDummyECRClient(region string, targetAccount string, role string, options config.ECROptions, authToken []byte) *ECRClient { return &ECRClient{ targetAccount: targetAccount, options: options, ecrDomain: fmt.Sprintf("%s.dkr.ecr.%s.amazonaws.com", targetAccount, region), authToken: authToken, } } func NewMockECRClient(ecrClient ecriface.ECRAPI, region string, ecrDomain string, targetAccount, role string) (*ECRClient, error) { client := &ECRClient{ client: ecrClient, ecrDomain: ecrDomain, cache: nil, scheduler: nil, targetAccount: targetAccount, authToken: []byte("mock-ecr-client-fake-auth-token"), options: config.ECROptions{ ImageTagMutability: "MUTABLE", ImageScanningConfiguration: config.ImageScanningConfiguration{ImageScanOnPush: true}, EncryptionConfiguration: config.EncryptionConfiguration{EncryptionType: "AES256"}, Tags: []config.Tag{{Key: "CreatedBy", Value: "k8s-image-swapper"}, {Key: "AnotherTag", Value: "another-tag"}}, }, } return client, nil } ================================================ FILE: pkg/registry/ecr_test.go ================================================ package registry import ( "encoding/base64" "testing" "github.com/containers/image/v5/transports/alltransports" "github.com/estahn/k8s-image-swapper/pkg/config" "github.com/stretchr/testify/assert" ) func TestDockerConfig(t *testing.T) { fakeToken := []byte("token") fakeBase64Token := base64.StdEncoding.EncodeToString(fakeToken) expected := []byte("{\"auths\":{\"12345678912.dkr.ecr.us-east-1.amazonaws.com\":{\"auth\":\"" + fakeBase64Token + "\"}}}") fakeRegistry := NewDummyECRClient("us-east-1", "12345678912", "", config.ECROptions{}, fakeToken) r, _ := GenerateDockerConfig(fakeRegistry) assert.Equal(t, r, expected) } func TestECRIsOrigin(t *testing.T) { type testCase struct { input string expected bool } testcases := []testCase{ { input: "k8s.gcr.io/ingress-nginx/controller@sha256:9bba603b99bf25f6d117cf1235b6598c16033ad027b143c90fa5b3cc583c5713", expected: false, }, { input: "12345678912.dkr.ecr.us-east-1.amazonaws.com/k8s.gcr.io/ingress-nginx/controller@sha256:9bba603b99bf25f6d117cf1235b6598c16033ad027b143c90fa5b3cc583c5713", expected: true, }, } fakeRegistry := NewDummyECRClient("us-east-1", "12345678912", "", config.ECROptions{}, []byte("")) for _, testcase := range testcases { imageRef, err := alltransports.ParseImageName("docker://" + testcase.input) assert.NoError(t, err) result := fakeRegistry.IsOrigin(imageRef) assert.Equal(t, testcase.expected, result) } } ================================================ FILE: pkg/registry/gar.go ================================================ package registry import ( "context" "encoding/base64" "encoding/json" "fmt" "math/rand" "os/exec" "strings" "time" artifactregistry "cloud.google.com/go/artifactregistry/apiv1" "github.com/containers/image/v5/docker/reference" ctypes "github.com/containers/image/v5/types" "github.com/dgraph-io/ristretto" "github.com/estahn/k8s-image-swapper/pkg/config" "github.com/go-co-op/gocron" "google.golang.org/api/option" "google.golang.org/api/transport" "github.com/rs/zerolog/log" ) type GARAPI interface{} type GARClient struct { client GARAPI garDomain string cache *ristretto.Cache scheduler *gocron.Scheduler authToken []byte } func NewGARClient(clientConfig config.GCP) (*GARClient, error) { cache, err := ristretto.NewCache(&ristretto.Config{ NumCounters: 1e7, // number of keys to track frequency of (10M). MaxCost: 1 << 30, // maximum cost of cache (1GB). BufferItems: 64, // number of keys per Get buffer. }) if err != nil { panic(err) } scheduler := gocron.NewScheduler(time.UTC) scheduler.StartAsync() client := &GARClient{ client: nil, garDomain: clientConfig.GarDomain(), cache: cache, scheduler: scheduler, } if err := client.scheduleTokenRenewal(); err != nil { return nil, err } return client, nil } // CreateRepository is empty since repositories are not created for artifact registry func (e *GARClient) CreateRepository(ctx context.Context, name string) error { return nil } func (e *GARClient) RepositoryExists() bool { panic("implement me") } func (e *GARClient) CopyImage(ctx context.Context, srcRef ctypes.ImageReference, srcCreds string, destRef ctypes.ImageReference, destCreds string) error { src := srcRef.DockerReference().String() dest := destRef.DockerReference().String() creds := []string{"--src-authfile", srcCreds} // use client credentials for any source GAR repositories if strings.HasSuffix(reference.Domain(srcRef.DockerReference()), "-docker.pkg.dev") { creds = []string{"--src-creds", e.Credentials()} } app := "skopeo" args := []string{ "--override-os", "linux", "copy", "--multi-arch", "all", "--retry-times", "3", "docker://" + src, "docker://" + dest, } if len(creds[1]) > 0 { args = append(args, creds...) } else { args = append(args, "--src-no-creds") } if len(destCreds) > 0 { args = append(args, "--dest-creds", destCreds) } else { args = append(args, "--dest-no-creds") } log.Ctx(ctx). Trace(). Str("app", app). Strs("args", args). Msg("execute command to copy image") output, cmdErr := exec.CommandContext(ctx, app, args...).CombinedOutput() // check if the command timed out during execution for proper logging if err := ctx.Err(); err != nil { return err } // enrich error with output from the command which may contain the actual reason if cmdErr != nil { return fmt.Errorf("command error, stderr: %s, stdout: %s", cmdErr.Error(), string(output)) } return nil } func (e *GARClient) PullImage() error { panic("implement me") } func (e *GARClient) PutImage() error { panic("implement me") } func (e *GARClient) ImageExists(ctx context.Context, imageRef ctypes.ImageReference) bool { ref := imageRef.DockerReference().String() if _, found := e.cache.Get(ref); found { log.Ctx(ctx).Trace().Str("ref", ref).Msg("found in cache") return true } app := "skopeo" args := []string{ "inspect", "--retry-times", "3", "docker://" + ref, "--creds", e.Credentials(), } log.Ctx(ctx).Trace().Str("app", app).Strs("args", args).Msg("executing command to inspect image") if err := exec.CommandContext(ctx, app, args...).Run(); err != nil { log.Trace().Str("ref", ref).Msg("not found in target repository") return false } log.Ctx(ctx).Trace().Str("ref", ref).Msg("found in target repository") e.cache.SetWithTTL(ref, "", 1, 24*time.Hour+time.Duration(rand.Intn(180))*time.Minute) return true } func (e *GARClient) Endpoint() string { return e.garDomain } // IsOrigin returns true if the references origin is from this registry func (e *GARClient) IsOrigin(imageRef ctypes.ImageReference) bool { return strings.HasPrefix(imageRef.DockerReference().String(), e.Endpoint()) } // requestAuthToken requests and returns an authentication token from GAR with its expiration date func (e *GARClient) requestAuthToken() ([]byte, time.Time, error) { ctx := context.Background() creds, err := transport.Creds(ctx, option.WithScopes(artifactregistry.DefaultAuthScopes()...)) if err != nil { log.Err(err).Msg("generating gcp creds") return []byte(""), time.Time{}, err } token, err := creds.TokenSource.Token() if err != nil { log.Err(err).Msg("generating token") return []byte(""), time.Time{}, err } return []byte(fmt.Sprintf("oauth2accesstoken:%v", token.AccessToken)), token.Expiry, nil } // scheduleTokenRenewal sets a scheduler to execute token renewal before the token expires func (e *GARClient) scheduleTokenRenewal() error { token, expiryAt, err := e.requestAuthToken() if err != nil { return err } renewalAt := expiryAt.Add(-2 * time.Minute) e.authToken = token log.Debug().Time("expiryAt", expiryAt).Time("renewalAt", renewalAt).Msg("auth token set, schedule next token renewal") j, _ := e.scheduler.Every(1).StartAt(renewalAt).Do(e.scheduleTokenRenewal) j.LimitRunsTo(1) return nil } func (e *GARClient) Credentials() string { return string(e.authToken) } func (e *GARClient) DockerConfig() ([]byte, error) { dockerConfig := DockerConfig{ AuthConfigs: map[string]AuthConfig{ e.garDomain: { Auth: base64.StdEncoding.EncodeToString(e.authToken), }, }, } dockerConfigJson, err := json.Marshal(dockerConfig) if err != nil { return []byte{}, err } return dockerConfigJson, nil } func NewMockGARClient(garClient GARAPI, garDomain string) (*GARClient, error) { client := &GARClient{ client: garClient, garDomain: garDomain, cache: nil, scheduler: nil, authToken: []byte("oauth2accesstoken:mock-gar-client-fake-auth-token"), } return client, nil } ================================================ FILE: pkg/registry/gar_test.go ================================================ package registry import ( "testing" "github.com/containers/image/v5/transports/alltransports" "github.com/stretchr/testify/assert" ) func TestGARIsOrigin(t *testing.T) { type testCase struct { input string expected bool } testcases := []testCase{ { input: "k8s.gcr.io/ingress-nginx/controller@sha256:9bba603b99bf25f6d117cf1235b6598c16033ad027b143c90fa5b3cc583c5713", expected: false, }, { input: "us-central1-docker.pkg.dev/gcp-project-123/main/k8s.gcr.io/ingress-nginx/controller@sha256:9bba603b99bf25f6d117cf1235b6598c16033ad027b143c90fa5b3cc583c5713", expected: true, }, } fakeRegistry, _ := NewMockGARClient(nil, "us-central1-docker.pkg.dev/gcp-project-123/main") for _, testcase := range testcases { imageRef, err := alltransports.ParseImageName("docker://" + testcase.input) assert.NoError(t, err) result := fakeRegistry.IsOrigin(imageRef) assert.Equal(t, testcase.expected, result) } } ================================================ FILE: pkg/registry/inmemory.go ================================================ package registry ================================================ FILE: pkg/secrets/dummy.go ================================================ package secrets import ( "context" "github.com/estahn/k8s-image-swapper/pkg/registry" v1 "k8s.io/api/core/v1" ) // DummyImagePullSecretsProvider does nothing type DummyImagePullSecretsProvider struct { } // NewDummyImagePullSecretsProvider initialises a dummy image pull secrets provider func NewDummyImagePullSecretsProvider() ImagePullSecretsProvider { return &DummyImagePullSecretsProvider{} } func (p *DummyImagePullSecretsProvider) SetAuthenticatedRegistries(registries []registry.Client) { } // GetImagePullSecrets returns an empty ImagePullSecretsResult func (p *DummyImagePullSecretsProvider) GetImagePullSecrets(ctx context.Context, pod *v1.Pod) (*ImagePullSecretsResult, error) { return NewImagePullSecretsResult(), nil } ================================================ FILE: pkg/secrets/dummy_test.go ================================================ package secrets import ( "context" "reflect" "testing" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestDummyImagePullSecretsProvider_GetImagePullSecrets(t *testing.T) { type args struct { pod *corev1.Pod } tests := []struct { name string args args want *ImagePullSecretsResult wantErr bool }{ { name: "default", args: args{ pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Namespace: "test-ns", Name: "my-pod", }, Spec: corev1.PodSpec{ ServiceAccountName: "my-service-account", ImagePullSecrets: []corev1.LocalObjectReference{ {Name: "my-pod-secret"}, }, }, }, }, want: NewImagePullSecretsResult(), wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := &DummyImagePullSecretsProvider{} got, err := p.GetImagePullSecrets(context.Background(), tt.args.pod) if (err != nil) != tt.wantErr { t.Errorf("GetImagePullSecrets() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("GetImagePullSecrets() got = %v, want %v", got, tt.want) } }) } } func TestNewDummyImagePullSecretsProvider(t *testing.T) { tests := []struct { name string want ImagePullSecretsProvider }{ { name: "default", want: &DummyImagePullSecretsProvider{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := NewDummyImagePullSecretsProvider(); !reflect.DeepEqual(got, tt.want) { t.Errorf("NewDummyImagePullSecretsProvider() = %v, want %v", got, tt.want) } }) } } ================================================ FILE: pkg/secrets/kubernetes.go ================================================ package secrets import ( "context" "fmt" "os" "github.com/estahn/k8s-image-swapper/pkg/registry" jsonpatch "github.com/evanphx/json-patch" "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" ) // KubernetesImagePullSecretsProvider retrieves the secrets holding docker auth information from Kubernetes and merges // them if necessary. Supports Pod secrets as well as ServiceAccount secrets. type KubernetesImagePullSecretsProvider struct { kubernetesClient kubernetes.Interface authenticatedRegistries []registry.Client } // ImagePullSecretsResult contains the result of GetImagePullSecrets type ImagePullSecretsResult struct { Secrets map[string][]byte Aggregate []byte } // NewImagePullSecretsResult initialises ImagePullSecretsResult func NewImagePullSecretsResult() *ImagePullSecretsResult { return &ImagePullSecretsResult{ Secrets: map[string][]byte{}, Aggregate: []byte("{}"), } } // Initialiaze an ImagePullSecretsResult and registers image pull secrets from the given registries func NewImagePullSecretsResultWithDefaults(defaultImagePullSecrets []registry.Client) *ImagePullSecretsResult { imagePullSecretsResult := NewImagePullSecretsResult() for index, reg := range defaultImagePullSecrets { dockerConfig, err := registry.GenerateDockerConfig(reg) if err != nil { log.Err(err) } else { imagePullSecretsResult.Add(fmt.Sprintf("source-ecr-%d", index), dockerConfig) } } return imagePullSecretsResult } // Add a secrets to internal list and rebuilds the aggregate func (r *ImagePullSecretsResult) Add(name string, data []byte) { r.Secrets[name] = data r.Aggregate, _ = jsonpatch.MergePatch(r.Aggregate, data) } // AuthFile provides the aggregate as a file to be used by a docker client func (r *ImagePullSecretsResult) AuthFile() (*os.File, error) { tmpfile, err := os.CreateTemp("", "auth") if err != nil { return nil, err } if _, err := tmpfile.Write(r.Aggregate); err != nil { return nil, err } if err := tmpfile.Close(); err != nil { return nil, err } return tmpfile, nil } func NewKubernetesImagePullSecretsProvider(clientset kubernetes.Interface) ImagePullSecretsProvider { return &KubernetesImagePullSecretsProvider{ kubernetesClient: clientset, authenticatedRegistries: []registry.Client{}, } } func (p *KubernetesImagePullSecretsProvider) SetAuthenticatedRegistries(registries []registry.Client) { p.authenticatedRegistries = registries } // GetImagePullSecrets returns all secrets with their respective content func (p *KubernetesImagePullSecretsProvider) GetImagePullSecrets(ctx context.Context, pod *v1.Pod) (*ImagePullSecretsResult, error) { var secrets = make(map[string][]byte) imagePullSecrets := pod.Spec.ImagePullSecrets // retrieve secret names from pod ServiceAccount (spec.imagePullSecrets) serviceAccount, err := p.kubernetesClient.CoreV1(). ServiceAccounts(pod.Namespace). Get(ctx, pod.Spec.ServiceAccountName, metav1.GetOptions{}) if err != nil { log.Ctx(ctx).Warn().Msg("error fetching referenced service account, continue without service account imagePullSecrets") } if serviceAccount != nil { imagePullSecrets = append(imagePullSecrets, serviceAccount.ImagePullSecrets...) } result := NewImagePullSecretsResultWithDefaults(p.authenticatedRegistries) for _, imagePullSecret := range imagePullSecrets { // fetch a secret only once if _, exists := secrets[imagePullSecret.Name]; exists { continue } secret, err := p.kubernetesClient.CoreV1().Secrets(pod.Namespace).Get(ctx, imagePullSecret.Name, metav1.GetOptions{}) if err != nil { log.Ctx(ctx).Err(err).Msg("error fetching secret, continue without imagePullSecrets") } if secret == nil || secret.Type != v1.SecretTypeDockerConfigJson { continue } result.Add(imagePullSecret.Name, secret.Data[v1.DockerConfigJsonKey]) } return result, nil } ================================================ FILE: pkg/secrets/kubernetes_test.go ================================================ package secrets import ( "context" "encoding/base64" "fmt" "testing" "github.com/estahn/k8s-image-swapper/pkg/config" "github.com/estahn/k8s-image-swapper/pkg/registry" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" ) //type ExampleTestSuite struct { // suite.Suite //} // //func (suite *ExampleTestSuite) SetupTest() { //} //func (suite *ExampleTestSuite) TestExample() { // assert.Equal(suite.T(), 5, 1) //} // //func TestExampleTestSuite(t *testing.T) { // suite.Run(t, new(ExampleTestSuite)) //} // Test: //+------------------+-----+----------------+ //| | Pod | ServiceAccount | //+------------------+-----+----------------+ //| ImagePullSecrets | Y | Y | //+------------------+-----+----------------+ //| ImagePullSecrets | Y | N | //+------------------+-----+----------------+ //| ImagePullSecrets | N | Y | //+------------------+-----+----------------+ //| ImagePullSecrets | N | N | //+------------------+-----+----------------+ // // Multple image pull secrets on pod + service account // Pod secret should override service account secret func TestKubernetesCredentialProvider_GetImagePullSecrets(t *testing.T) { clientSet := fake.NewSimpleClientset() svcAccount := &corev1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ Name: "my-service-account", }, ImagePullSecrets: []corev1.LocalObjectReference{ {Name: "my-sa-secret"}, }, } svcAccountSecretDockerConfigJson := []byte(`{"auths":{"my-sa-secret.registry.example.com":{"username":"my-sa-secret","password":"xxxxxxxxxxx","email":"jdoe@example.com","auth":"c3R...zE2"}}}`) svcAccountSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "my-sa-secret", }, Type: corev1.SecretTypeDockerConfigJson, Data: map[string][]byte{ corev1.DockerConfigJsonKey: svcAccountSecretDockerConfigJson, }, } podSecretDockerConfigJson := []byte(`{"auths":{"my-pod-secret.registry.example.com":{"username":"my-sa-secret","password":"xxxxxxxxxxx","email":"jdoe@example.com","auth":"c3R...zE2"}}}`) podSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "my-pod-secret", }, Type: corev1.SecretTypeDockerConfigJson, Data: map[string][]byte{ corev1.DockerConfigJsonKey: podSecretDockerConfigJson, }, } pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Namespace: "test-ns", Name: "my-pod", }, Spec: corev1.PodSpec{ ServiceAccountName: "my-service-account", ImagePullSecrets: []corev1.LocalObjectReference{ {Name: "my-pod-secret"}, }, }, } _, _ = clientSet.CoreV1().ServiceAccounts("test-ns").Create(context.TODO(), svcAccount, metav1.CreateOptions{}) _, _ = clientSet.CoreV1().Secrets("test-ns").Create(context.TODO(), svcAccountSecret, metav1.CreateOptions{}) _, _ = clientSet.CoreV1().Secrets("test-ns").Create(context.TODO(), podSecret, metav1.CreateOptions{}) provider := NewKubernetesImagePullSecretsProvider(clientSet) result, err := provider.GetImagePullSecrets(context.Background(), pod) assert.NoError(t, err) assert.NotNil(t, result) assert.Len(t, result.Secrets, 2) assert.Equal(t, svcAccountSecretDockerConfigJson, result.Secrets["my-sa-secret"]) assert.Equal(t, podSecretDockerConfigJson, result.Secrets["my-pod-secret"]) } // TestImagePullSecretsResult_WithDefault tests if authenticated private registries work func TestImagePullSecretsResult_WithDefault(t *testing.T) { fakeToken := []byte("token") fakeBase64Token := base64.StdEncoding.EncodeToString(fakeToken) fakeAccountIds := []string{"12345678912", "9876543210"} fakeRegions := []string{"us-east-1", "us-west-1"} fakeEcrDomains := []string{ fmt.Sprintf("%s.dkr.ecr.%s.amazonaws.com", fakeAccountIds[0], fakeRegions[0]), fmt.Sprintf("%s.dkr.ecr.%s.amazonaws.com", fakeAccountIds[1], fakeRegions[1]), } expected := &ImagePullSecretsResult{ Secrets: map[string][]byte{ "source-ecr-0": []byte("{\"auths\":{\"" + fakeEcrDomains[0] + "\":{\"auth\":\"" + fakeBase64Token + "\"}}}"), "source-ecr-1": []byte("{\"auths\":{\"" + fakeEcrDomains[1] + "\":{\"auth\":\"" + fakeBase64Token + "\"}}}"), }, Aggregate: []byte("{\"auths\":{\"" + fakeEcrDomains[0] + "\":{\"auth\":\"" + fakeBase64Token + "\"},\"" + fakeEcrDomains[1] + "\":{\"auth\":\"" + fakeBase64Token + "\"}}}"), } fakeRegistry1 := registry.NewDummyECRClient(fakeRegions[0], fakeAccountIds[0], "", config.ECROptions{}, fakeToken) fakeRegistry2 := registry.NewDummyECRClient(fakeRegions[1], fakeAccountIds[1], "", config.ECROptions{}, fakeToken) fakeRegistries := []registry.Client{fakeRegistry1, fakeRegistry2} r := NewImagePullSecretsResultWithDefaults(fakeRegistries) assert.Equal(t, r, expected) } // TestImagePullSecretsResult_Add tests if aggregation works func TestImagePullSecretsResult_Add(t *testing.T) { expected := &ImagePullSecretsResult{ Secrets: map[string][]byte{ "foo": []byte("{\"foo\":\"123\"}"), "bar": []byte("{\"bar\":\"456\"}"), }, Aggregate: []byte("{\"bar\":\"456\",\"foo\":\"123\"}"), } r := NewImagePullSecretsResult() r.Add("foo", []byte("{\"foo\":\"123\"}")) r.Add("bar", []byte("{\"bar\":\"456\"}")) assert.Equal(t, r, expected) } ================================================ FILE: pkg/secrets/provider.go ================================================ package secrets import ( "context" "github.com/estahn/k8s-image-swapper/pkg/registry" v1 "k8s.io/api/core/v1" ) type ImagePullSecretsProvider interface { GetImagePullSecrets(ctx context.Context, pod *v1.Pod) (*ImagePullSecretsResult, error) SetAuthenticatedRegistries(privateRegistries []registry.Client) } ================================================ FILE: pkg/types/types.go ================================================ package types import "fmt" type Registry int const ( RegistryUnknown = iota RegistryAWS RegistryGCP ) func (p Registry) String() string { return [...]string{"unknown", "aws", "gcp"}[p] } func ParseRegistry(p string) (Registry, error) { switch p { case Registry(RegistryAWS).String(): return RegistryAWS, nil case Registry(RegistryGCP).String(): return RegistryGCP, nil } return RegistryUnknown, fmt.Errorf("unknown target registry string: '%s', defaulting to unknown", p) } type ImageSwapPolicy int const ( ImageSwapPolicyAlways = iota ImageSwapPolicyExists ) func (p ImageSwapPolicy) String() string { return [...]string{"always", "exists"}[p] } func ParseImageSwapPolicy(p string) (ImageSwapPolicy, error) { switch p { case ImageSwapPolicy(ImageSwapPolicyAlways).String(): return ImageSwapPolicyAlways, nil case ImageSwapPolicy(ImageSwapPolicyExists).String(): return ImageSwapPolicyExists, nil } return ImageSwapPolicyExists, fmt.Errorf("unknown image swap policy string: '%s', defaulting to exists", p) } type ImageCopyPolicy int const ( ImageCopyPolicyDelayed = iota ImageCopyPolicyImmediate ImageCopyPolicyForce ImageCopyPolicyNone ) func (p ImageCopyPolicy) String() string { return [...]string{"delayed", "immediate", "force", "none"}[p] } func ParseImageCopyPolicy(p string) (ImageCopyPolicy, error) { switch p { case ImageCopyPolicy(ImageCopyPolicyDelayed).String(): return ImageCopyPolicyDelayed, nil case ImageCopyPolicy(ImageCopyPolicyImmediate).String(): return ImageCopyPolicyImmediate, nil case ImageCopyPolicy(ImageCopyPolicyForce).String(): return ImageCopyPolicyForce, nil case ImageCopyPolicy(ImageCopyPolicyNone).String(): return ImageCopyPolicyNone, nil } return ImageCopyPolicyDelayed, fmt.Errorf("unknown image copy policy string: '%s', defaulting to delayed", p) } ================================================ FILE: pkg/types/types_test.go ================================================ package types import "testing" func TestParseImageSwapPolicy(t *testing.T) { type args struct { p string } tests := []struct { name string args args want ImageSwapPolicy wantErr bool }{ { name: "always", args: args{p: "always"}, want: ImageSwapPolicyAlways, }, { name: "exists", args: args{p: "exists"}, want: ImageSwapPolicyExists, }, { name: "random-non-existent", args: args{p: "random-non-existent"}, want: ImageSwapPolicyExists, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := ParseImageSwapPolicy(tt.args.p) if (err != nil) != tt.wantErr { t.Errorf("ParseImageSwapPolicy() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("ParseImageSwapPolicy() got = %v, want %v", got, tt.want) } }) } } func TestParseImageCopyPolicy(t *testing.T) { type args struct { p string } tests := []struct { name string args args want ImageCopyPolicy wantErr bool }{ { name: "delayed", args: args{p: "delayed"}, want: ImageCopyPolicyDelayed, }, { name: "immediate", args: args{p: "immediate"}, want: ImageCopyPolicyImmediate, }, { name: "force", args: args{p: "force"}, want: ImageCopyPolicyForce, }, { name: "none", args: args{p: "none"}, want: ImageCopyPolicyNone, }, { name: "random-non-existent", args: args{p: "random-non-existent"}, want: ImageCopyPolicyDelayed, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := ParseImageCopyPolicy(tt.args.p) if (err != nil) != tt.wantErr { t.Errorf("ParseImageCopyPolicy() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("ParseImageCopyPolicy() got = %v, want %v", got, tt.want) } }) } } ================================================ FILE: pkg/webhook/image_copier.go ================================================ package webhook import ( "context" "errors" "os" "github.com/containers/image/v5/docker/reference" ctypes "github.com/containers/image/v5/types" "github.com/rs/zerolog/log" corev1 "k8s.io/api/core/v1" ) // struct representing a job of copying an image with its subcontext type ImageCopier struct { sourcePod *corev1.Pod sourceImageRef ctypes.ImageReference targetImageRef ctypes.ImageReference imagePullPolicy corev1.PullPolicy imageSwapper *ImageSwapper context context.Context cancelContext context.CancelFunc } type Task struct { function func() error description string } var ErrImageAlreadyPresent = errors.New("image already present in target registry") // replace the default context with a new one with a timeout func (ic *ImageCopier) withDeadline() *ImageCopier { imageCopierContext, imageCopierContextCancel := context.WithTimeout(ic.context, ic.imageSwapper.imageCopyDeadline) ic.context = imageCopierContext ic.cancelContext = imageCopierContextCancel return ic } // start the image copy job func (ic *ImageCopier) start() { if _, hasDeadline := ic.context.Deadline(); hasDeadline { defer ic.cancelContext() } // list of actions to execute in order to copy an image tasks := []*Task{ { function: ic.taskCheckImage, description: "checking image presence in target registry", }, { function: ic.taskCreateRepository, description: "creating a new repository in target registry", }, { function: ic.taskCopyImage, description: "copying image data to target repository", }, } for _, task := range tasks { err := ic.run(task.function) if err != nil { if errors.Is(err, context.DeadlineExceeded) { log.Ctx(ic.context).Err(err).Msg("timeout during image copy") } else if errors.Is(err, ErrImageAlreadyPresent) { log.Ctx(ic.context).Trace().Msgf("image copy aborted: %s", err.Error()) } else { log.Ctx(ic.context).Err(err).Msgf("image copy error while %s", task.description) } break } } } // run a task function and check for timeout func (ic *ImageCopier) run(taskFunc func() error) error { if err := ic.context.Err(); err != nil { return err } return taskFunc() } func (ic *ImageCopier) taskCheckImage() error { registryClient := ic.imageSwapper.registryClient imageAlreadyExists := registryClient.ImageExists(ic.context, ic.targetImageRef) && ic.imagePullPolicy != corev1.PullAlways if err := ic.context.Err(); err != nil { return err } else if imageAlreadyExists { return ErrImageAlreadyPresent } return nil } func (ic *ImageCopier) taskCreateRepository() error { createRepoName := reference.TrimNamed(ic.sourceImageRef.DockerReference()).String() return ic.imageSwapper.registryClient.CreateRepository(ic.context, createRepoName) } func (ic *ImageCopier) taskCopyImage() error { ctx := ic.context // Retrieve secrets and auth credentials imagePullSecrets, err := ic.imageSwapper.imagePullSecretProvider.GetImagePullSecrets(ctx, ic.sourcePod) // not possible at the moment if err != nil { return err } authFile, err := imagePullSecrets.AuthFile() if err != nil { log.Ctx(ctx).Err(err).Msg("failed generating authFile") } defer func() { if err := os.RemoveAll(authFile.Name()); err != nil { log.Ctx(ctx).Err(err).Str("file", authFile.Name()).Msg("failed removing auth file") } }() if err := ctx.Err(); err != nil { return err } // Copy image // TODO: refactor to use structure instead of passing file name / string // // or transform registryClient creds into auth compatible form, e.g. // {"auths":{"aws_account_id.dkr.ecr.region.amazonaws.com":{"username":"AWS","password":"..." }}} return ic.imageSwapper.registryClient.CopyImage(ctx, ic.sourceImageRef, authFile.Name(), ic.targetImageRef, ic.imageSwapper.registryClient.Credentials()) } ================================================ FILE: pkg/webhook/image_copier_test.go ================================================ package webhook import ( "context" "testing" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ecr" "github.com/containers/image/v5/transports/alltransports" "github.com/estahn/k8s-image-swapper/pkg/registry" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" corev1 "k8s.io/api/core/v1" ) func TestImageCopier_withDeadline(t *testing.T) { mutator := NewImageSwapperWithOpts( nil, ImageCopyDeadline(8*time.Second), ) imageSwapper, _ := mutator.(*ImageSwapper) imageCopier := &ImageCopier{ imageSwapper: imageSwapper, context: context.Background(), } imageCopier = imageCopier.withDeadline() deadline, hasDeadline := imageCopier.context.Deadline() // test that a deadline has been set assert.Equal(t, true, hasDeadline) // test that the deadline is future assert.GreaterOrEqual(t, deadline, time.Now()) // test that the context can be canceled assert.NotEqual(t, nil, imageCopier.context.Done()) imageCopier.cancelContext() _, ok := <-imageCopier.context.Done() // test that the Done channel is closed, meaning the context is canceled assert.Equal(t, false, ok) } func TestImageCopier_tasksTimeout(t *testing.T) { ecrClient := new(mockECRClient) ecrClient.On( "CreateRepositoryWithContext", mock.AnythingOfType("*context.timerCtx"), &ecr.CreateRepositoryInput{ ImageScanningConfiguration: &ecr.ImageScanningConfiguration{ ScanOnPush: aws.Bool(true), }, ImageTagMutability: aws.String("MUTABLE"), RepositoryName: aws.String("docker.io/library/init-container"), RegistryId: aws.String("123456789"), Tags: []*ecr.Tag{ { Key: aws.String("CreatedBy"), Value: aws.String("k8s-image-swapper"), }, }, }).Return(mock.Anything) registryClient, _ := registry.NewMockECRClient(ecrClient, "ap-southeast-2", "123456789.dkr.ecr.ap-southeast-2.amazonaws.com", "123456789", "arn:aws:iam::123456789:role/fakerole") // image swapper with an instant timeout for testing purpose mutator := NewImageSwapperWithOpts( registryClient, ImageCopyDeadline(0*time.Second), ) imageSwapper, _ := mutator.(*ImageSwapper) srcRef, _ := alltransports.ParseImageName("docker://library/init-container:latest") targetRef, _ := alltransports.ParseImageName("docker://123456789.dkr.ecr.ap-southeast-2.amazonaws.com/docker.io/library/init-container:latest") imageCopier := &ImageCopier{ imageSwapper: imageSwapper, context: context.Background(), sourceImageRef: srcRef, targetImageRef: targetRef, imagePullPolicy: corev1.PullAlways, sourcePod: &corev1.Pod{ Spec: corev1.PodSpec{ ServiceAccountName: "service-account", ImagePullSecrets: []corev1.LocalObjectReference{}, }, }, } imageCopier = imageCopier.withDeadline() // test that copy steps generate timeout errors var timeoutError error timeoutError = imageCopier.run(imageCopier.taskCheckImage) assert.Equal(t, context.DeadlineExceeded, timeoutError) timeoutError = imageCopier.run(imageCopier.taskCreateRepository) assert.Equal(t, context.DeadlineExceeded, timeoutError) timeoutError = imageCopier.run(imageCopier.taskCopyImage) assert.Equal(t, context.DeadlineExceeded, timeoutError) timeoutError = imageCopier.taskCheckImage() assert.Equal(t, context.DeadlineExceeded, timeoutError) timeoutError = imageCopier.taskCreateRepository() assert.Equal(t, context.DeadlineExceeded, timeoutError) timeoutError = imageCopier.taskCopyImage() assert.Equal(t, context.DeadlineExceeded, timeoutError) } ================================================ FILE: pkg/webhook/image_swapper.go ================================================ package webhook import ( "context" "encoding/json" "fmt" "time" "github.com/alitto/pond" "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/transports/alltransports" ctypes "github.com/containers/image/v5/types" "github.com/estahn/k8s-image-swapper/pkg/config" "github.com/estahn/k8s-image-swapper/pkg/registry" "github.com/estahn/k8s-image-swapper/pkg/secrets" types "github.com/estahn/k8s-image-swapper/pkg/types" jmespath "github.com/jmespath/go-jmespath" "github.com/rs/zerolog/log" kwhmodel "github.com/slok/kubewebhook/v2/pkg/model" "github.com/slok/kubewebhook/v2/pkg/webhook" kwhmutating "github.com/slok/kubewebhook/v2/pkg/webhook/mutating" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // Option represents an option that can be passed when instantiating the image swapper to customize it type Option func(*ImageSwapper) // ImagePullSecretsProvider allows to pass a provider reading out Kubernetes secrets func ImagePullSecretsProvider(provider secrets.ImagePullSecretsProvider) Option { return func(swapper *ImageSwapper) { swapper.imagePullSecretProvider = provider } } // Filters allows to pass JMESPathFilter to select the images to be swapped func Filters(filters []config.JMESPathFilter) Option { return func(swapper *ImageSwapper) { swapper.filters = filters } } // ImageSwapPolicy allows to pass the ImageSwapPolicy option func ImageSwapPolicy(policy types.ImageSwapPolicy) Option { return func(swapper *ImageSwapper) { swapper.imageSwapPolicy = policy } } // ImageCopyPolicy allows to pass the ImageCopyPolicy option func ImageCopyPolicy(policy types.ImageCopyPolicy) Option { return func(swapper *ImageSwapper) { swapper.imageCopyPolicy = policy } } // ImageCopyDeadline allows to pass the ImageCopyPolicy option func ImageCopyDeadline(deadline time.Duration) Option { return func(swapper *ImageSwapper) { swapper.imageCopyDeadline = deadline } } // Copier allows to pass the copier option func Copier(pool *pond.WorkerPool) Option { return func(swapper *ImageSwapper) { swapper.copier = pool } } // ImageSwapper is a mutator that will download images and change the image name. type ImageSwapper struct { registryClient registry.Client imagePullSecretProvider secrets.ImagePullSecretsProvider // filters defines a list of expressions to remove objects that should not be processed, // by default all objects will be processed filters []config.JMESPathFilter // copier manages the jobs copying the images to the target registry copier *pond.WorkerPool imageCopyDeadline time.Duration imageSwapPolicy types.ImageSwapPolicy imageCopyPolicy types.ImageCopyPolicy } // NewImageSwapper returns a new ImageSwapper initialized. func NewImageSwapper(registryClient registry.Client, imagePullSecretProvider secrets.ImagePullSecretsProvider, filters []config.JMESPathFilter, imageSwapPolicy types.ImageSwapPolicy, imageCopyPolicy types.ImageCopyPolicy, imageCopyDeadline time.Duration) kwhmutating.Mutator { return &ImageSwapper{ registryClient: registryClient, imagePullSecretProvider: imagePullSecretProvider, filters: filters, copier: pond.New(100, 1000), imageSwapPolicy: imageSwapPolicy, imageCopyPolicy: imageCopyPolicy, imageCopyDeadline: imageCopyDeadline, } } // NewImageSwapperWithOpts returns a configured ImageSwapper instance func NewImageSwapperWithOpts(registryClient registry.Client, opts ...Option) kwhmutating.Mutator { swapper := &ImageSwapper{ registryClient: registryClient, imagePullSecretProvider: secrets.NewDummyImagePullSecretsProvider(), filters: []config.JMESPathFilter{}, imageSwapPolicy: types.ImageSwapPolicyExists, imageCopyPolicy: types.ImageCopyPolicyDelayed, } for _, opt := range opts { opt(swapper) } // Initialise worker pool if not configured if swapper.copier == nil { swapper.copier = pond.New(100, 1000) } return swapper } func NewImageSwapperWebhookWithOpts(registryClient registry.Client, opts ...Option) (webhook.Webhook, error) { imageSwapper := NewImageSwapperWithOpts(registryClient, opts...) mt := kwhmutating.MutatorFunc(imageSwapper.Mutate) mcfg := kwhmutating.WebhookConfig{ ID: "k8s-image-swapper", Obj: &corev1.Pod{}, Mutator: mt, } return kwhmutating.NewWebhook(mcfg) } func NewImageSwapperWebhook(registryClient registry.Client, imagePullSecretProvider secrets.ImagePullSecretsProvider, filters []config.JMESPathFilter, imageSwapPolicy types.ImageSwapPolicy, imageCopyPolicy types.ImageCopyPolicy, imageCopyDeadline time.Duration) (webhook.Webhook, error) { imageSwapper := NewImageSwapper(registryClient, imagePullSecretProvider, filters, imageSwapPolicy, imageCopyPolicy, imageCopyDeadline) mt := kwhmutating.MutatorFunc(imageSwapper.Mutate) mcfg := kwhmutating.WebhookConfig{ ID: "k8s-image-swapper", Obj: &corev1.Pod{}, Mutator: mt, } return kwhmutating.NewWebhook(mcfg) } // imageNamesWithDigestOrTag strips the tag from ambiguous image references that have a digest as well (e.g. `image:tag@sha256:123...`). // Such image references are supported by docker but, due to their ambiguity, // explicitly not by containers/image. func imageNamesWithDigestOrTag(imageName string) (string, error) { ref, err := reference.ParseNormalizedNamed(imageName) if err != nil { return "", err } _, isTagged := ref.(reference.NamedTagged) canonical, isDigested := ref.(reference.Canonical) if isTagged && isDigested { canonical, err = reference.WithDigest(reference.TrimNamed(ref), canonical.Digest()) if err != nil { return "", err } imageName = canonical.String() } return imageName, nil } // Mutate replaces the image ref. Satisfies mutating.Mutator interface. func (p *ImageSwapper) Mutate(ctx context.Context, ar *kwhmodel.AdmissionReview, obj metav1.Object) (*kwhmutating.MutatorResult, error) { pod, ok := obj.(*corev1.Pod) if !ok { return &kwhmutating.MutatorResult{}, nil } logger := log.With(). Str("uid", string(ar.ID)). Str("kind", ar.RequestGVK.String()). Str("namespace", ar.Namespace). Str("name", pod.Name). Logger() lctx := logger.WithContext(context.Background()) containerSets := []*[]corev1.Container{&pod.Spec.Containers, &pod.Spec.InitContainers} for _, containerSet := range containerSets { containers := *containerSet for i, container := range containers { normalizedName, err := imageNamesWithDigestOrTag(container.Image) if err != nil { log.Ctx(lctx).Warn().Msgf("unable to normalize source name %s: %v", container.Image, err) continue } srcRef, err := alltransports.ParseImageName("docker://" + normalizedName) if err != nil { log.Ctx(lctx).Warn().Msgf("invalid source name %s: %v", normalizedName, err) continue } // skip if the source originates from the target registry if p.registryClient.IsOrigin(srcRef) { log.Ctx(lctx).Debug().Str("registry", srcRef.DockerReference().String()).Msg("skip due to source and target being the same registry") continue } filterCtx := NewFilterContext(*ar, pod, container) if filterMatch(filterCtx, p.filters) { log.Ctx(lctx).Debug().Msg("skip due to filter condition") continue } targetRef := p.targetRef(srcRef) targetImage := targetRef.DockerReference().String() imageCopierLogger := logger.With(). Str("source-image", srcRef.DockerReference().String()). Str("target-image", targetImage). Logger() imageCopierContext := imageCopierLogger.WithContext(lctx) // create an object responsible for the image copy imageCopier := ImageCopier{ sourcePod: pod, sourceImageRef: srcRef, targetImageRef: targetRef, imagePullPolicy: container.ImagePullPolicy, imageSwapper: p, context: imageCopierContext, } // imageCopyPolicy switch p.imageCopyPolicy { case types.ImageCopyPolicyDelayed: p.copier.Submit(imageCopier.start) case types.ImageCopyPolicyImmediate: p.copier.SubmitAndWait(imageCopier.withDeadline().start) case types.ImageCopyPolicyForce: imageCopier.withDeadline().start() case types.ImageCopyPolicyNone: // do not copy image default: panic("unknown imageCopyPolicy") } // imageSwapPolicy switch p.imageSwapPolicy { case types.ImageSwapPolicyAlways: log.Ctx(lctx).Debug().Str("image", targetImage).Msg("set new container image") containers[i].Image = targetImage case types.ImageSwapPolicyExists: if p.registryClient.ImageExists(lctx, targetRef) { log.Ctx(lctx).Debug().Str("image", targetImage).Msg("set new container image") containers[i].Image = targetImage } else { log.Ctx(lctx).Debug().Str("image", targetImage).Msg("container image not found in target registry, not swapping") } default: panic("unknown imageSwapPolicy") } } } return &kwhmutating.MutatorResult{MutatedObject: pod}, nil } // filterMatch returns true if one of the filters matches the context func filterMatch(ctx FilterContext, filters []config.JMESPathFilter) bool { // Simplify FilterContext to be easier searchable by marshaling it to JSON and back to an interface var filterContext interface{} jsonBlob, err := json.Marshal(ctx) if err != nil { log.Err(err).Msg("could not marshal filter context") return false } err = json.Unmarshal(jsonBlob, &filterContext) if err != nil { log.Err(err).Msg("could not unmarshal json blob") return false } log.Debug().Interface("object", filterContext).Msg("generated filter context") for idx, filter := range filters { results, err := jmespath.Search(filter.JMESPath, filterContext) log.Debug().Str("filter", filter.JMESPath).Interface("results", results).Msg("jmespath search results") if err != nil { log.Err(err).Str("filter", filter.JMESPath).Msgf("Filter (idx %v) could not be evaluated.", idx) return false } switch results.(type) { case bool: if results == true { return true } default: log.Warn().Str("filter", filter.JMESPath).Msg("filter does not return a bool value") } } return false } // targetName returns the reference in the target repository func (p *ImageSwapper) targetRef(srcRef ctypes.ImageReference) ctypes.ImageReference { targetImage := fmt.Sprintf("%s/%s", p.registryClient.Endpoint(), srcRef.DockerReference().String()) ref, err := alltransports.ParseImageName("docker://" + targetImage) if err != nil { log.Warn().Msgf("invalid target name %s: %v", targetImage, err) } return ref } // FilterContext is being used by JMESPath to search and match type FilterContext struct { // Obj contains the object submitted to the webhook (currently only pods) Obj metav1.Object `json:"obj,omitempty"` // Container contains the currently processed container Container corev1.Container `json:"container,omitempty"` } func NewFilterContext(request kwhmodel.AdmissionReview, obj metav1.Object, container corev1.Container) FilterContext { if obj.GetNamespace() == "" { obj.SetNamespace(request.Namespace) } return FilterContext{Obj: obj, Container: container} } ================================================ FILE: pkg/webhook/image_swapper_test.go ================================================ package webhook import ( "context" "encoding/json" "os" "testing" "github.com/alitto/pond" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/service/ecr" "github.com/aws/aws-sdk-go/service/ecr/ecriface" "github.com/estahn/k8s-image-swapper/pkg/config" "github.com/estahn/k8s-image-swapper/pkg/registry" "github.com/estahn/k8s-image-swapper/pkg/secrets" "github.com/estahn/k8s-image-swapper/pkg/types" "github.com/slok/kubewebhook/v2/pkg/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" admissionv1 "k8s.io/api/admission/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" ) //func TestImageSwapperMutator(t *testing.T) { // tests := []struct { // name string // pod *corev1.Pod // labels map[string]string // expPod *corev1.Pod // expErr bool // }{ // { // name: "Prefix docker hub images with host docker.io.", // pod: &corev1.Pod{ // Spec: corev1.PodSpec{ // Containers: []corev1.Container{ // { // Image: "nginx:latest", // }, // }, // }, // }, // expPod: &corev1.Pod{ // Spec: corev1.PodSpec{ // Containers: []corev1.Container{ // { // Image: "foobar.com/docker.io/nginx:latest", // }, // }, // }, // }, // }, // { // name: "Don't mutate if targetRegistry host is target targetRegistry.", // pod: &corev1.Pod{ // Spec: corev1.PodSpec{ // Containers: []corev1.Container{ // { // Image: "foobar.com/docker.io/nginx:latest", // }, // }, // }, // }, // expPod: &corev1.Pod{ // Spec: corev1.PodSpec{ // Containers: []corev1.Container{ // { // Image: "foobar.com/docker.io/nginx:latest", // }, // }, // }, // }, // }, // } // // for _, test := range tests { // t.Run(test.name, func(t *testing.T) { // assert := assert.New(t) // // pl := NewImageSwapper("foobar.com") // // gotPod := test.pod // _, err := pl.Mutate(context.TODO(), gotPod) // // if test.expErr { // assert.Error(err) // } else if assert.NoError(err) { // assert.Equal(test.expPod, gotPod) // } // }) // } // //} // //func TestAnnotatePodMutator2(t *testing.T) { // tests := []struct { // name string // pod *corev1.Pod // labels map[string]string // expPod *corev1.Pod // expErr bool // }{ // { // name: "Mutating a pod without labels should set the labels correctly.", // pod: &corev1.Pod{ // ObjectMeta: metav1.ObjectMeta{ // Name: "test", // }, // }, // labels: map[string]string{"bruce": "wayne", "peter": "parker"}, // expPod: &corev1.Pod{ // ObjectMeta: metav1.ObjectMeta{ // Name: "test", // Labels: map[string]string{"bruce": "wayne", "peter": "parker"}, // }, // }, // }, // { // name: "Mutating a pod with labels should aggregate and replace the labels with the existing ones.", // pod: &corev1.Pod{ // ObjectMeta: metav1.ObjectMeta{ // Name: "test", // Labels: map[string]string{"bruce": "banner", "tony": "stark"}, // }, // }, // labels: map[string]string{"bruce": "wayne", "peter": "parker"}, // expPod: &corev1.Pod{ // ObjectMeta: metav1.ObjectMeta{ // Name: "test", // Labels: map[string]string{"bruce": "wayne", "peter": "parker", "tony": "stark"}, // }, // }, // }, // } // // for _, test := range tests { // t.Run(test.name, func(t *testing.T) { // assert := assert.New(t) // // pl := mutatortesting.NewPodLabeler(test.labels) // gotPod := test.pod // _, err := pl.Mutate(context.TODO(), gotPod) // // if test.expErr { // assert.Error(err) // } else if assert.NoError(err) { // // Check the expected pod. // assert.Equal(test.expPod, gotPod) // } // }) // } // //} //func TestRegistryHost(t *testing.T) { // assert.Equal(t, "", registryDomain("nginx:latest")) // assert.Equal(t, "docker.io", registryDomain("docker.io/nginx:latest")) //} func TestFilterMatch(t *testing.T) { filterContext := FilterContext{ Obj: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Namespace: "kube-system", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx:latest", }, }, }, }, Container: corev1.Container{ Name: "nginx", Image: "nginx:latest", }, } assert.True(t, filterMatch(filterContext, []config.JMESPathFilter{{JMESPath: "obj.metadata.namespace == 'kube-system'"}})) assert.False(t, filterMatch(filterContext, []config.JMESPathFilter{{JMESPath: "obj.metadata.namespace != 'kube-system'"}})) assert.False(t, filterMatch(filterContext, []config.JMESPathFilter{{JMESPath: "obj"}})) assert.True(t, filterMatch(filterContext, []config.JMESPathFilter{{JMESPath: "container.name == 'nginx'"}})) // false syntax test assert.False(t, filterMatch(filterContext, []config.JMESPathFilter{{JMESPath: "."}})) // non-boolean value assert.False(t, filterMatch(filterContext, []config.JMESPathFilter{{JMESPath: "obj"}})) assert.False(t, filterMatch(filterContext, []config.JMESPathFilter{{JMESPath: "contains(container.image, '.dkr.ecr.') && contains(container.image, '.amazonaws.com')"}})) } type mockECRClient struct { mock.Mock ecriface.ECRAPI } func (m *mockECRClient) CreateRepositoryWithContext(ctx context.Context, createRepositoryInput *ecr.CreateRepositoryInput, opts ...request.Option) (*ecr.CreateRepositoryOutput, error) { if ctx.Err() != nil { return nil, ctx.Err() } m.Called(ctx, createRepositoryInput) return &ecr.CreateRepositoryOutput{}, nil } func TestHelperProcess(t *testing.T) { if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { return } os.Exit(0) } func readAdmissionReviewFromFile(filename string) (*admissionv1.AdmissionReview, error) { data, err := os.ReadFile("../../test/requests/" + filename) if err != nil { return nil, err } ar := &admissionv1.AdmissionReview{} if err := json.Unmarshal(data, ar); err != nil { return nil, err } return ar, nil } func TestImageSwapper_Mutate(t *testing.T) { expectedRepositories := []string{ "docker.io/library/init-container", "docker.io/library/nginx", "k8s.gcr.io/ingress-nginx/controller", "us-central1-docker.pkg.dev/gcp-project-123/main/k8s.gcr.io/ingress-nginx/controller", } ecrClient := new(mockECRClient) for _, expectedRepository := range expectedRepositories { ecrClient.On( "CreateRepositoryWithContext", mock.AnythingOfType("*context.valueCtx"), &ecr.CreateRepositoryInput{ ImageScanningConfiguration: &ecr.ImageScanningConfiguration{ ScanOnPush: aws.Bool(true), }, EncryptionConfiguration: &ecr.EncryptionConfiguration{ EncryptionType: aws.String("AES256"), }, ImageTagMutability: aws.String("MUTABLE"), RepositoryName: aws.String(expectedRepository), RegistryId: aws.String("123456789"), Tags: []*ecr.Tag{ { Key: aws.String("CreatedBy"), Value: aws.String("k8s-image-swapper"), }, { Key: aws.String("AnotherTag"), Value: aws.String("another-tag"), }, }, }).Return(mock.Anything) } registryClient, _ := registry.NewMockECRClient(ecrClient, "ap-southeast-2", "123456789.dkr.ecr.ap-southeast-2.amazonaws.com", "123456789", "arn:aws:iam::123456789:role/fakerole") admissionReview, _ := readAdmissionReviewFromFile("admissionreview-simple.json") admissionReviewModel := model.NewAdmissionReviewV1(admissionReview) copier := pond.New(1, 1) // TODO: test types.ImageSwapPolicyExists wh, err := NewImageSwapperWebhookWithOpts( registryClient, Copier(copier), ImageSwapPolicy(types.ImageSwapPolicyAlways), ) assert.NoError(t, err, "NewImageSwapperWebhookWithOpts executed without errors") resp, err := wh.Review(context.Background(), admissionReviewModel) // TODO: think about moving "expected" into a file, e.g. admissionreview-simple-response-ecr.json // container with name "skip-test-gar" should be skipped, hence there is no "replace" operation for it expected := `[ {"op":"replace","path":"/spec/initContainers/0/image","value":"123456789.dkr.ecr.ap-southeast-2.amazonaws.com/docker.io/library/init-container:latest"}, {"op":"replace","path":"/spec/containers/0/image","value":"123456789.dkr.ecr.ap-southeast-2.amazonaws.com/docker.io/library/nginx:latest"}, {"op":"replace","path":"/spec/containers/1/image","value":"123456789.dkr.ecr.ap-southeast-2.amazonaws.com/k8s.gcr.io/ingress-nginx/controller@sha256:9bba603b99bf25f6d117cf1235b6598c16033ad027b143c90fa5b3cc583c5713"}, {"op":"replace","path":"/spec/containers/3/image","value":"123456789.dkr.ecr.ap-southeast-2.amazonaws.com/us-central1-docker.pkg.dev/gcp-project-123/main/k8s.gcr.io/ingress-nginx/controller@sha256:9bba603b99bf25f6d117cf1235b6598c16033ad027b143c90fa5b3cc583c5713"} ]` assert.JSONEq(t, expected, string(resp.(*model.MutatingAdmissionResponse).JSONPatchPatch)) assert.Nil(t, resp.(*model.MutatingAdmissionResponse).Warnings) assert.NoError(t, err, "Webhook executed without errors") // Ensure the worker pool is empty before asserting ecrClient copier.StopAndWait() ecrClient.AssertExpectations(t) } // TestImageSwapper_MutateWithImagePullSecrets tests mutating with imagePullSecret support func TestImageSwapper_MutateWithImagePullSecrets(t *testing.T) { ecrClient := new(mockECRClient) ecrClient.On( "CreateRepositoryWithContext", mock.AnythingOfType("*context.valueCtx"), &ecr.CreateRepositoryInput{ ImageScanningConfiguration: &ecr.ImageScanningConfiguration{ ScanOnPush: aws.Bool(true), }, EncryptionConfiguration: &ecr.EncryptionConfiguration{ EncryptionType: aws.String("AES256"), }, ImageTagMutability: aws.String("MUTABLE"), RegistryId: aws.String("123456789"), RepositoryName: aws.String("docker.io/library/nginx"), Tags: []*ecr.Tag{ { Key: aws.String("CreatedBy"), Value: aws.String("k8s-image-swapper"), }, { Key: aws.String("AnotherTag"), Value: aws.String("another-tag"), }, }, }).Return(mock.Anything) registryClient, _ := registry.NewMockECRClient(ecrClient, "ap-southeast-2", "123456789.dkr.ecr.ap-southeast-2.amazonaws.com", "123456789", "arn:aws:iam::123456789:role/fakerole") admissionReview, _ := readAdmissionReviewFromFile("admissionreview-imagepullsecrets.json") admissionReviewModel := model.NewAdmissionReviewV1(admissionReview) clientSet := fake.NewSimpleClientset() svcAccount := &corev1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ Name: "my-service-account", }, ImagePullSecrets: []corev1.LocalObjectReference{ {Name: "my-sa-secret"}, }, } svcAccountSecretDockerConfigJson := []byte(`{"auths":{"my-sa-secret.registry.example.com":{"username":"my-sa-secret","password":"xxxxxxxxxxx","email":"jdoe@example.com","auth":"c3R...zE2"}}}`) svcAccountSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "my-sa-secret", }, Type: corev1.SecretTypeDockerConfigJson, Data: map[string][]byte{ corev1.DockerConfigJsonKey: svcAccountSecretDockerConfigJson, }, } podSecretDockerConfigJson := []byte(`{"auths":{"my-pod-secret.registry.example.com":{"username":"my-sa-secret","password":"xxxxxxxxxxx","email":"jdoe@example.com","auth":"c3R...zE2"}}}`) podSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "my-pod-secret", }, Type: corev1.SecretTypeDockerConfigJson, Data: map[string][]byte{ corev1.DockerConfigJsonKey: podSecretDockerConfigJson, }, } _, _ = clientSet.CoreV1().ServiceAccounts("test-ns").Create(context.Background(), svcAccount, metav1.CreateOptions{}) _, _ = clientSet.CoreV1().Secrets("test-ns").Create(context.Background(), svcAccountSecret, metav1.CreateOptions{}) _, _ = clientSet.CoreV1().Secrets("test-ns").Create(context.Background(), podSecret, metav1.CreateOptions{}) provider := secrets.NewKubernetesImagePullSecretsProvider(clientSet) copier := pond.New(1, 1) // TODO: test types.ImageSwapPolicyExists wh, err := NewImageSwapperWebhookWithOpts( registryClient, ImagePullSecretsProvider(provider), Copier(copier), ImageSwapPolicy(types.ImageSwapPolicyAlways), ) assert.NoError(t, err, "NewImageSwapperWebhookWithOpts executed without errors") resp, err := wh.Review(context.Background(), admissionReviewModel) assert.JSONEq(t, "[{\"op\":\"replace\",\"path\":\"/spec/containers/0/image\",\"value\":\"123456789.dkr.ecr.ap-southeast-2.amazonaws.com/docker.io/library/nginx:latest\"}]", string(resp.(*model.MutatingAdmissionResponse).JSONPatchPatch)) assert.Nil(t, resp.(*model.MutatingAdmissionResponse).Warnings) assert.NoError(t, err, "Webhook executed without errors") // Ensure the worker pool is empty before asserting ecrClient copier.StopAndWait() ecrClient.AssertExpectations(t) } func TestImageSwapper_GAR_Mutate(t *testing.T) { registryClient, _ := registry.NewMockGARClient(nil, "us-central1-docker.pkg.dev/gcp-project-123/main") admissionReview, _ := readAdmissionReviewFromFile("admissionreview-simple.json") admissionReviewModel := model.NewAdmissionReviewV1(admissionReview) copier := pond.New(1, 1) // TODO: test types.ImageSwapPolicyExists wh, err := NewImageSwapperWebhookWithOpts( registryClient, Copier(copier), ImageSwapPolicy(types.ImageSwapPolicyAlways), ) assert.NoError(t, err, "NewImageSwapperWebhookWithOpts executed without errors") resp, err := wh.Review(context.TODO(), admissionReviewModel) // container with name "skip-test-gar" should be skipped, hence there is no "replace" operation for it expected := `[ {"op":"replace","path":"/spec/initContainers/0/image","value":"us-central1-docker.pkg.dev/gcp-project-123/main/docker.io/library/init-container:latest"}, {"op":"replace","path":"/spec/containers/0/image","value":"us-central1-docker.pkg.dev/gcp-project-123/main/docker.io/library/nginx:latest"}, {"op":"replace","path":"/spec/containers/1/image","value":"us-central1-docker.pkg.dev/gcp-project-123/main/k8s.gcr.io/ingress-nginx/controller@sha256:9bba603b99bf25f6d117cf1235b6598c16033ad027b143c90fa5b3cc583c5713"}, {"op":"replace","path":"/spec/containers/2/image","value":"us-central1-docker.pkg.dev/gcp-project-123/main/123456789.dkr.ecr.ap-southeast-2.amazonaws.com/k8s.gcr.io/ingress-nginx/controller@sha256:9bba603b99bf25f6d117cf1235b6598c16033ad027b143c90fa5b3cc583c5713"} ]` assert.JSONEq(t, expected, string(resp.(*model.MutatingAdmissionResponse).JSONPatchPatch)) assert.Nil(t, resp.(*model.MutatingAdmissionResponse).Warnings) assert.NoError(t, err, "Webhook executed without errors") } ================================================ FILE: test/curl.sh ================================================ #!/bin/bash data='{"kind":"AdmissionReview","apiVersion":"admission.k8s.io/v1","request":{"uid":"c78e0c58-7389-4838-b4f5-28d6005c1cc3","kind":{"group":"","version":"v1","kind":"Pod"},"resource":{"group":"","version":"v1","resource":"pods"},"requestKind":{"group":"","version":"v1","kind":"Pod"},"requestResource":{"group":"","version":"v1","resource":"pods"},"name":"nginx28","namespace":"default","operation":"CREATE","userInfo":{"username":"kubernetes-admin","groups":["system:masters","system:authenticated"]},"object":{"kind":"Pod","apiVersion":"v1","metadata":{"name":"nginx28","creationTimestamp":null,"labels":{"run":"nginx28"}},"spec":{"volumes":[{"name":"default-token-fjtvr","secret":{"secretName":"default-token-fjtvr"}}],"containers":[{"name":"nginx28","image":"nginx","resources":{},"volumeMounts":[{"name":"default-token-fjtvr","readOnly":true,"mountPath":"/var/run/secrets/kubernetes.io/serviceaccount"}],"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","imagePullPolicy":"Always"}],"restartPolicy":"Never","terminationGracePeriodSeconds":30,"dnsPolicy":"ClusterFirst","serviceAccountName":"default","serviceAccount":"default","securityContext":{},"schedulerName":"default-scheduler","tolerations":[{"key":"node.kubernetes.io/not-ready","operator":"Exists","effect":"NoExecute","tolerationSeconds":300},{"key":"node.kubernetes.io/unreachable","operator":"Exists","effect":"NoExecute","tolerationSeconds":300}],"priority":0,"enableServiceLinks":true},"status":{}},"oldObject":null,"dryRun":false,"options":{"kind":"CreateOptions","apiVersion":"meta.k8s.io/v1","fieldManager":"kubectl-run"}}}' curl --data "$data" http://127.0.0.1:8080/webhook ================================================ FILE: test/e2e_test.go ================================================ //go:build integration // +build integration package test import ( "bytes" "encoding/base64" "fmt" "os" "path/filepath" "regexp" "strings" "testing" "time" awssdk "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ecr" "github.com/gruntwork-io/terratest/modules/aws" "github.com/gruntwork-io/terratest/modules/helm" "github.com/gruntwork-io/terratest/modules/k8s" "github.com/gruntwork-io/terratest/modules/logger" "github.com/gruntwork-io/terratest/modules/random" "github.com/gruntwork-io/terratest/modules/shell" test_structure "github.com/gruntwork-io/terratest/modules/test-structure" terratesttesting "github.com/gruntwork-io/terratest/modules/testing" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // IsKindCluster returns true if the underlying kubernetes cluster is kind. This is determined by getting the // associated nodes and checking if a node is named "kind-control-plane". //func IsKindCluster(t terratestTesting.TestingT, options *k8s.KubectlOptions) (bool, error) { // nodes, err := k8s.GetNodesE(t, options) // if err != nil { // return false, err // } // // // ASSUMPTION: All minikube setups will have nodes with labels that are namespaced with minikube.k8s.io // for _, node := range nodes { // if !nodeHasMinikubeLabel(node) { // return false, nil // } // } // // // At this point we know that all the nodes in the cluster has the minikube label, so we return true. // return true, nil //} // nodeHasMinikubeLabel returns true if any of the labels on the node is namespaced with minikube.k8s.io //func nodeHasMinikubeLabel(node corev1.Node) bool { // labels := node.GetLabels() // for key, _ := range labels { // if strings.HasPrefix(key, "minikube.k8s.io") { // return true // } // } // return false //} // This file contains examples of how to use terratest to test helm charts by deploying the chart and verifying the // deployment by hitting the service endpoint. func TestHelmDeployment(t *testing.T) { workingDir, _ := filepath.Abs("..") awsAccountID := os.Getenv("AWS_ACCOUNT_ID") awsRegion := os.Getenv("AWS_DEFAULT_REGION") awsAccessKeyID := os.Getenv("AWS_ACCESS_KEY_ID") awsSecretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY") ecrRegistry := awsAccountID + ".dkr.ecr." + awsRegion + ".amazonaws.com" ecrRepository := "docker.io/library/nginx" logger.Default = logger.New(newSensitiveLogger( logger.Default, []*regexp.Regexp{ regexp.MustCompile(awsAccountID), regexp.MustCompile(awsAccessKeyID), regexp.MustCompile(awsSecretAccessKey), regexp.MustCompile(`(--docker-password=)\S+`), }, )) // To ensure we can reuse the resource config on the same cluster to test different scenarios, we setup a unique // namespace for the resources for this test. // Note that namespaces must be lowercase. namespaceName := fmt.Sprintf("k8s-image-swapper-%s", strings.ToLower(random.UniqueId())) releaseName := fmt.Sprintf("k8s-image-swapper-%s", strings.ToLower(random.UniqueId())) // Setup the kubectl config and context. Here we choose to use the defaults, which is: // - HOME/.kube/config for the kubectl config file // - Current context of the kubectl config file kubectlOptions := k8s.NewKubectlOptions("", "", namespaceName) // Init ECR client ecrClient := aws.NewECRClient(t, awsRegion) defer test_structure.RunTestStage(t, "cleanup_aws", func() { _, err := ecrClient.DeleteRepository(&ecr.DeleteRepositoryInput{ RepositoryName: awssdk.String(ecrRepository), RegistryId: awssdk.String(awsAccountID), Force: awssdk.Bool(true), }) require.NoError(t, err) }) defer test_structure.RunTestStage(t, "cleanup_k8s", func() { // Return the output before cleanup - helps in debugging k8s.RunKubectl(t, kubectlOptions, "logs", "--selector=app.kubernetes.io/name=k8s-image-swapper", "--tail=-1") helm.Delete(t, &helm.Options{KubectlOptions: kubectlOptions}, releaseName, true) k8s.DeleteNamespace(t, kubectlOptions, namespaceName) }) test_structure.RunTestStage(t, "build_and_load_docker_image", func() { // Generate docker image to be tested shell.RunCommand(t, shell.Command{ Command: "goreleaser", Args: []string{"release", "--snapshot", "--skip-publish", "--rm-dist"}, WorkingDir: workingDir, }) // Tag with "local" to ensure kind is not pulling from the GitHub Registry shell.RunCommand(t, shell.Command{ Command: "docker", Args: []string{"tag", "ghcr.io/estahn/k8s-image-swapper:latest", "local/k8s-image-swapper:latest"}, }) // Load generated docker image into kind shell.RunCommand(t, shell.Command{ Command: "kind", Args: []string{"load", "docker-image", "local/k8s-image-swapper:latest"}, }) }) test_structure.RunTestStage(t, "deploy_webhook", func() { k8s.CreateNamespace(t, kubectlOptions, namespaceName) // Setup permissions for kind to be able to pull from ECR ecrAuthToken, _ := ecrClient.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{}) ecrDecodedAuthToken, _ := base64.StdEncoding.DecodeString(*ecrAuthToken.AuthorizationData[0].AuthorizationToken) ecrUsernamePassword := bytes.Split(ecrDecodedAuthToken, []byte(":")) secretName := awsRegion + "-ecr-registry" k8s.RunKubectl(t, kubectlOptions, "create", "secret", "docker-registry", secretName, "--docker-server="+*ecrAuthToken.AuthorizationData[0].ProxyEndpoint, "--docker-username="+string(ecrUsernamePassword[0]), "--docker-password="+string(ecrUsernamePassword[1]), "--docker-email=anymail.doesnt.matter@email.com", ) k8s.RunKubectl(t, kubectlOptions, "patch", "serviceaccount", "default", "-p", fmt.Sprintf("{\"imagePullSecrets\":[{\"name\":\"%s\"}]}", secretName), ) // Setup the args. For this test, we will set the following input values: options := &helm.Options{ KubectlOptions: kubectlOptions, SetValues: map[string]string{ "config.logFormat": "console", "config.logLevel": "debug", "config.dryRun": "false", "config.target.aws.accountId": awsAccountID, "config.target.aws.region": awsRegion, "config.imageSwapPolicy": "always", "config.imageCopyPolicy": "delayed", "config.source.filters[0].jmespath": "obj.metadata.name != 'nginx'", "awsSecretName": "k8s-image-swapper-aws", "image.repository": "local/k8s-image-swapper", "image.tag": "latest", }, } k8s.RunKubectl(t, kubectlOptions, "create", "secret", "generic", "k8s-image-swapper-aws", fmt.Sprintf("--from-literal=aws_access_key_id=%s", awsAccessKeyID), fmt.Sprintf("--from-literal=aws_secret_access_key=%s", awsSecretAccessKey), ) // Deploy the chart using `helm install`. Note that we use the version without `E`, since we want to assert the // install succeeds without any errors. helm.Install(t, options, "estahn/k8s-image-swapper", releaseName) }) test_structure.RunTestStage(t, "validate", func() { k8s.WaitUntilNumPodsCreated(t, kubectlOptions, metav1.ListOptions{LabelSelector: "app.kubernetes.io/name=k8s-image-swapper"}, 1, 30, 10*time.Second) k8s.WaitUntilServiceAvailable(t, kubectlOptions, releaseName, 30, 10*time.Second) // Launch nginx container to verify functionality k8s.RunKubectl(t, kubectlOptions, "run", "nginx", "--image=nginx", "--restart=Never") k8s.WaitUntilPodAvailable(t, kubectlOptions, "nginx", 30, 10*time.Second) // Verify container is running with images from ECR. // Implicit proof for repository creation and images pull/push via k8s-image-swapper. nginxPod := k8s.GetPod(t, kubectlOptions, "nginx") require.Equal(t, ecrRegistry+"/"+ecrRepository+":latest", nginxPod.Spec.Containers[0].Image, "container should be prefixed with ECR address") }) } type sensitiveLogger struct { logger logger.TestLogger patterns []*regexp.Regexp } func newSensitiveLogger(logger *logger.Logger, patterns []*regexp.Regexp) *sensitiveLogger { return &sensitiveLogger{ logger: logger, patterns: patterns, } } func (l *sensitiveLogger) Logf(t terratesttesting.TestingT, format string, args ...interface{}) { var redactedArgs []interface{} obfuscateWith := "$1*******" redactedArgs = args for _, pattern := range l.patterns { for i, arg := range redactedArgs { switch arg := arg.(type) { case string: redactedArgs[i] = pattern.ReplaceAllString(arg, obfuscateWith) case []string: var result []string for _, s := range arg { result = append(result, pattern.ReplaceAllString(s, obfuscateWith)) } redactedArgs[i] = result default: panic("type needs implementation") } } } l.logger.Logf(t, format, redactedArgs...) } ================================================ FILE: test/kind-with-registry.sh ================================================ #!/bin/sh set -o errexit # create registry container unless it already exists reg_name='kind-registry' reg_port='5000' running="$(docker inspect -f '{{.State.Running}}' "${reg_name}" 2>/dev/null || true)" if [ "${running}" != 'true' ]; then docker run \ -d --restart=always -p "127.0.0.1:${reg_port}:5000" --name "${reg_name}" \ registry:2 fi # create a cluster with the local registry enabled in containerd envsubst < test/kind.yaml | kind create cluster --config=- # connect the registry to the cluster network # (the network may already be connected) docker network connect "kind" "${reg_name}" || true # Document the local registry # https://github.com/kubernetes/enhancements/tree/master/keps/sig-cluster-lifecycle/generic/1755-communicating-a-local-registry cat <