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
================================================
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.
[](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:
[](http://slack.kubernetes.io/)
[](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
[](https://github.com/estahn/k8s-image-swapper/releases/latest)
[](https://artifacthub.io/packages/helm/estahn/k8s-image-swapper)
[](/LICENSE.md)
[](https://codecov.io/gh/estahn/k8s-image-swapper)
[](http://godoc.org/github.com/estahn/k8s-image-swapper)
## :star2: Stargazers over time
[](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.
{ 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
================================================
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

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