Compare commits

..

No commits in common. "main" and "api/v0.20.1" have entirely different histories.

140 changed files with 9464 additions and 13337 deletions

View File

@ -1 +1 @@
build/
build/libgit2/

View File

@ -1,32 +0,0 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
labels: ["dependencies"]
schedule:
interval: "monthly"
groups:
go-deps:
patterns:
- "*"
allow:
- dependency-type: "direct"
ignore:
# Kubernetes deps are updated by fluxcd/pkg
- dependency-name: "k8s.io/*"
- dependency-name: "sigs.k8s.io/*"
- dependency-name: "github.com/go-logr/*"
# Flux APIs are updated at release time
- dependency-name: "github.com/fluxcd/image-automation-controller/api"
- dependency-name: "github.com/fluxcd/image-reflector-controller/api"
- dependency-name: "github.com/fluxcd/source-controller/api"
- package-ecosystem: "github-actions"
directory: "/"
labels: ["area/ci", "dependencies"]
groups:
ci:
patterns:
- "*"
schedule:
interval: "monthly"

14
.github/labels.yaml vendored
View File

@ -1,14 +0,0 @@
# Configuration file to declaratively configure labels
# Ref: https://github.com/EndBug/label-sync#Config-files
- name: area/git
description: Git related issues and pull requests
color: '#863faf'
- name: area/kyaml
description: YAML patching related issues and pull requests
color: '#86dbf2'
# TODO: enable this when we have a release/v1.0.x branch
#- name: backport:release/v1.0.x
# description: To be backported to release/v1.0.x
# color: '#ffd700'

View File

@ -1,31 +0,0 @@
name: backport
on:
pull_request_target:
types: [closed, labeled]
jobs:
pull-request:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
if: github.event.pull_request.state == 'closed' && github.event.pull_request.merged && (github.event_name != 'labeled' || startsWith('backport:', github.event.label.name))
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Create backport PRs
uses: korthout/backport-action@ca4972adce8039ff995e618f5fc02d1b7961f27a # v3.3.0
# xref: https://github.com/korthout/backport-action#inputs
with:
# Use token to allow workflows to be triggered for the created PR
github_token: ${{ secrets.BOT_GITHUB_TOKEN }}
# Match labels with a pattern `backport:<target-branch>`
label_pattern: '^backport:([^ ]+)$'
# A bit shorter pull-request title than the default
pull_title: '[${target_branch}] ${pull_title}'
# Simpler PR description than default
pull_description: |-
Automated backport to `${target_branch}`, triggered by a label in #${pull_number}.

View File

@ -1,9 +1,9 @@
name: build
on:
workflow_dispatch:
pull_request:
push:
branches: [ 'main', 'release/**' ]
branches:
- main
permissions:
contents: read # for actions/checkout to fetch code
@ -13,30 +13,59 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
uses: actions/setup-go@v2
with:
go-version: 1.25.x
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Setup QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- name: Setup Docker Buildx
id: buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
go-version: 1.17.x
- name: Restore go cache
uses: actions/cache@v1
with:
# the ff is mounted into the container as ~/go/pkg/mod
path: /home/runner/work/_temp/_github_home/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Run tests
run: make test
- name: Verify
run: make verify
- name: Build multi-arch container image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
push: false
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm/v7,linux/arm64
tags: |
${{ github.repository }}:latest
kind-linux-arm64:
# Hosted on Equinix
# Docs: https://github.com/fluxcd/flux2/tree/main/.github/runners
runs-on: [self-hosted, Linux, ARM64, equinix]
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: 1.17.x
- name: Run tests
run: make test
- name: Verify
run: make verify
# Runs 'make test' on macos-10.15 to assure development environment for
# contributors using MacOS.
darwin-amd64:
runs-on: macos-10.15
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: 1.17.x
- name: Restore Go cache
uses: actions/cache@v1
with:
path: /home/runner/work/_temp/_github_home/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Run tests
run: make test
- name: Verify
run: make verify

View File

@ -1,23 +1,24 @@
name: fuzz
name: CIFuzz
on:
pull_request:
branches: [ 'main', 'release/**' ]
branches:
- main
permissions:
contents: read # for actions/checkout to fetch code
contents: read
jobs:
smoketest:
Fuzzing:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: 1.25.x
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Smoke test Fuzzers
run: make fuzz-smoketest
- name: Checkout
uses: actions/checkout@v2
- name: Restore Go cache
uses: actions/cache@v1
with:
path: /home/runner/work/_temp/_github_home/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Smoke test Fuzzers
run: make fuzz-smoketest

36
.github/workflows/nightly.yaml vendored Normal file
View File

@ -0,0 +1,36 @@
name: nightly
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
env:
REPOSITORY: ${{ github.repository }}
permissions:
contents: read # for actions/checkout to fetch code
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup QEMU
uses: docker/setup-qemu-action@v1
with:
platforms: all
- name: Setup Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
with:
buildkitd-flags: "--debug"
- name: Build multi-arch container image
uses: docker/build-push-action@v2
with:
push: false
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm/v7,linux/arm64
tags: |
${{ env.REPOSITORY }}:nightly

View File

@ -7,29 +7,22 @@ on:
inputs:
tag:
description: 'image tag prefix'
default: 'preview'
default: 'rc'
required: true
permissions:
contents: read
contents: write # needed to write releases
id-token: write # needed for keyless signing
packages: write # needed for ghcr access
env:
CONTROLLER: ${{ github.event.repository.name }}
jobs:
release:
outputs:
hashes: ${{ steps.slsa.outputs.hashes }}
image_url: ${{ steps.slsa.outputs.image_url }}
image_digest: ${{ steps.slsa.outputs.image_digest }}
build-push:
runs-on: ubuntu-latest
permissions:
contents: write # for creating the GitHub release.
id-token: write # for creating OIDC tokens for signing.
packages: write # for pushing and signing container images.
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v2
- name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@main
- name: Prepare
@ -39,27 +32,27 @@ jobs:
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION=${GITHUB_REF/refs\/tags\//}
fi
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
echo ::set-output name=BUILD_DATE::$(date -u +'%Y-%m-%dT%H:%M:%SZ')
echo ::set-output name=VERSION::${VERSION}
- name: Setup QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
uses: docker/setup-qemu-action@v1
- name: Setup Docker Buildx
id: buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
uses: docker/setup-buildx-action@v1
- name: Login to GitHub Container Registry
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
uses: docker/login-action@v1
with:
registry: ghcr.io
username: fluxcdbot
password: ${{ secrets.GHCR_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
uses: docker/login-action@v1
with:
username: fluxcdbot
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
- name: Generate images meta
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
uses: docker/metadata-action@v3
with:
images: |
fluxcd/${{ env.CONTROLLER }}
@ -67,11 +60,8 @@ jobs:
tags: |
type=raw,value=${{ steps.prep.outputs.VERSION }}
- name: Publish images
id: build-push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@v2
with:
sbom: true
provenance: true
push: true
builder: ${{ steps.buildx.outputs.name }}
context: .
@ -79,82 +69,32 @@ jobs:
platforms: linux/amd64,linux/arm/v7,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
- name: Check images
run: |
docker buildx imagetools inspect docker.io/fluxcd/${{ env.CONTROLLER }}:${{ steps.prep.outputs.VERSION }}
docker buildx imagetools inspect ghcr.io/fluxcd/${{ env.CONTROLLER }}:${{ steps.prep.outputs.VERSION }}
docker pull docker.io/fluxcd/${{ env.CONTROLLER }}:${{ steps.prep.outputs.VERSION }}
docker pull ghcr.io/fluxcd/${{ env.CONTROLLER }}:${{ steps.prep.outputs.VERSION }}
- uses: sigstore/cosign-installer@main
- name: Sign images
env:
COSIGN_EXPERIMENTAL: 1
run: |
cosign sign --yes fluxcd/${{ env.CONTROLLER }}@${{ steps.build-push.outputs.digest }}
cosign sign --yes ghcr.io/fluxcd/${{ env.CONTROLLER }}@${{ steps.build-push.outputs.digest }}
cosign sign fluxcd/${{ env.CONTROLLER }}:${{ steps.prep.outputs.VERSION }}
cosign sign ghcr.io/fluxcd/${{ env.CONTROLLER }}:${{ steps.prep.outputs.VERSION }}
- name: Generate release artifacts
if: startsWith(github.ref, 'refs/tags/v')
run: |
mkdir -p config/release
kustomize build ./config/crd > ./config/release/${{ env.CONTROLLER }}.crds.yaml
kustomize build ./config/manager > ./config/release/${{ env.CONTROLLER }}.deployment.yaml
- uses: anchore/sbom-action/download-syft@da167eac915b4e86f08b264dbdbc867b61be6f0c # v0.20.5
echo '[CHANGELOG](https://github.com/fluxcd/${{ env.CONTROLLER }}/blob/main/CHANGELOG.md)' > ./config/release/notes.md
- uses: anchore/sbom-action/download-syft@v0
- name: Create release and SBOM
id: run-goreleaser
if: startsWith(github.ref, 'refs/tags/v')
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
uses: goreleaser/goreleaser-action@v2
with:
version: latest
args: release --clean --skip=validate
args: release --release-notes=config/release/notes.md --rm-dist --skip-validate
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate SLSA metadata
id: slsa
env:
ARTIFACTS: "${{ steps.run-goreleaser.outputs.artifacts }}"
run: |
hashes=$(echo -E $ARTIFACTS | jq --raw-output '.[] | {name, "digest": (.extra.Digest // .extra.Checksum)} | select(.digest) | {digest} + {name} | join(" ") | sub("^sha256:";"")' | base64 -w0)
echo "hashes=$hashes" >> $GITHUB_OUTPUT
image_url=fluxcd/${{ env.CONTROLLER }}:${{ steps.prep.outputs.version }}
echo "image_url=$image_url" >> $GITHUB_OUTPUT
image_digest=${{ steps.build-push.outputs.digest }}
echo "image_digest=$image_digest" >> $GITHUB_OUTPUT
release-provenance:
needs: [release]
permissions:
actions: read # for detecting the Github Actions environment.
id-token: write # for creating OIDC tokens for signing.
contents: write # for uploading attestations to GitHub releases.
if: startsWith(github.ref, 'refs/tags/v')
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
with:
provenance-name: "provenance.intoto.jsonl"
base64-subjects: "${{ needs.release.outputs.hashes }}"
upload-assets: true
dockerhub-provenance:
needs: [release]
permissions:
actions: read # for detecting the Github Actions environment.
id-token: write # for creating OIDC tokens for signing.
packages: write # for uploading attestations.
if: startsWith(github.ref, 'refs/tags/v')
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
with:
image: ${{ needs.release.outputs.image_url }}
digest: ${{ needs.release.outputs.image_digest }}
registry-username: fluxcdbot
secrets:
registry-password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
ghcr-provenance:
needs: [release]
permissions:
actions: read # for detecting the Github Actions environment.
id-token: write # for creating OIDC tokens for signing.
packages: write # for uploading attestations.
if: startsWith(github.ref, 'refs/tags/v')
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
with:
image: ghcr.io/${{ needs.release.outputs.image_url }}
digest: ${{ needs.release.outputs.image_digest }}
registry-username: fluxcdbot
secrets:
registry-password: ${{ secrets.GHCR_TOKEN }}

View File

@ -1,10 +1,10 @@
name: scan
name: Scan
on:
push:
branches: [ 'main', 'release/**' ]
branches: [ main ]
pull_request:
branches: [ 'main', 'release/**' ]
branches: [ main ]
schedule:
- cron: '18 10 * * 3'
@ -17,10 +17,9 @@ jobs:
name: FOSSA
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v2
- name: Run FOSSA scan and upload build data
uses: fossa-contrib/fossa-action@3d2ef181b1820d6dcd1972f86a767d18167fa19b # v3.0.1
uses: fossa-contrib/fossa-action@v1
with:
# FOSSA Push-Only API Token
fossa-api-key: 5ee8bf422db1471e0bcf2bcb289185de
@ -30,23 +29,13 @@ jobs:
name: CodeQL
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: 1.25.x
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Checkout repository
uses: actions/checkout@v2
- name: Initialize CodeQL
uses: github/codeql-action/init@3c3833e0f8c1c83d449a7478aa59c036a9165498 # v3.29.11
uses: github/codeql-action/init@v1
with:
languages: go
# xref: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# xref: https://codeql.github.com/codeql-query-help/go/
queries: security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@3c3833e0f8c1c83d449a7478aa59c036a9165498 # v3.29.11
uses: github/codeql-action/autobuild@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@3c3833e0f8c1c83d449a7478aa59c036a9165498 # v3.29.11
uses: github/codeql-action/analyze@v1

View File

@ -1,28 +0,0 @@
name: sync-labels
on:
workflow_dispatch:
push:
branches:
- main
paths:
- .github/labels.yaml
permissions:
contents: read
jobs:
labels:
name: Run sync
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2.3.3
with:
# Configuration file
config-file: |
https://raw.githubusercontent.com/fluxcd/community/main/.github/standard-labels.yaml
.github/labels.yaml
# Strictly declarative
delete-other-labels: true

7
.gitignore vendored
View File

@ -2,7 +2,7 @@ notes
# Thes are downloaded in the Makefile
cache/*
internal/controller/testdata/crds/*
controllers/testdata/crds/*
# Binaries for programs and plugins
*.exe
@ -29,8 +29,5 @@ testbin
*.swo
*~
# Exclude all build related files
# Exclude all libgit2 related files
build/
# CRDs for fuzzing tests.
internal/controller/testdata/crd

View File

@ -4,26 +4,9 @@ builds:
- skip: true
release:
prerelease: "true"
extra_files:
- glob: config/release/*.yaml
prerelease: "true"
header: |
## Changelog
[{{.Tag}} changelog](https://github.com/fluxcd/{{.ProjectName}}/blob/{{.Tag}}/CHANGELOG.md)
footer: |
## Container images
- `docker.io/fluxcd/{{.ProjectName}}:{{.Tag}}`
- `ghcr.io/fluxcd/{{.ProjectName}}:{{.Tag}}`
Supported architectures: `linux/amd64`, `linux/arm64` and `linux/arm/v7`.
The container images are built on GitHub hosted runners and are signed with cosign and GitHub OIDC.
To verify the images and their provenance (SLSA level 3), please see the [security documentation](https://fluxcd.io/flux/security/).
changelog:
disable: true
checksum:
extra_files:
@ -49,7 +32,6 @@ signs:
certificate: "${artifact}.pem"
args:
- sign-blob
- "--yes"
- "--output-certificate=${certificate}"
- "--output-signature=${signature}"
- "${artifact}"

1239
ATTRIBUTIONS.md Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
# Development
> **Note:** Please take a look at <https://fluxcd.io/contributing/flux/>
> **Note:** Please take a look at <https://fluxcd.io/docs/contributing/flux/>
> to find out about how to contribute to Flux and how to interact with the
> Flux Development team.
@ -13,9 +13,82 @@ There are a number of dependencies required to be able to run the controller and
- [Install Docker](https://docs.docker.com/engine/install/)
- (Optional) [Install Kubebuilder](https://book.kubebuilder.io/quick-start.html#installation)
The following dependencies are also used by some of the `make` targets:
The dependency [libgit2](https://libgit2.org/) also needs to be installed to be able
to run `image-automation-controller` or its test-suite locally (not in a container).
- `controller-gen` (v0.19.0)
In case this dependency is not present on your system (at the expected
version), the first invocation of a `make` target that requires the
dependency will attempt to compile it locally to `hack/libgit2`. For this build
to succeed ensure the following dependencies are present on your system:
- [CMake](https://cmake.org/download/)
- [OpenSSL 1.1](https://www.openssl.org/source/)
- [LibSSH2](https://www.libssh2.org/)
- [pkg-config](https://freedesktop.org/wiki/Software/pkg-config/)
Triggering a manual build of the dependency is possible as well by running
`make libgit2`. To enforce the build, for example if your system dependencies
match but are not linked in a compatible way, append `LIBGIT2_FORCE=1` to the
`make` command.
Follow the instructions below to install these dependencies to your system.
### macOS
```console
$ # Ensure libgit2 dependencies are available
$ brew install cmake openssl@1.1 libssh2 pkg-config
$ LIBGIT2_FORCE=1 make libgit2
```
#### openssl and pkg-config
You may see this message when trying to run a build:
```
# pkg-config --cflags libgit2
Package openssl was not found in the pkg-config search path.
Perhaps you should add the directory containing `openssl.pc'
to the PKG_CONFIG_PATH environment variable
Package 'openssl', required by 'libgit2', not found
pkg-config: exit status 1
```
On some macOS systems `brew` will not link the pkg-config file for
openssl into expected directory, because it clashes with a
system-provided openssl. When installing it, or if you do
```console
brew link openssl@1.1
Warning: Refusing to link macOS provided/shadowed software: openssl@1.1
If you need to have openssl@1.1 first in your PATH, run:
echo 'export PATH="/usr/local/opt/openssl@1.1/bin:$PATH"' >> ~/.profile
For compilers to find openssl@1.1 you may need to set:
export LDFLAGS="-L/usr/local/opt/openssl@1.1/lib"
export CPPFLAGS="-I/usr/local/opt/openssl@1.1/include"
For pkg-config to find openssl@1.1 you may need to set:
export PKG_CONFIG_PATH="/usr/local/opt/openssl@1.1/lib/pkgconfig"
```
.. it tells you (in the last part of the message) how to add `openssl`
to your `PKG_CONFIG_PATH` so it can be found.
### Linux
```console
$ # Ensure libgit2 dependencies are available
$ pacman -S cmake openssl libssh2
$ LIBGIT2_FORCE=1 make libgit2
```
**Note:** Example shown is for Arch Linux, but likewise procedure can be
followed using any other package manager, e.g. `apt`.
In addition to the above, the following dependencies are also used by some of the `make` targets:
- `controller-gen` (v0.7.0)
- `gen-crd-api-reference-docs` (v0.3.0)
- `setup-envtest` (latest)
@ -24,7 +97,7 @@ If any of the above dependencies are not present on your system, the first invoc
## How to run the test suite
Prerequisites:
* Go >= 1.25
* Go >= 1.17
You can run the test suite by simply doing
@ -82,25 +155,4 @@ Deploy `image-automation-controller` into the cluster that is configured in the
```sh
make deploy
```
### Debugging controller with VSCode
Create a `.vscode/launch.json` file:
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/main.go"
}
]
}
```
Start debugging by either clicking `Run` > `Start Debugging` or using
the relevant shortcut.
```

View File

@ -1,15 +1,28 @@
ARG GO_VERSION=1.25
ARG XX_VERSION=1.6.1
ARG BASE_VARIANT=alpine
ARG GO_VERSION=1.17
ARG XX_VERSION=1.1.0
ARG LIBGIT2_IMG=ghcr.io/fluxcd/golang-with-libgit2
ARG LIBGIT2_TAG=libgit2-1.1.1-7
FROM ${LIBGIT2_IMG}:${LIBGIT2_TAG} AS libgit2-libs
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
# Docker buildkit multi-arch build requires golang alpine
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-${BASE_VARIANT} as gostable
FROM gostable AS go-linux
# Build-base consists of build platform dependencies and xx.
# These will be used at current arch to yield execute the cross compilations.
FROM go-${TARGETOS} AS build-base
RUN apk add --no-cache clang lld pkgconfig
# Copy the build utilities.
COPY --from=xx / /
ARG TARGETPLATFORM
# build-go-mod can still be cached at build platform architecture.
FROM build-base as build-go-mod
# Configure workspace
WORKDIR /workspace
@ -24,24 +37,76 @@ COPY go.sum go.sum
# Cache modules
RUN go mod download
# Copy source code
COPY main.go main.go
COPY internal/ internal/
# The musl-tool-chain layer is an adhoc solution
# for the problem in which xx gets confused during compilation
# and a) looks for gold linker and then b) cannot find musl's dynamic linker.
FROM --platform=$BUILDPLATFORM alpine as musl-tool-chain
COPY --from=xx / /
RUN apk add bash curl tar
WORKDIR /workspace
COPY hack/download-musl.sh .
ARG TARGETPLATFORM
ARG TARGETARCH
RUN ROOT_DIR="$(pwd)" TARGET_ARCH="$(xx-info alpine-arch)" ENV_FILE=true \
./download-musl.sh
# build without specifing the arch
ENV CGO_ENABLED=0
RUN xx-go build -trimpath -a -o image-automation-controller main.go
# Build stage install per target platform
# dependency and effectively cross compile the application.
FROM build-go-mod as build
FROM alpine:3.22
ARG TARGETPLATFORM
COPY --from=libgit2-libs /usr/local/ /usr/local/
# Some dependencies have to installed
# for the target platform: https://github.com/tonistiigi/xx#go--cgo
RUN xx-apk add musl-dev gcc lld
WORKDIR /workspace
# Copy source code
COPY main.go main.go
COPY controllers/ controllers/
COPY pkg/ pkg/
COPY --from=musl-tool-chain /workspace/build /workspace/build
ARG TARGETPLATFORM
ARG TARGETARCH
ENV CGO_ENABLED=1
# Performance related changes:
# - Use read-only bind instead of copying go source files.
# - Cache go packages.
RUN export $(cat build/musl/$(xx-info alpine-arch).env | xargs) && \
export LIBRARY_PATH="/usr/local/$(xx-info triple):/usr/local/$(xx-info triple)/lib64" && \
export PKG_CONFIG_PATH="/usr/local/$(xx-info triple)/lib/pkgconfig:/usr/local/$(xx-info triple)/lib64/pkgconfig" && \
export CGO_LDFLAGS="$(pkg-config --static --libs --cflags libssh2 openssl libgit2) -static" && \
GOARCH=$TARGETARCH go build \
-ldflags "-s -w" \
-tags 'netgo,osusergo,static_build' \
-o /image-automation-controller -trimpath main.go;
# Ensure that the binary was cross-compiled correctly to the target platform.
RUN xx-verify --static /image-automation-controller
FROM alpine:3.15
ARG TARGETPLATFORM
RUN apk --no-cache add ca-certificates \
&& update-ca-certificates
COPY --from=builder /workspace/image-automation-controller /usr/local/bin/
# Create minimal nsswitch.conf file to prioritize the usage of /etc/hosts over DNS queries.
# https://github.com/gliderlabs/docker-alpine/issues/367#issuecomment-354316460
RUN [ ! -e /etc/nsswitch.conf ] && echo 'hosts: files dns' > /etc/nsswitch.conf
# Copy over binary from build
COPY --from=build /image-automation-controller /usr/local/bin/
COPY ATTRIBUTIONS.md /
USER 65534:65534
ENTRYPOINT [ "image-automation-controller" ]

View File

@ -9,8 +9,7 @@ from the main Flux v2 git repository, as listed in
In alphabetical order:
Dipti Pai, Microsoft <diptipai@microsoft.com> (github: @dipti-pai, slack: Dipti Pai)
Paulo Gomes, SUSE <pjbgf@linux.com> (github: @pjbgf, slack: pjbgf)
Paulo Gomes, Weaveworks <paulo.gomes@weave.works> (github: @pjbgf, slack: pjbgf)
Retired maintainers:

125
Makefile
View File

@ -6,37 +6,75 @@ TAG ?= latest
# Produce CRDs that work back to Kubernetes 1.16
CRD_OPTIONS ?= crd:crdVersions=v1
# Base image used to build the Go binary
LIBGIT2_IMG ?= ghcr.io/fluxcd/golang-with-libgit2
LIBGIT2_TAG ?= libgit2-1.1.1-7
# Allows for defining additional Docker buildx arguments,
# e.g. '--push'.
BUILD_ARGS ?=
# Architectures to build images for
BUILD_PLATFORMS ?= linux/amd64,linux/arm64,linux/arm/v7
# Allows for defining additional Go test args, e.g. '-tags integration'.
GO_TEST_ARGS ?= -race
# Directory with versioned, downloaded things
CACHE := cache
# Version of the source-controller from which to get the GitRepository CRD.
# Pulls source-controller/api's version set in go.mod.
SOURCE_VER ?= $(shell go list -m github.com/fluxcd/source-controller/api | awk '{print $$2}')
# Change this if you bump the source-controller/api version in go.mod.
SOURCE_VER ?= v0.21.1
# Version of the image-reflector-controller from which to get the ImagePolicy CRD.
# Pulls image-reflector-controller/api's version set in go.mod.
REFLECTOR_VER ?= $(shell go list -m github.com/fluxcd/image-reflector-controller/api | awk '{print $$2}')
# Change this if you bump the image-reflector-controller/api version in go.mod.
REFLECTOR_VER ?= v0.16.0
# Repository root based on Git metadata.
REPOSITORY_ROOT := $(shell git rev-parse --show-toplevel)
BUILD_DIR := $(REPOSITORY_ROOT)/build
# FUZZ_TIME defines the max amount of time, in Go Duration,
# each fuzzer should run for.
FUZZ_TIME ?= 1m
# Other dependency versions
ENVTEST_BIN_VERSION ?= 1.19.2
# Caches libgit2 versions per tag, "forcing" rebuild only when needed.
LIBGIT2_PATH := $(BUILD_DIR)/libgit2/$(LIBGIT2_TAG)
LIBGIT2_LIB_PATH := $(LIBGIT2_PATH)/lib
LIBGIT2_LIB64_PATH := $(LIBGIT2_PATH)/lib64
LIBGIT2 := $(LIBGIT2_LIB_PATH)/libgit2.a
MUSL-CC =
export CGO_ENABLED=1
export PKG_CONFIG_PATH=$(LIBGIT2_LIB_PATH)/pkgconfig
export LIBRARY_PATH=$(LIBGIT2_LIB_PATH)
export CGO_CFLAGS=-I$(LIBGIT2_PATH)/include -I$(LIBGIT2_PATH)/include/openssl
# The pkg-config command will yield warning messages until libgit2 is downloaded.
ifeq ($(shell uname -s),Darwin)
export CGO_LDFLAGS=$(shell PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) pkg-config --libs --static --cflags libssh2 openssl libgit2 2>/dev/null)
GO_STATIC_FLAGS=-ldflags "-s -w" -tags 'netgo,osusergo,static_build'
else
export PKG_CONFIG_PATH:=$(PKG_CONFIG_PATH):$(LIBGIT2_LIB64_PATH)/pkgconfig
export LIBRARY_PATH:=$(LIBRARY_PATH):$(LIBGIT2_LIB64_PATH)
export CGO_LDFLAGS=$(shell PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) pkg-config --libs --static --cflags libssh2 openssl libgit2 2>/dev/null)
endif
ifeq ($(shell uname -s),Linux)
ifeq ($(shell uname -m),x86_64)
# Linux x86_64 seem to be able to cope with the static libraries
# by having only musl-dev installed, without the need of using musl toolchain.
GO_STATIC_FLAGS=-ldflags "-s -w" -tags 'netgo,osusergo,static_build'
else
MUSL-PREFIX=$(BUILD_DIR)/musl/$(shell uname -m)-linux-musl-native/bin/$(shell uname -m)-linux-musl
MUSL-CC=$(MUSL-PREFIX)-gcc
export CC=$(MUSL-PREFIX)-gcc
export CXX=$(MUSL-PREFIX)-g++
export AR=$(MUSL-PREFIX)-ar
GO_STATIC_FLAGS=-ldflags "-s -w -extldflags \"-static\"" -tags 'netgo,osusergo,static_build'
endif
endif
# API (doc) generation utilities
CONTROLLER_GEN_VERSION ?= v0.19.0
GEN_API_REF_DOCS_VERSION ?= e327d0730470cbd61b06300f81c5fcf91c23c113
CONTROLLER_GEN_VERSION ?= v0.7.0
GEN_API_REF_DOCS_VERSION ?= v0.3.0
# If gobin not set, create one on ./build and add to path.
ifeq (,$(shell go env GOBIN))
@ -58,7 +96,7 @@ ifeq ($(shell uname -s),Darwin)
ENVTEST_ARCH=amd64
endif
TEST_CRDS := internal/controller/testdata/crds
TEST_CRDS := controllers/testdata/crds
# Log level for `make run`
LOG_LEVEL ?= info
@ -92,19 +130,25 @@ ${CACHE}/imagepolicies_${REFLECTOR_VER}.yaml:
curl -s --fail https://raw.githubusercontent.com/fluxcd/image-reflector-controller/${REFLECTOR_VER}/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml \
-o ${CACHE}/imagepolicies_${REFLECTOR_VER}.yaml
check-deps:
ifeq ($(shell uname -s),Darwin)
if ! command -v pkg-config &> /dev/null; then echo "pkg-config is required"; exit 1; fi
endif
KUBEBUILDER_ASSETS?="$(shell $(ENVTEST) --arch=$(ENVTEST_ARCH) use -i $(ENVTEST_KUBERNETES_VERSION) --bin-dir=$(ENVTEST_ASSETS_DIR) -p path)"
test: tidy test-api test_deps generate fmt vet manifests api-docs install-envtest ## Run tests
test: $(LIBGIT2) test-api test_deps generate fmt vet manifests api-docs install-envtest ## Run tests
KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) \
go test $(GO_TEST_ARGS) ./... -coverprofile cover.out
go test $(GO_STATIC_FLAGS) ./... -coverprofile cover.out
test-api: ## Run api tests
cd api; go test $(GO_TEST_ARGS) ./... -coverprofile cover.out
cd api; go test ./... -coverprofile cover.out
manager: generate fmt vet ## Build manager binary
go build -o $(BUILD_DIR)/bin/manager ./main.go
manager: $(LIBGIT2) generate fmt vet ## Build manager binary
go run ./main.go
run: generate fmt vet manifests # Run against the configured Kubernetes cluster in ~/.kube/config
go run ./main.go --log-level=${LOG_LEVEL} --log-encoding=console
run: $(LIBGIT2) generate fmt vet manifests # Run against the configured Kubernetes cluster in ~/.kube/config
go run $(GO_STATIC_FLAGS) ./main.go --log-level=${LOG_LEVEL} --log-encoding=console
install: manifests ## Install CRDs into a cluster
kustomize build config/crd | kubectl apply -f -
@ -123,21 +167,20 @@ dev-deploy: manifests
rm -rf config/dev
manifests: controller-gen ## Generate manifests e.g. CRD, RBAC etc.
$(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role paths="./..." output:crd:artifacts:config="config/crd/bases"
cd api; $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role paths="./..." output:crd:artifacts:config="../config/crd/bases"
api-docs: gen-crd-api-reference-docs ## Generate API reference documentation
$(GEN_CRD_API_REFERENCE_DOCS) -api-dir=./api/v1beta2 -config=./hack/api-docs/config.json -template-dir=./hack/api-docs/template -out-file=./docs/api/v1beta2/image-automation.md
$(GEN_CRD_API_REFERENCE_DOCS) -api-dir=./api/v1beta1 -config=./hack/api-docs/config.json -template-dir=./hack/api-docs/template -out-file=./docs/api/image-automation.md
tidy: ## Run go mod tidy
cd api; rm -f go.sum; go mod tidy -compat=1.25
rm -f go.sum; go mod tidy -compat=1.25
cd api; rm -f go.sum; go mod tidy
rm -f go.sum; go mod tidy
fmt: ## Run go fmt against code
go fmt ./...
cd api; go fmt ./...
vet: ## Run go vet against code
vet: $(LIBGIT2) ## Run go vet against code
go vet ./...
cd api; go vet ./...
@ -147,6 +190,8 @@ generate: controller-gen ## Generate code
docker-build: ## Build the Docker image
docker buildx build \
--build-arg LIBGIT2_IMG=$(LIBGIT2_IMG) \
--build-arg LIBGIT2_TAG=$(LIBGIT2_TAG) \
--platform=$(BUILD_PLATFORMS) \
-t $(IMG):$(TAG) \
$(BUILD_ARGS) .
@ -161,13 +206,23 @@ docker-deploy: ## Set the Docker image in-cluster
CONTROLLER_GEN = $(GOBIN)/controller-gen
.PHONY: controller-gen
controller-gen: ## Download controller-gen locally if necessary.
$(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_GEN_VERSION))
$(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.7.0)
libgit2: $(LIBGIT2) ## Detect or download libgit2 library
$(LIBGIT2): $(MUSL-CC)
IMG=$(LIBGIT2_IMG) TAG=$(LIBGIT2_TAG) ./hack/install-libraries.sh
$(MUSL-CC):
ifneq ($(shell uname -s),Darwin)
./hack/download-musl.sh
endif
# Find or download gen-crd-api-reference-docs
GEN_CRD_API_REFERENCE_DOCS = $(GOBIN)/gen-crd-api-reference-docs
.PHONY: gen-crd-api-reference-docs
gen-crd-api-reference-docs:
$(call go-install-tool,$(GEN_CRD_API_REFERENCE_DOCS),github.com/ahmetb/gen-crd-api-reference-docs@$(GEN_API_REF_DOCS_VERSION))
$(call go-install-tool,$(GEN_CRD_API_REFERENCE_DOCS),github.com/ahmetb/gen-crd-api-reference-docs@v0.3.0)
ENVTEST_ASSETS_DIR=$(BUILD_DIR)/testbin
ENVTEST_KUBERNETES_VERSION?=latest
@ -181,8 +236,8 @@ ENVTEST = $(GOBIN)/setup-envtest
setup-envtest: ## Download envtest-setup locally if necessary.
$(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest@latest)
# Build fuzzers used by oss-fuzz.
fuzz-build:
# Build fuzzers
fuzz-build: $(LIBGIT2)
rm -rf $(shell pwd)/build/fuzz/
mkdir -p $(shell pwd)/build/fuzz/out/
@ -193,7 +248,6 @@ fuzz-build:
-v "$(shell pwd)/build/fuzz/out":/out \
local-fuzzing:latest
# Run each fuzzer once to ensure they will work when executed by oss-fuzz.
fuzz-smoketest: fuzz-build
docker run --rm \
-v "$(shell pwd)/build/fuzz/out":/out \
@ -201,12 +255,6 @@ fuzz-smoketest: fuzz-build
local-fuzzing:latest \
bash -c "/runner.sh"
# Run fuzz tests for the duration set in FUZZ_TIME.
fuzz-native:
KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) \
FUZZ_TIME=$(FUZZ_TIME) \
./tests/fuzz/native_go_run.sh
# go-install-tool will 'go install' any package $2 and install it to $1.
define go-install-tool
@[ -f $(1) ] || { \
@ -220,7 +268,10 @@ rm -rf $$TMP_DIR ;\
}
endef
verify:
update-attributions:
./hack/update-attributions.sh
verify: update-attributions fmt
ifneq (, $(shell git status --porcelain --untracked-files=no))
@{ \
echo "working directory is dirty:"; \

View File

@ -3,8 +3,11 @@ repo: github.com/fluxcd/image-automation-controller
resources:
- group: image
kind: ImageUpdateAutomation
version: v1beta1
version: v1alpha1
- group: image
kind: ImageUpdateAutomation
version: v1beta2
version: v1alpha2
- group: image
kind: ImageUpdateAutomation
version: v1beta1
version: "2"

View File

@ -17,9 +17,15 @@ updating YAML files in a git repository, and committing the changes.
## How to install it
Please see the [installation and use
guide](https://fluxcd.io/flux/guides/image-update/).
guide](https://toolkit.fluxcd.io/guides/image-update/).
## How to work on it
For additional information on dependecies and how to contribute
please refer to [DEVELOPMENT.md](DEVELOPMENT.md).
The shared library `libgit2` needs to be installed to test or build
locally. The version required corresponds to the version of git2go
(which are Go bindings for libgit2), according to [this
table](https://github.com/libgit2/git2go#which-go-version-to-use).
See
https://github.com/fluxcd/source-controller/blob/main/DEVELOPMENT.md#installing-required-dependencies
for instructions on how to install `libgit2`.

View File

@ -1,34 +1,29 @@
module github.com/fluxcd/image-automation-controller/api
go 1.25.0
go 1.17
require (
github.com/fluxcd/pkg/apis/meta v1.20.0
github.com/fluxcd/source-controller/api v1.6.2
k8s.io/apimachinery v0.34.0
sigs.k8s.io/controller-runtime v0.22.0
github.com/fluxcd/pkg/apis/meta v0.10.2
github.com/fluxcd/source-controller/api v0.21.0
k8s.io/apimachinery v0.23.1
sigs.k8s.io/controller-runtime v0.11.0
)
require (
github.com/fluxcd/pkg/apis/acl v0.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/fluxcd/pkg/apis/acl v0.0.3 // indirect
github.com/go-logr/logr v1.2.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/spf13/pflag v1.0.7 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/text v0.28.0 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
golang.org/x/net v0.0.0-20211215060638-4ddde0e984e9 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/klog/v2 v2.30.0 // indirect
k8s.io/utils v0.0.0-20211208161948-7d6a63dca704 // indirect
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.0 // indirect
)

View File

@ -1,116 +1,933 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA=
github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo=
github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA=
github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fluxcd/pkg/apis/acl v0.7.0 h1:dMhZJH+g6ZRPjs4zVOAN9vHBd1DcavFgcIFkg5ooOE0=
github.com/fluxcd/pkg/apis/acl v0.7.0/go.mod h1:uv7pXXR/gydiX4MUwlQa7vS8JONEDztynnjTvY3JxKQ=
github.com/fluxcd/pkg/apis/meta v1.20.0 h1:l9h0kWoDZTcYV0WJkFMgDXq6Q4tSojrJ+bHpFJSsaW0=
github.com/fluxcd/pkg/apis/meta v1.20.0/go.mod h1:XUAEUgT4gkWDAEN79E141tmL+v4SV50tVZ/Ojpc/ueg=
github.com/fluxcd/source-controller/api v1.6.2 h1:UmodAeqLIeF29HdTqf2GiacZyO+hJydJlepDaYsMvhc=
github.com/fluxcd/source-controller/api v1.6.2/go.mod h1:ZJcAi0nemsnBxjVgmJl0WQzNvB0rMETxQMTdoFosmMw=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
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-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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fluxcd/pkg/apis/acl v0.0.3 h1:Lw0ZHdpnO4G7Zy9KjrzwwBmDZQuy4qEjaU/RvA6k1lc=
github.com/fluxcd/pkg/apis/acl v0.0.3/go.mod h1:XPts6lRJ9C9fIF9xVWofmQwftvhY25n1ps7W9xw0XLU=
github.com/fluxcd/pkg/apis/meta v0.10.2 h1:pnDBBEvfs4HaKiVAYgz+e/AQ8dLvcgmVfSeBroZ/KKI=
github.com/fluxcd/pkg/apis/meta v0.10.2/go.mod h1:KQ2er9xa6koy7uoPMZjIjNudB5p4tXs+w0GO6fRcy7I=
github.com/fluxcd/source-controller/api v0.21.0 h1:Ds1XGYd2iS59V6g642CiI2jM7c4ZQyB9aq9+A/UhxsQ=
github.com/fluxcd/source-controller/api v0.21.0/go.mod h1:Ab2qDmAUz6ZCp8UaHYLYzxyFrC1FQqEqjxiROb/Rdiw=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/zapr v1.2.0/go.mod h1:Qa4Bsj2Vb+FAVeAKsLD8RLQ+YRJB8YDmOAKxaBQf7Ro=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
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/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/cel-go v0.9.0/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w=
github.com/google/cel-spec v0.6.0/go.mod h1:Nwjgxy5CbjlPrtCWjeDjUyKMl8w41YBYGjsyDdqk0xA=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=
github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/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/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
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/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/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/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.28.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
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/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0=
go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE=
go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc=
go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4=
go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo=
go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM=
go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU=
go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw=
go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc=
go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE=
go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE=
go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211215060638-4ddde0e984e9 h1:kmreh1vGI63l2FxOAYS3Yv6ATsi7lSTuwNSVbGfJV9I=
golang.org/x/net v0.0.0-20211215060638-4ddde0e984e9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8 h1:M69LAlWZCshgp0QSzyDcSsSIejIEeuaCVpmwcKwyLMk=
golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM=
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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201102152239-715cce707fb0/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
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-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE=
k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug=
k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0=
k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0=
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/controller-runtime v0.22.0 h1:mTOfibb8Hxwpx3xEkR56i7xSjB+nH4hZG37SrlCY5e0=
sigs.k8s.io/controller-runtime v0.22.0/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
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/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
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.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
k8s.io/api v0.23.0 h1:WrL1gb73VSC8obi8cuYETJGXEoFNEh3LU0Pt+Sokgro=
k8s.io/api v0.23.0/go.mod h1:8wmDdLBHBNxtOIytwLstXt5E9PddnZb0GaMcqsvDBpg=
k8s.io/apiextensions-apiserver v0.23.0/go.mod h1:xIFAEEDlAZgpVBl/1VSjGDmLoXAWRG40+GsWhKhAxY4=
k8s.io/apimachinery v0.23.0/go.mod h1:fFCTTBKvKcwTPFzjlcxp91uPFZr+JA0FubU4fLzzFYc=
k8s.io/apimachinery v0.23.1 h1:sfBjlDFwj2onG0Ijx5C+SrAoeUscPrmghm7wHP+uXlo=
k8s.io/apimachinery v0.23.1/go.mod h1:SADt2Kl8/sttJ62RRsi9MIV4o8f5S3coArm0Iu3fBno=
k8s.io/apiserver v0.23.0/go.mod h1:Cec35u/9zAepDPPFyT+UMrgqOCjgJ5qtfVJDxjZYmt4=
k8s.io/client-go v0.23.0/go.mod h1:hrDnpnK1mSr65lHHcUuIZIXDgEbzc7/683c6hyG4jTA=
k8s.io/code-generator v0.23.0/go.mod h1:vQvOhDXhuzqiVfM/YHp+dmg10WDZCchJVObc9MvowsE=
k8s.io/component-base v0.23.0/go.mod h1:DHH5uiFvLC1edCpvcTDV++NKULdYYU6pR9Tt3HIKMKI=
k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
k8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw=
k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk=
k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
k8s.io/utils v0.0.0-20211208161948-7d6a63dca704 h1:ZKMMxTvduyf5WUtREOqg5LiXaN1KO/+0oOQPRFrClpo=
k8s.io/utils v0.0.0-20211208161948-7d6a63dca704/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.25/go.mod h1:Mlj9PNLmG9bZ6BHFwFKDo5afkpWyUISkb9Me0GnK66I=
sigs.k8s.io/controller-runtime v0.11.0 h1:DqO+c8mywcZLFJWILq4iktoECTyn30Bkj0CwgqMpZWQ=
sigs.k8s.io/controller-runtime v0.11.0/go.mod h1:KKwLiTooNGu+JmLZGn9Sl3Gjmfj66eMbCQznLP5zcqA=
sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs=
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 h1:kDi4JBNAsJWfz1aEXhO8Jg87JJaPNLh5tIzYHgStQ9Y=
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY=
sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4=
sigs.k8s.io/structured-merge-diff/v4 v4.2.0 h1:kDvPBbnPk+qYmkHmSo8vKGp438IASWofnbbUKDE/bv0=
sigs.k8s.io/structured-merge-diff/v4 v4.2.0/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

View File

@ -1,5 +1,5 @@
/*
Copyright 2024 The Flux authors
Copyright 2020 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// Package v1beta2 contains API types for the image API group, version
// v1beta2. The types here are concerned with automated updates to
// git, based on metadata from OCI image registries gathered by the
// Package v1alpha1 contains API types for the image v1alpha1 API
// group. The types here are concerned with automated updates to git,
// based on metadata from OCI image registries gathered by the
// image-reflector-controller.
//
// +kubebuilder:object:generate=true
// +groupName=image.toolkit.fluxcd.io
package v1beta2
package v1alpha1

View File

@ -1,5 +1,5 @@
/*
Copyright 2024 The Flux authors
Copyright 2020 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// Package v1beta2 contains API Schema definitions for the image v1beta2 API group
// Package v1alpha1 contains API Schema definitions for the image v1alpha1 API group
// +kubebuilder:object:generate=true
// +groupName=image.toolkit.fluxcd.io
package v1beta2
package v1alpha1
import (
"k8s.io/apimachinery/pkg/runtime/schema"
@ -26,7 +26,7 @@ import (
var (
// GroupVersion is group version used to register these objects
GroupVersion = schema.GroupVersion{Group: "image.toolkit.fluxcd.io", Version: "v1beta2"}
GroupVersion = schema.GroupVersion{Group: "image.toolkit.fluxcd.io", Version: "v1alpha1"}
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}

View File

@ -0,0 +1,203 @@
/*
Copyright 2020 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/fluxcd/pkg/apis/meta"
)
const ImageUpdateAutomationKind = "ImageUpdateAutomation"
// ImageUpdateAutomationSpec defines the desired state of ImageUpdateAutomation
type ImageUpdateAutomationSpec struct {
// Checkout gives the parameters for cloning the git repository,
// ready to make changes.
// +required
Checkout GitCheckoutSpec `json:"checkout"`
// Interval gives an lower bound for how often the automation
// run should be attempted.
// +required
Interval metav1.Duration `json:"interval"`
// Update gives the specification for how to update the files in
// the repository. This can be left empty, to use the default
// value.
// +kubebuilder:default={"strategy":"Setters"}
Update *UpdateStrategy `json:"update,omitempty"`
// Commit specifies how to commit to the git repository.
// +required
Commit CommitSpec `json:"commit"`
// Push specifies how and where to push commits made by the
// automation. If missing, commits are pushed (back) to
// `.spec.checkout.branch`.
// +optional
Push *PushSpec `json:"push,omitempty"`
// Suspend tells the controller to not run this automation, until
// it is unset (or set to false). Defaults to false.
// +optional
Suspend bool `json:"suspend,omitempty"`
}
type GitCheckoutSpec struct {
// GitRepositoryRef refers to the resource giving access details
// to a git repository to update files in.
// +required
GitRepositoryRef meta.LocalObjectReference `json:"gitRepositoryRef"`
// Branch gives the branch to clone from the git repository. If
// `.spec.push` is not supplied, commits will also be pushed to
// this branch.
// +required
Branch string `json:"branch"`
}
// UpdateStrategyName is the type for names that go in
// .update.strategy. NB the value in the const immediately below.
// +kubebuilder:validation:Enum=Setters
type UpdateStrategyName string
const (
// UpdateStrategySetters is the name of the update strategy that
// uses kyaml setters. NB the value in the enum annotation for the
// type, above.
UpdateStrategySetters UpdateStrategyName = "Setters"
)
// UpdateStrategy is a union of the various strategies for updating
// the Git repository. Parameters for each strategy (if any) can be
// inlined here.
type UpdateStrategy struct {
// Strategy names the strategy to be used.
// +required
// +kubebuilder:default=Setters
Strategy UpdateStrategyName `json:"strategy"`
// Path to the directory containing the manifests to be updated.
// Defaults to 'None', which translates to the root path
// of the GitRepositoryRef.
// +optional
Path string `json:"path,omitempty"`
}
// CommitSpec specifies how to commit changes to the git repository
type CommitSpec struct {
// AuthorName gives the name to provide when making a commit
// +required
AuthorName string `json:"authorName"`
// AuthorEmail gives the email to provide when making a commit
// +required
AuthorEmail string `json:"authorEmail"`
// SigningKey provides the option to sign commits with a GPG key
// +optional
SigningKey *SigningKey `json:"signingKey,omitempty"`
// MessageTemplate provides a template for the commit message,
// into which will be interpolated the details of the change made.
// +optional
MessageTemplate string `json:"messageTemplate,omitempty"`
}
// PushSpec specifies how and where to push commits.
type PushSpec struct {
// Branch specifies that commits should be pushed to the branch
// named. The branch is created using `.spec.checkout.branch` as the
// starting point, if it doesn't already exist.
// +required
Branch string `json:"branch"`
}
// ImageUpdateAutomationStatus defines the observed state of ImageUpdateAutomation
type ImageUpdateAutomationStatus struct {
// LastAutomationRunTime records the last time the controller ran
// this automation through to completion (even if no updates were
// made).
// +optional
LastAutomationRunTime *metav1.Time `json:"lastAutomationRunTime,omitempty"`
// LastPushCommit records the SHA1 of the last commit made by the
// controller, for this automation object
// +optional
LastPushCommit string `json:"lastPushCommit,omitempty"`
// LastPushTime records the time of the last pushed change.
// +optional
LastPushTime *metav1.Time `json:"lastPushTime,omitempty"`
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
meta.ReconcileRequestStatus `json:",inline"`
}
// SigningKey references a Kubernetes secret that contains a GPG keypair
type SigningKey struct {
// SecretRef holds the name to a secret that contains a 'git.asc' key
// corresponding to the ASCII Armored file containing the GPG signing
// keypair as the value. It must be in the same namespace as the
// ImageUpdateAutomation.
// +required
SecretRef meta.LocalObjectReference `json:"secretRef,omitempty"`
}
const (
// GitNotAvailableReason is used for ConditionReady when the
// automation run cannot proceed because the git repository is
// missing or cannot be cloned.
GitNotAvailableReason = "GitRepositoryNotAvailable"
// NoStrategyReason is used for ConditionReady when the automation
// run cannot proceed because there is no update strategy given in
// the spec.
NoStrategyReason = "MissingUpdateStrategy"
)
// SetImageUpdateAutomationReadiness sets the ready condition with the given status, reason and message.
func SetImageUpdateAutomationReadiness(auto *ImageUpdateAutomation, status metav1.ConditionStatus, reason, message string) {
auto.Status.ObservedGeneration = auto.ObjectMeta.Generation
meta.SetResourceCondition(auto, meta.ReadyCondition, status, reason, message)
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Last run",type=string,JSONPath=`.status.lastAutomationRunTime`
// ImageUpdateAutomation is the Schema for the imageupdateautomations API
type ImageUpdateAutomation struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ImageUpdateAutomationSpec `json:"spec,omitempty"`
Status ImageUpdateAutomationStatus `json:"status,omitempty"`
}
func (auto *ImageUpdateAutomation) GetStatusConditions() *[]metav1.Condition {
return &auto.Status.Conditions
}
// +kubebuilder:object:root=true
// ImageUpdateAutomationList contains a list of ImageUpdateAutomation
type ImageUpdateAutomationList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []ImageUpdateAutomation `json:"items"`
}
func init() {
SchemeBuilder.Register(&ImageUpdateAutomation{}, &ImageUpdateAutomationList{})
}

View File

@ -0,0 +1,227 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright 2020 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by controller-gen. DO NOT EDIT.
package v1alpha1
import (
"k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CommitSpec) DeepCopyInto(out *CommitSpec) {
*out = *in
if in.SigningKey != nil {
in, out := &in.SigningKey, &out.SigningKey
*out = new(SigningKey)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommitSpec.
func (in *CommitSpec) DeepCopy() *CommitSpec {
if in == nil {
return nil
}
out := new(CommitSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GitCheckoutSpec) DeepCopyInto(out *GitCheckoutSpec) {
*out = *in
out.GitRepositoryRef = in.GitRepositoryRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitCheckoutSpec.
func (in *GitCheckoutSpec) DeepCopy() *GitCheckoutSpec {
if in == nil {
return nil
}
out := new(GitCheckoutSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ImageUpdateAutomation) DeepCopyInto(out *ImageUpdateAutomation) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageUpdateAutomation.
func (in *ImageUpdateAutomation) DeepCopy() *ImageUpdateAutomation {
if in == nil {
return nil
}
out := new(ImageUpdateAutomation)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ImageUpdateAutomation) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ImageUpdateAutomationList) DeepCopyInto(out *ImageUpdateAutomationList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]ImageUpdateAutomation, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageUpdateAutomationList.
func (in *ImageUpdateAutomationList) DeepCopy() *ImageUpdateAutomationList {
if in == nil {
return nil
}
out := new(ImageUpdateAutomationList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ImageUpdateAutomationList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ImageUpdateAutomationSpec) DeepCopyInto(out *ImageUpdateAutomationSpec) {
*out = *in
out.Checkout = in.Checkout
out.Interval = in.Interval
if in.Update != nil {
in, out := &in.Update, &out.Update
*out = new(UpdateStrategy)
**out = **in
}
in.Commit.DeepCopyInto(&out.Commit)
if in.Push != nil {
in, out := &in.Push, &out.Push
*out = new(PushSpec)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageUpdateAutomationSpec.
func (in *ImageUpdateAutomationSpec) DeepCopy() *ImageUpdateAutomationSpec {
if in == nil {
return nil
}
out := new(ImageUpdateAutomationSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ImageUpdateAutomationStatus) DeepCopyInto(out *ImageUpdateAutomationStatus) {
*out = *in
if in.LastAutomationRunTime != nil {
in, out := &in.LastAutomationRunTime, &out.LastAutomationRunTime
*out = (*in).DeepCopy()
}
if in.LastPushTime != nil {
in, out := &in.LastPushTime, &out.LastPushTime
*out = (*in).DeepCopy()
}
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
out.ReconcileRequestStatus = in.ReconcileRequestStatus
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageUpdateAutomationStatus.
func (in *ImageUpdateAutomationStatus) DeepCopy() *ImageUpdateAutomationStatus {
if in == nil {
return nil
}
out := new(ImageUpdateAutomationStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSpec) DeepCopyInto(out *PushSpec) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSpec.
func (in *PushSpec) DeepCopy() *PushSpec {
if in == nil {
return nil
}
out := new(PushSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SigningKey) DeepCopyInto(out *SigningKey) {
*out = *in
out.SecretRef = in.SecretRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SigningKey.
func (in *SigningKey) DeepCopy() *SigningKey {
if in == nil {
return nil
}
out := new(SigningKey)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UpdateStrategy) DeepCopyInto(out *UpdateStrategy) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpdateStrategy.
func (in *UpdateStrategy) DeepCopy() *UpdateStrategy {
if in == nil {
return nil
}
out := new(UpdateStrategy)
in.DeepCopyInto(out)
return out
}

View File

@ -1,5 +1,5 @@
/*
Copyright 2025 The Flux authors
Copyright 2020 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package constants
const (
// SetterShortHand is a shorthand that can be used to mark
// setters; instead of
// # { "$ref": "#/definitions/
SetterShortHand = "$imagepolicy"
)
// Package v1alpha2 contains API types for the image v1alpha2 API
// group. The types here are concerned with automated updates to git,
// based on metadata from OCI image registries gathered by the
// image-reflector-controller. There is some rearrangement from
// v1alpha1 to make room for future enhancements.
//
// +kubebuilder:object:generate=true
// +groupName=image.toolkit.fluxcd.io
package v1alpha2

View File

@ -1,5 +1,5 @@
/*
Copyright 2024 The Flux authors
Copyright 2020 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package v1beta2
package v1alpha2
import (
"github.com/fluxcd/pkg/apis/meta"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
)
type GitSpec struct {
@ -39,14 +39,6 @@ type GitSpec struct {
Push *PushSpec `json:"push,omitempty"`
}
// HasRefspec returns if the GitSpec has a Refspec.
func (gs GitSpec) HasRefspec() bool {
if gs.Push == nil {
return false
}
return gs.Push.Refspec != ""
}
type GitCheckoutSpec struct {
// Reference gives a branch, tag or commit to clone from the Git
// repository.
@ -65,14 +57,8 @@ type CommitSpec struct {
SigningKey *SigningKey `json:"signingKey,omitempty"`
// MessageTemplate provides a template for the commit message,
// into which will be interpolated the details of the change made.
// Note: The `Updated` template field has been removed. Use `Changed` instead.
// +optional
MessageTemplate string `json:"messageTemplate,omitempty"`
// MessageTemplateValues provides additional values to be available to the
// templating rendering.
// +optional
MessageTemplateValues map[string]string `json:"messageTemplateValues,omitempty"`
}
type CommitUser struct {
@ -99,20 +85,6 @@ type PushSpec struct {
// Branch specifies that commits should be pushed to the branch
// named. The branch is created using `.spec.checkout.branch` as the
// starting point, if it doesn't already exist.
// +optional
Branch string `json:"branch,omitempty"`
// Refspec specifies the Git Refspec to use for a push operation.
// If both Branch and Refspec are provided, then the commit is pushed
// to the branch and also using the specified refspec.
// For more details about Git Refspecs, see:
// https://git-scm.com/book/en/v2/Git-Internals-The-Refspec
// +optional
Refspec string `json:"refspec,omitempty"`
// Options specifies the push options that are sent to the Git
// server when performing a push operation. For details, see:
// https://git-scm.com/docs/git-push#Documentation/git-push.txt---push-optionltoptiongt
// +optional
Options map[string]string `json:"options,omitempty"`
// +required
Branch string `json:"branch"`
}

View File

@ -0,0 +1,33 @@
/*
Copyright 2020 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha2
import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)
var (
// GroupVersion is group version used to register these objects
GroupVersion = schema.GroupVersion{Group: "image.toolkit.fluxcd.io", Version: "v1alpha2"}
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)

View File

@ -1,5 +1,5 @@
/*
Copyright 2024 The Flux authors
Copyright 2020 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -14,28 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package v1beta2
package v1alpha2
import (
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/fluxcd/pkg/apis/meta"
)
const (
ImageUpdateAutomationKind = "ImageUpdateAutomation"
ImageUpdateAutomationFinalizer = "finalizers.fluxcd.io"
)
const ImageUpdateAutomationKind = "ImageUpdateAutomation"
// ImageUpdateAutomationSpec defines the desired state of ImageUpdateAutomation
type ImageUpdateAutomationSpec struct {
// SourceRef refers to the resource giving access details
// to a git repository.
// +required
SourceRef CrossNamespaceSourceReference `json:"sourceRef"`
SourceRef SourceReference `json:"sourceRef"`
// GitSpec contains all the git-specific definitions. This is
// technically optional, but in practice mandatory until there are
// other kinds of source allowed.
@ -44,16 +38,9 @@ type ImageUpdateAutomationSpec struct {
// Interval gives an lower bound for how often the automation
// run should be attempted.
// +kubebuilder:validation:Type=string
// +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$"
// +required
Interval metav1.Duration `json:"interval"`
// PolicySelector allows to filter applied policies based on labels.
// By default includes all policies in namespace.
// +optional
PolicySelector *metav1.LabelSelector `json:"policySelector,omitempty"`
// Update gives the specification for how to update the files in
// the repository. This can be left empty, to use the default
// value.
@ -83,9 +70,9 @@ const (
// inlined here.
type UpdateStrategy struct {
// Strategy names the strategy to be used.
// +optional
// +required
// +kubebuilder:default=Setters
Strategy UpdateStrategyName `json:"strategy,omitempty"`
Strategy UpdateStrategyName `json:"strategy"`
// Path to the directory containing the manifests to be updated.
// Defaults to 'None', which translates to the root path
@ -111,57 +98,42 @@ type ImageUpdateAutomationStatus struct {
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
// ObservedPolicies is the list of observed ImagePolicies that were
// considered by the ImageUpdateAutomation update process.
// +optional
ObservedPolicies ObservedPolicies `json:"observedPolicies,omitempty"`
// ObservedPolicies []ObservedPolicy `json:"observedPolicies,omitempty"`
// ObservedSourceRevision is the last observed source revision. This can be
// used to determine if the source has been updated since last observation.
// +optional
ObservedSourceRevision string `json:"observedSourceRevision,omitempty"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
meta.ReconcileRequestStatus `json:",inline"`
}
// ObservedPolicies is a map of policy name and ImageRef of their latest
// ImageRef.
type ObservedPolicies map[string]ImageRef
const (
// GitNotAvailableReason is used for ConditionReady when the
// automation run cannot proceed because the git repository is
// missing or cannot be cloned.
GitNotAvailableReason = "GitRepositoryNotAvailable"
// NoStrategyReason is used for ConditionReady when the automation
// run cannot proceed because there is no update strategy given in
// the spec.
NoStrategyReason = "MissingUpdateStrategy"
)
// SetImageUpdateAutomationReadiness sets the ready condition with the given status, reason and message.
func SetImageUpdateAutomationReadiness(auto *ImageUpdateAutomation, status metav1.ConditionStatus, reason, message string) {
auto.Status.ObservedGeneration = auto.ObjectMeta.Generation
meta.SetResourceCondition(auto, meta.ReadyCondition, status, reason, message)
}
//+kubebuilder:storageversion
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:resource:shortName=iua;imgupd;imgauto
//+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description=""
//+kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description=""
//+kubebuilder:printcolumn:name="Last run",type="string",JSONPath=".status.lastAutomationRunTime",priority=1
//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description=""
//+kubebuilder:printcolumn:name="Last run",type=string,JSONPath=`.status.lastAutomationRunTime`
// ImageUpdateAutomation is the Schema for the imageupdateautomations API
type ImageUpdateAutomation struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ImageUpdateAutomationSpec `json:"spec,omitempty"`
// +kubebuilder:default={"observedGeneration":-1}
Spec ImageUpdateAutomationSpec `json:"spec,omitempty"`
Status ImageUpdateAutomationStatus `json:"status,omitempty"`
}
// GetRequeueAfter returns the duration after which the ImageUpdateAutomation
// must be reconciled again.
func (auto ImageUpdateAutomation) GetRequeueAfter() time.Duration {
return auto.Spec.Interval.Duration
}
// GetConditions returns the status conditions of the object.
func (auto ImageUpdateAutomation) GetConditions() []metav1.Condition {
return auto.Status.Conditions
}
// SetConditions sets the status conditions on the object.
func (auto *ImageUpdateAutomation) SetConditions(conditions []metav1.Condition) {
auto.Status.Conditions = conditions
func (auto *ImageUpdateAutomation) GetStatusConditions() *[]metav1.Condition {
return &auto.Status.Conditions
}
//+kubebuilder:object:root=true

View File

@ -1,5 +1,5 @@
/*
Copyright 2022 The Flux authors
Copyright 2020 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -14,14 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package v1beta1
package v1alpha2
const (
// ReconciliationSucceededReason represents the fact that
// the reconciliation succeeded.
ReconciliationSucceededReason string = "ReconciliationSucceeded"
// SourceReference contains enough information to let you locate the
// typed, referenced source object.
type SourceReference struct {
// API version of the referent
// +optional
APIVersion string `json:"apiVersion,omitempty"`
// ReconciliationFailedReason represents the fact that
// the reconciliation failed.
ReconciliationFailedReason string = "ReconciliationFailed"
)
// Kind of the referent
// +kubebuilder:validation:Enum=GitRepository
// +kubebuilder:default=GitRepository
// +required
Kind string `json:"kind"`
// Name of the referent
// +required
Name string `json:"name"`
}

View File

@ -1,7 +1,8 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright 2025 The Flux authors
Copyright 2020 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -18,7 +19,7 @@ limitations under the License.
// Code generated by controller-gen. DO NOT EDIT.
package v1beta2
package v1alpha2
import (
"k8s.io/apimachinery/pkg/apis/meta/v1"
@ -34,13 +35,6 @@ func (in *CommitSpec) DeepCopyInto(out *CommitSpec) {
*out = new(SigningKey)
**out = **in
}
if in.MessageTemplateValues != nil {
in, out := &in.MessageTemplateValues, &out.MessageTemplateValues
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommitSpec.
@ -68,21 +62,6 @@ func (in *CommitUser) DeepCopy() *CommitUser {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CrossNamespaceSourceReference) DeepCopyInto(out *CrossNamespaceSourceReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CrossNamespaceSourceReference.
func (in *CrossNamespaceSourceReference) DeepCopy() *CrossNamespaceSourceReference {
if in == nil {
return nil
}
out := new(CrossNamespaceSourceReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GitCheckoutSpec) DeepCopyInto(out *GitCheckoutSpec) {
*out = *in
@ -111,7 +90,7 @@ func (in *GitSpec) DeepCopyInto(out *GitSpec) {
if in.Push != nil {
in, out := &in.Push, &out.Push
*out = new(PushSpec)
(*in).DeepCopyInto(*out)
**out = **in
}
}
@ -125,21 +104,6 @@ func (in *GitSpec) DeepCopy() *GitSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ImageRef) DeepCopyInto(out *ImageRef) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageRef.
func (in *ImageRef) DeepCopy() *ImageRef {
if in == nil {
return nil
}
out := new(ImageRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ImageUpdateAutomation) DeepCopyInto(out *ImageUpdateAutomation) {
*out = *in
@ -209,11 +173,6 @@ func (in *ImageUpdateAutomationSpec) DeepCopyInto(out *ImageUpdateAutomationSpec
(*in).DeepCopyInto(*out)
}
out.Interval = in.Interval
if in.PolicySelector != nil {
in, out := &in.PolicySelector, &out.PolicySelector
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
if in.Update != nil {
in, out := &in.Update, &out.Update
*out = new(UpdateStrategy)
@ -249,13 +208,6 @@ func (in *ImageUpdateAutomationStatus) DeepCopyInto(out *ImageUpdateAutomationSt
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.ObservedPolicies != nil {
in, out := &in.ObservedPolicies, &out.ObservedPolicies
*out = make(ObservedPolicies, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
out.ReconcileRequestStatus = in.ReconcileRequestStatus
}
@ -269,37 +221,9 @@ func (in *ImageUpdateAutomationStatus) DeepCopy() *ImageUpdateAutomationStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in ObservedPolicies) DeepCopyInto(out *ObservedPolicies) {
{
in := &in
*out = make(ObservedPolicies, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObservedPolicies.
func (in ObservedPolicies) DeepCopy() ObservedPolicies {
if in == nil {
return nil
}
out := new(ObservedPolicies)
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSpec) DeepCopyInto(out *PushSpec) {
*out = *in
if in.Options != nil {
in, out := &in.Options, &out.Options
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSpec.
@ -328,6 +252,21 @@ func (in *SigningKey) DeepCopy() *SigningKey {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SourceReference) DeepCopyInto(out *SourceReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceReference.
func (in *SourceReference) DeepCopy() *SourceReference {
if in == nil {
return nil
}
out := new(SourceReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UpdateStrategy) DeepCopyInto(out *UpdateStrategy) {
*out = *in

View File

@ -14,9 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// Package v1beta1 contains API types for the image v1beta1 API group
//
// Deprecated: v1beta1 is no longer supported, use v1 instead.
// Package v1beta1 contains API types for the image API group, version
// v1beta1. The types here are concerned with automated updates to
// git, based on metadata from OCI image registries gathered by the
// image-reflector-controller. v1alpha2 did some rearrangement from
// v1alpha1 to make room for future enhancements; v1beta1 does not
// change the schema from v1alpha2.
//
// +kubebuilder:object:generate=true
// +groupName=image.toolkit.fluxcd.io

View File

@ -18,7 +18,7 @@ package v1beta1
import (
"github.com/fluxcd/pkg/apis/meta"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
)
type GitSpec struct {
@ -85,20 +85,6 @@ type PushSpec struct {
// Branch specifies that commits should be pushed to the branch
// named. The branch is created using `.spec.checkout.branch` as the
// starting point, if it doesn't already exist.
// +optional
Branch string `json:"branch,omitempty"`
// Refspec specifies the Git Refspec to use for a push operation.
// If both Branch and Refspec are provided, then the commit is pushed
// to the branch and also using the specified refspec.
// For more details about Git Refspecs, see:
// https://git-scm.com/book/en/v2/Git-Internals-The-Refspec
// +optional
Refspec string `json:"refspec,omitempty"`
// Options specifies the push options that are sent to the Git
// server when performing a push operation. For details, see:
// https://git-scm.com/docs/git-push#Documentation/git-push.txt---push-optionltoptiongt
// +optional
Options map[string]string `json:"options,omitempty"`
// +required
Branch string `json:"branch"`
}

View File

@ -17,18 +17,12 @@ limitations under the License.
package v1beta1
import (
"time"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/fluxcd/pkg/apis/meta"
)
const (
ImageUpdateAutomationKind = "ImageUpdateAutomation"
ImageUpdateAutomationFinalizer = "finalizers.fluxcd.io"
)
const ImageUpdateAutomationKind = "ImageUpdateAutomation"
// ImageUpdateAutomationSpec defines the desired state of ImageUpdateAutomation
type ImageUpdateAutomationSpec struct {
@ -44,8 +38,6 @@ type ImageUpdateAutomationSpec struct {
// Interval gives an lower bound for how often the automation
// run should be attempted.
// +kubebuilder:validation:Type=string
// +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$"
// +required
Interval metav1.Duration `json:"interval"`
@ -124,17 +116,13 @@ const (
// SetImageUpdateAutomationReadiness sets the ready condition with the given status, reason and message.
func SetImageUpdateAutomationReadiness(auto *ImageUpdateAutomation, status metav1.ConditionStatus, reason, message string) {
auto.Status.ObservedGeneration = auto.ObjectMeta.Generation
newCondition := metav1.Condition{
Type: meta.ReadyCondition,
Status: status,
Reason: reason,
Message: message,
}
apimeta.SetStatusCondition(auto.GetStatusConditions(), newCondition)
meta.SetResourceCondition(auto, meta.ReadyCondition, status, reason, message)
}
// +kubebuilder:object:root=true
// +kubebuilder:skipversion
//+kubebuilder:storageversion
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:printcolumn:name="Last run",type=string,JSONPath=`.status.lastAutomationRunTime`
// ImageUpdateAutomation is the Schema for the imageupdateautomations API
type ImageUpdateAutomation struct {
@ -146,24 +134,6 @@ type ImageUpdateAutomation struct {
Status ImageUpdateAutomationStatus `json:"status,omitempty"`
}
// GetRequeueAfter returns the duration after which the ImageUpdateAutomation
// must be reconciled again.
func (auto ImageUpdateAutomation) GetRequeueAfter() time.Duration {
return auto.Spec.Interval.Duration
}
// GetConditions returns the status conditions of the object.
func (auto ImageUpdateAutomation) GetConditions() []metav1.Condition {
return auto.Status.Conditions
}
// SetConditions sets the status conditions on the object.
func (auto *ImageUpdateAutomation) SetConditions(conditions []metav1.Condition) {
auto.Status.Conditions = conditions
}
// GetStatusConditions returns a pointer to the Status.Conditions slice.
// Deprecated: use GetConditions instead.
func (auto *ImageUpdateAutomation) GetStatusConditions() *[]metav1.Condition {
return &auto.Status.Conditions
}

View File

@ -1,7 +1,8 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright 2025 The Flux authors
Copyright 2020 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -104,7 +105,7 @@ func (in *GitSpec) DeepCopyInto(out *GitSpec) {
if in.Push != nil {
in, out := &in.Push, &out.Push
*out = new(PushSpec)
(*in).DeepCopyInto(*out)
**out = **in
}
}
@ -238,13 +239,6 @@ func (in *ImageUpdateAutomationStatus) DeepCopy() *ImageUpdateAutomationStatus {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSpec) DeepCopyInto(out *PushSpec) {
*out = *in
if in.Options != nil {
in, out := &in.Options, &out.Options
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSpec.

View File

@ -1,42 +0,0 @@
/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1beta2
const (
// InvalidUpdateStrategyReason represents an invalid image update strategy
// configuration.
InvalidUpdateStrategyReason string = "InvalidUpdateStrategy"
// InvalidSourceConfigReason represents an invalid source configuration.
InvalidSourceConfigReason string = "InvalidSourceConfiguration"
// SourceManagerFailedReason represents a failure in the SourceManager which
// manages the source.
SourceManagerFailedReason string = "SourceManagerFailed"
// GitOperationFailedReason represents a failure in Git source operation.
GitOperationFailedReason string = "GitOperationFailed"
// UpdateFailedReason represents a failure during source update.
UpdateFailedReason string = "UpdateFailed"
// InvalidPolicySelectorReason represents an invalid policy selector.
InvalidPolicySelectorReason string = "InvalidPolicySelector"
// RemovedTemplateFieldReason represents usage of removed template field.
RemovedTemplateFieldReason string = "RemovedTemplateField"
)

View File

@ -1,71 +0,0 @@
/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1beta2
import (
"fmt"
)
// CrossNamespaceSourceReference contains enough information to let you locate the
// typed Kubernetes resource object at cluster level.
type CrossNamespaceSourceReference struct {
// API version of the referent.
// +optional
APIVersion string `json:"apiVersion,omitempty"`
// Kind of the referent.
// +kubebuilder:validation:Enum=GitRepository
// +kubebuilder:default=GitRepository
// +required
Kind string `json:"kind"`
// Name of the referent.
// +required
Name string `json:"name"`
// Namespace of the referent, defaults to the namespace of the Kubernetes resource object that contains the reference.
// +optional
Namespace string `json:"namespace,omitempty"`
}
func (s *CrossNamespaceSourceReference) String() string {
if s.Namespace != "" {
return fmt.Sprintf("%s/%s/%s", s.Kind, s.Namespace, s.Name)
}
return fmt.Sprintf("%s/%s", s.Kind, s.Name)
}
// ImageRef represents an image reference.
type ImageRef struct {
// Name is the bare image's name.
// +required
Name string `json:"name"`
// Tag is the image's tag.
// +required
Tag string `json:"tag"`
// Digest is the image's digest.
// +optional
Digest string `json:"digest,omitempty"`
}
func (in *ImageRef) String() string {
res := in.Name + ":" + in.Tag
if in.Digest != "" {
res += "@" + in.Digest
}
return res
}

View File

@ -1,9 +1,11 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.7.0
creationTimestamp: null
name: imageupdateautomations.image.toolkit.fluxcd.io
spec:
group: image.toolkit.fluxcd.io
@ -11,47 +13,259 @@ spec:
kind: ImageUpdateAutomation
listKind: ImageUpdateAutomationList
plural: imageupdateautomations
shortNames:
- iua
- imgupd
- imgauto
singular: imageupdateautomation
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready
type: string
- jsonPath: .status.conditions[?(@.type=="Ready")].message
name: Status
type: string
- jsonPath: .status.lastAutomationRunTime
name: Last run
priority: 1
type: string
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1beta2
name: v1alpha1
schema:
openAPIV3Schema:
description: ImageUpdateAutomation is the Schema for the imageupdateautomations
API
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: ImageUpdateAutomationSpec defines the desired state of ImageUpdateAutomation
properties:
checkout:
description: Checkout gives the parameters for cloning the git repository,
ready to make changes.
properties:
branch:
description: Branch gives the branch to clone from the git repository.
If `.spec.push` is not supplied, commits will also be pushed
to this branch.
type: string
gitRepositoryRef:
description: GitRepositoryRef refers to the resource giving access
details to a git repository to update files in.
properties:
name:
description: Name of the referent
type: string
required:
- name
type: object
required:
- branch
- gitRepositoryRef
type: object
commit:
description: Commit specifies how to commit to the git repository.
properties:
authorEmail:
description: AuthorEmail gives the email to provide when making
a commit
type: string
authorName:
description: AuthorName gives the name to provide when making
a commit
type: string
messageTemplate:
description: MessageTemplate provides a template for the commit
message, into which will be interpolated the details of the
change made.
type: string
signingKey:
description: SigningKey provides the option to sign commits with
a GPG key
properties:
secretRef:
description: SecretRef holds the name to a secret that contains
a 'git.asc' key corresponding to the ASCII Armored file
containing the GPG signing keypair as the value. It must
be in the same namespace as the ImageUpdateAutomation.
properties:
name:
description: Name of the referent
type: string
required:
- name
type: object
type: object
required:
- authorEmail
- authorName
type: object
interval:
description: Interval gives an lower bound for how often the automation
run should be attempted.
type: string
push:
description: Push specifies how and where to push commits made by
the automation. If missing, commits are pushed (back) to `.spec.checkout.branch`.
properties:
branch:
description: Branch specifies that commits should be pushed to
the branch named. The branch is created using `.spec.checkout.branch`
as the starting point, if it doesn't already exist.
type: string
required:
- branch
type: object
suspend:
description: Suspend tells the controller to not run this automation,
until it is unset (or set to false). Defaults to false.
type: boolean
update:
default:
strategy: Setters
description: Update gives the specification for how to update the
files in the repository. This can be left empty, to use the default
value.
properties:
path:
description: Path to the directory containing the manifests to
be updated. Defaults to 'None', which translates to the root
path of the GitRepositoryRef.
type: string
strategy:
default: Setters
description: Strategy names the strategy to be used.
enum:
- Setters
type: string
required:
- strategy
type: object
required:
- checkout
- commit
- interval
type: object
status:
description: ImageUpdateAutomationStatus defines the observed state of
ImageUpdateAutomation
properties:
conditions:
items:
description: "Condition contains details for one aspect of the current
state of this API Resource. --- This struct is intended for direct
use as an array at the field path .status.conditions. For example,
type FooStatus struct{ // Represents the observations of a
foo's current state. // Known .status.conditions.type are:
\"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type
\ // +patchStrategy=merge // +listType=map // +listMapKey=type
\ Conditions []metav1.Condition `json:\"conditions,omitempty\"
patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`
\n // other fields }"
properties:
lastTransitionTime:
description: lastTransitionTime is the last time the condition
transitioned from one status to another. This should be when
the underlying condition changed. If that is not known, then
using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: message is a human readable message indicating
details about the transition. This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: observedGeneration represents the .metadata.generation
that the condition was set based upon. For instance, if .metadata.generation
is currently 12, but the .status.conditions[x].observedGeneration
is 9, the condition is out of date with respect to the current
state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: reason contains a programmatic identifier indicating
the reason for the condition's last transition. Producers
of specific condition types may define expected values and
meanings for this field, and whether the values are considered
a guaranteed API. The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
--- Many .condition.type values are consistent across resources
like Available, but because arbitrary conditions can be useful
(see .node.status.conditions), the ability to deconflict is
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
lastAutomationRunTime:
description: LastAutomationRunTime records the last time the controller
ran this automation through to completion (even if no updates were
made).
format: date-time
type: string
lastHandledReconcileAt:
description: LastHandledReconcileAt holds the value of the most recent
reconcile request value, so a change can be detected.
type: string
lastPushCommit:
description: LastPushCommit records the SHA1 of the last commit made
by the controller, for this automation object
type: string
lastPushTime:
description: LastPushTime records the time of the last pushed change.
format: date-time
type: string
observedGeneration:
format: int64
type: integer
type: object
type: object
served: true
storage: false
subresources:
status: {}
- additionalPrinterColumns:
- jsonPath: .status.lastAutomationRunTime
name: Last run
type: string
name: v1alpha2
schema:
openAPIV3Schema:
description: ImageUpdateAutomation is the Schema for the imageupdateautomations
API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
@ -59,46 +273,34 @@ spec:
description: ImageUpdateAutomationSpec defines the desired state of ImageUpdateAutomation
properties:
git:
description: |-
GitSpec contains all the git-specific definitions. This is
technically optional, but in practice mandatory until there are
description: GitSpec contains all the git-specific definitions. This
is technically optional, but in practice mandatory until there are
other kinds of source allowed.
properties:
checkout:
description: |-
Checkout gives the parameters for cloning the git repository,
ready to make changes. If not present, the `spec.ref` field from the
referenced `GitRepository` or its default will be used.
description: Checkout gives the parameters for cloning the git
repository, ready to make changes. If not present, the `spec.ref`
field from the referenced `GitRepository` or its default will
be used.
properties:
ref:
description: |-
Reference gives a branch, tag or commit to clone from the Git
repository.
description: Reference gives a branch, tag or commit to clone
from the Git repository.
properties:
branch:
description: Branch to check out, defaults to 'master'
if no other field is defined.
description: The Git branch to checkout, defaults to master.
type: string
commit:
description: |-
Commit SHA to check out, takes precedence over all reference fields.
This can be combined with Branch to shallow clone the branch, in which
the commit is expected to exist.
type: string
name:
description: |-
Name of the reference to check out; takes precedence over Branch, Tag and SemVer.
It must be a valid Git reference: https://git-scm.com/docs/git-check-ref-format#_description
Examples: "refs/heads/main", "refs/tags/v0.1.0", "refs/pull/420/head", "refs/merge-requests/1/head"
description: The Git commit SHA to checkout, if specified
Tag filters will be ignored.
type: string
semver:
description: SemVer tag expression to check out, takes
precedence over Tag.
description: The Git tag semver expression, takes precedence
over Tag.
type: string
tag:
description: Tag to check out, takes precedence over Branch.
description: The Git tag to checkout, takes precedence
over Branch.
type: string
type: object
required:
@ -108,9 +310,8 @@ spec:
description: Commit specifies how to commit to the git repository.
properties:
author:
description: |-
Author gives the email and optionally the name to use as the
author of commits.
description: Author gives the email and optionally the name
to use as the author of commits.
properties:
email:
description: Email gives the email to provide when making
@ -124,130 +325,323 @@ spec:
- email
type: object
messageTemplate:
description: |-
MessageTemplate provides a template for the commit message,
into which will be interpolated the details of the change made.
Note: The `Updated` template field has been removed. Use `Changed` instead.
description: MessageTemplate provides a template for the commit
message, into which will be interpolated the details of
the change made.
type: string
messageTemplateValues:
additionalProperties:
type: string
description: |-
MessageTemplateValues provides additional values to be available to the
templating rendering.
type: object
signingKey:
description: SigningKey provides the option to sign commits
with a GPG key
properties:
secretRef:
description: |-
SecretRef holds the name to a secret that contains a 'git.asc' key
corresponding to the ASCII Armored file containing the GPG signing
keypair as the value. It must be in the same namespace as the
ImageUpdateAutomation.
description: SecretRef holds the name to a secret that
contains a 'git.asc' key corresponding to the ASCII
Armored file containing the GPG signing keypair as the
value. It must be in the same namespace as the ImageUpdateAutomation.
properties:
name:
description: Name of the referent.
description: Name of the referent
type: string
required:
- name
type: object
required:
- secretRef
type: object
required:
- author
type: object
push:
description: |-
Push specifies how and where to push commits made by the
automation. If missing, commits are pushed (back) to
description: Push specifies how and where to push commits made
by the automation. If missing, commits are pushed (back) to
`.spec.checkout.branch` or its default.
properties:
branch:
description: |-
Branch specifies that commits should be pushed to the branch
named. The branch is created using `.spec.checkout.branch` as the
starting point, if it doesn't already exist.
type: string
options:
additionalProperties:
type: string
description: |-
Options specifies the push options that are sent to the Git
server when performing a push operation. For details, see:
https://git-scm.com/docs/git-push#Documentation/git-push.txt---push-optionltoptiongt
type: object
refspec:
description: |-
Refspec specifies the Git Refspec to use for a push operation.
If both Branch and Refspec are provided, then the commit is pushed
to the branch and also using the specified refspec.
For more details about Git Refspecs, see:
https://git-scm.com/book/en/v2/Git-Internals-The-Refspec
description: Branch specifies that commits should be pushed
to the branch named. The branch is created using `.spec.checkout.branch`
as the starting point, if it doesn't already exist.
type: string
required:
- branch
type: object
required:
- commit
type: object
interval:
description: |-
Interval gives an lower bound for how often the automation
description: Interval gives an lower bound for how often the automation
run should be attempted.
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$
type: string
policySelector:
description: |-
PolicySelector allows to filter applied policies based on labels.
By default includes all policies in namespace.
properties:
matchExpressions:
description: matchExpressions is a list of label selector requirements.
The requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that the selector applies
to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
sourceRef:
description: |-
SourceRef refers to the resource giving access details
description: SourceRef refers to the resource giving access details
to a git repository.
properties:
apiVersion:
description: API version of the referent
type: string
kind:
default: GitRepository
description: Kind of the referent
enum:
- GitRepository
type: string
name:
description: Name of the referent
type: string
required:
- kind
- name
type: object
suspend:
description: Suspend tells the controller to not run this automation,
until it is unset (or set to false). Defaults to false.
type: boolean
update:
default:
strategy: Setters
description: Update gives the specification for how to update the
files in the repository. This can be left empty, to use the default
value.
properties:
path:
description: Path to the directory containing the manifests to
be updated. Defaults to 'None', which translates to the root
path of the GitRepositoryRef.
type: string
strategy:
default: Setters
description: Strategy names the strategy to be used.
enum:
- Setters
type: string
required:
- strategy
type: object
required:
- interval
- sourceRef
type: object
status:
description: ImageUpdateAutomationStatus defines the observed state of
ImageUpdateAutomation
properties:
conditions:
items:
description: "Condition contains details for one aspect of the current
state of this API Resource. --- This struct is intended for direct
use as an array at the field path .status.conditions. For example,
type FooStatus struct{ // Represents the observations of a
foo's current state. // Known .status.conditions.type are:
\"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type
\ // +patchStrategy=merge // +listType=map // +listMapKey=type
\ Conditions []metav1.Condition `json:\"conditions,omitempty\"
patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`
\n // other fields }"
properties:
lastTransitionTime:
description: lastTransitionTime is the last time the condition
transitioned from one status to another. This should be when
the underlying condition changed. If that is not known, then
using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: message is a human readable message indicating
details about the transition. This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: observedGeneration represents the .metadata.generation
that the condition was set based upon. For instance, if .metadata.generation
is currently 12, but the .status.conditions[x].observedGeneration
is 9, the condition is out of date with respect to the current
state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: reason contains a programmatic identifier indicating
the reason for the condition's last transition. Producers
of specific condition types may define expected values and
meanings for this field, and whether the values are considered
a guaranteed API. The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
--- Many .condition.type values are consistent across resources
like Available, but because arbitrary conditions can be useful
(see .node.status.conditions), the ability to deconflict is
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
lastAutomationRunTime:
description: LastAutomationRunTime records the last time the controller
ran this automation through to completion (even if no updates were
made).
format: date-time
type: string
lastHandledReconcileAt:
description: LastHandledReconcileAt holds the value of the most recent
reconcile request value, so a change can be detected.
type: string
lastPushCommit:
description: LastPushCommit records the SHA1 of the last commit made
by the controller, for this automation object
type: string
lastPushTime:
description: LastPushTime records the time of the last pushed change.
format: date-time
type: string
observedGeneration:
format: int64
type: integer
type: object
type: object
served: true
storage: false
subresources:
status: {}
- additionalPrinterColumns:
- jsonPath: .status.lastAutomationRunTime
name: Last run
type: string
name: v1beta1
schema:
openAPIV3Schema:
description: ImageUpdateAutomation is the Schema for the imageupdateautomations
API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: ImageUpdateAutomationSpec defines the desired state of ImageUpdateAutomation
properties:
git:
description: GitSpec contains all the git-specific definitions. This
is technically optional, but in practice mandatory until there are
other kinds of source allowed.
properties:
checkout:
description: Checkout gives the parameters for cloning the git
repository, ready to make changes. If not present, the `spec.ref`
field from the referenced `GitRepository` or its default will
be used.
properties:
ref:
description: Reference gives a branch, tag or commit to clone
from the Git repository.
properties:
branch:
description: The Git branch to checkout, defaults to master.
type: string
commit:
description: The Git commit SHA to checkout, if specified
Tag filters will be ignored.
type: string
semver:
description: The Git tag semver expression, takes precedence
over Tag.
type: string
tag:
description: The Git tag to checkout, takes precedence
over Branch.
type: string
type: object
required:
- ref
type: object
commit:
description: Commit specifies how to commit to the git repository.
properties:
author:
description: Author gives the email and optionally the name
to use as the author of commits.
properties:
email:
description: Email gives the email to provide when making
a commit.
type: string
name:
description: Name gives the name to provide when making
a commit.
type: string
required:
- email
type: object
messageTemplate:
description: MessageTemplate provides a template for the commit
message, into which will be interpolated the details of
the change made.
type: string
signingKey:
description: SigningKey provides the option to sign commits
with a GPG key
properties:
secretRef:
description: SecretRef holds the name to a secret that
contains a 'git.asc' key corresponding to the ASCII
Armored file containing the GPG signing keypair as the
value. It must be in the same namespace as the ImageUpdateAutomation.
properties:
name:
description: Name of the referent
type: string
required:
- name
type: object
type: object
required:
- author
type: object
push:
description: Push specifies how and where to push commits made
by the automation. If missing, commits are pushed (back) to
`.spec.checkout.branch` or its default.
properties:
branch:
description: Branch specifies that commits should be pushed
to the branch named. The branch is created using `.spec.checkout.branch`
as the starting point, if it doesn't already exist.
type: string
required:
- branch
type: object
required:
- commit
type: object
interval:
description: Interval gives an lower bound for how often the automation
run should be attempted.
type: string
sourceRef:
description: SourceRef refers to the resource giving access details
to a git repository.
properties:
apiVersion:
@ -271,23 +665,20 @@ spec:
- name
type: object
suspend:
description: |-
Suspend tells the controller to not run this automation, until
it is unset (or set to false). Defaults to false.
description: Suspend tells the controller to not run this automation,
until it is unset (or set to false). Defaults to false.
type: boolean
update:
default:
strategy: Setters
description: |-
Update gives the specification for how to update the files in
the repository. This can be left empty, to use the default
description: Update gives the specification for how to update the
files in the repository. This can be left empty, to use the default
value.
properties:
path:
description: |-
Path to the directory containing the manifests to be updated.
Defaults to 'None', which translates to the root path
of the GitRepositoryRef.
description: Path to the directory containing the manifests to
be updated. Defaults to 'None', which translates to the root
path of the GitRepositoryRef.
type: string
strategy:
default: Setters
@ -295,6 +686,8 @@ spec:
enum:
- Setters
type: string
required:
- strategy
type: object
required:
- interval
@ -308,35 +701,44 @@ spec:
properties:
conditions:
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
description: "Condition contains details for one aspect of the current
state of this API Resource. --- This struct is intended for direct
use as an array at the field path .status.conditions. For example,
type FooStatus struct{ // Represents the observations of a
foo's current state. // Known .status.conditions.type are:
\"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type
\ // +patchStrategy=merge // +listType=map // +listMapKey=type
\ Conditions []metav1.Condition `json:\"conditions,omitempty\"
patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`
\n // other fields }"
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
description: lastTransitionTime is the last time the condition
transitioned from one status to another. This should be when
the underlying condition changed. If that is not known, then
using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
description: message is a human readable message indicating
details about the transition. This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
description: observedGeneration represents the .metadata.generation
that the condition was set based upon. For instance, if .metadata.generation
is currently 12, but the .status.conditions[x].observedGeneration
is 9, the condition is out of date with respect to the current
state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
description: reason contains a programmatic identifier indicating
the reason for the condition's last transition. Producers
of specific condition types may define expected values and
meanings for this field, and whether the values are considered
a guaranteed API. The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
@ -351,6 +753,10 @@ spec:
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
--- Many .condition.type values are consistent across resources
like Available, but because arbitrary conditions can be useful
(see .node.status.conditions), the ability to deconflict is
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
@ -363,22 +769,18 @@ spec:
type: object
type: array
lastAutomationRunTime:
description: |-
LastAutomationRunTime records the last time the controller ran
this automation through to completion (even if no updates were
description: LastAutomationRunTime records the last time the controller
ran this automation through to completion (even if no updates were
made).
format: date-time
type: string
lastHandledReconcileAt:
description: |-
LastHandledReconcileAt holds the value of the most recent
reconcile request value, so a change of the annotation value
can be detected.
description: LastHandledReconcileAt holds the value of the most recent
reconcile request value, so a change can be detected.
type: string
lastPushCommit:
description: |-
LastPushCommit records the SHA1 of the last commit made by the
controller, for this automation object
description: LastPushCommit records the SHA1 of the last commit made
by the controller, for this automation object
type: string
lastPushTime:
description: LastPushTime records the time of the last pushed change.
@ -387,36 +789,15 @@ spec:
observedGeneration:
format: int64
type: integer
observedPolicies:
additionalProperties:
description: ImageRef represents an image reference.
properties:
digest:
description: Digest is the image's digest.
type: string
name:
description: Name is the bare image's name.
type: string
tag:
description: Tag is the image's tag.
type: string
required:
- name
- tag
type: object
description: |-
ObservedPolicies is the list of observed ImagePolicies that were
considered by the ImageUpdateAutomation update process.
type: object
observedSourceRevision:
description: |-
ObservedPolicies []ObservedPolicy `json:"observedPolicies,omitempty"`
ObservedSourceRevision is the last observed source revision. This can be
used to determine if the source has been updated since last observation.
type: string
type: object
type: object
served: true
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@ -5,4 +5,4 @@ resources:
images:
- name: fluxcd/image-automation-controller
newName: fluxcd/image-automation-controller
newTag: v0.41.2
newTag: v0.20.0

View File

@ -1,38 +1,11 @@
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
creationTimestamp: null
name: manager-role
rules:
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
- list
- watch
- apiGroups:
- image.toolkit.fluxcd.io
resources:
- imagepolicies
verbs:
- get
- list
- watch
- apiGroups:
- image.toolkit.fluxcd.io
resources:
- imagepolicies/status
verbs:
- get
- apiGroups:
- image.toolkit.fluxcd.io
resources:

View File

@ -0,0 +1,14 @@
apiVersion: image.toolkit.fluxcd.io/v1alpha1
kind: ImageUpdateAutomation
metadata:
name: imageupdateautomation-sample
spec:
checkout:
gitRepositoryRef:
name: app-repo
branch: main
interval: 5m
# update strategy is left to default to "Setters"
commit:
authorName: Fluxbot
authorEmail: fluxbot@example.com

View File

@ -1,4 +1,4 @@
apiVersion: image.toolkit.fluxcd.io/v1beta2
apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImageUpdateAutomation
metadata:
name: imageupdateautomation-sample

View File

@ -0,0 +1,76 @@
/*
Copyright 2020, 2021 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controllers
import (
"errors"
"testing"
)
func TestLibgit2ErrorTidy(t *testing.T) {
// this is what GitLab sends if the deploy key doesn't have write access
gitlabMessage := `remote:
remote: ========================================================================
remote:
remote: This deploy key does not have write access to this project.
remote:
remote: ========================================================================
remote:
`
expectedReformat := "remote: This deploy key does not have write access to this project."
err := errors.New(gitlabMessage)
err = libgit2PushError(err)
reformattedMessage := err.Error()
if reformattedMessage != expectedReformat {
t.Errorf("expected %q, got %q", expectedReformat, reformattedMessage)
}
}
func TestLibgit2Multiline(t *testing.T) {
// this is a hypothetical error message, in which the useful
// content spans more than one line
multilineMessage := `remote:
remote: ========================================================================
remote:
remote: This deploy key does not have write access to this project.
remote: You will need to create a new deploy key.
remote:
remote: ========================================================================
remote:
`
expectedReformat := "remote: This deploy key does not have write access to this project. You will need to create a new deploy key."
err := errors.New(multilineMessage)
err = libgit2PushError(err)
reformattedMessage := err.Error()
if reformattedMessage != expectedReformat {
t.Errorf("expected %q, got %q", expectedReformat, reformattedMessage)
}
}
func TestLibgit2ErrorUnchanged(t *testing.T) {
// this is (roughly) what GitHub sends if the deploy key doesn't have write access
regularMessage := `remote: ERROR: deploy key does not have permissions`
expectedReformat := regularMessage
err := errors.New(regularMessage)
err = libgit2PushError(err)
reformattedMessage := err.Error()
if reformattedMessage != expectedReformat {
t.Errorf("expected %q, got %q", expectedReformat, reformattedMessage)
}
}

180
controllers/git_test.go Normal file
View File

@ -0,0 +1,180 @@
package controllers
import (
"context"
"os"
"path/filepath"
"testing"
"time"
"github.com/go-git/go-billy/v5/memfs"
gogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/go-logr/logr"
"github.com/fluxcd/pkg/gittestserver"
)
func populateRepoFromFixture(repo *gogit.Repository, fixture string) error {
working, err := repo.Worktree()
if err != nil {
return err
}
fs := working.Filesystem
if err = filepath.Walk(fixture, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return fs.MkdirAll(fs.Join(path[len(fixture):]), info.Mode())
}
// copy symlinks as-is, so I can test what happens with broken symlinks
if info.Mode()&os.ModeSymlink > 0 {
target, err := os.Readlink(path)
if err != nil {
return err
}
return fs.Symlink(target, path[len(fixture):])
}
fileBytes, err := os.ReadFile(path)
if err != nil {
return err
}
ff, err := fs.Create(path[len(fixture):])
if err != nil {
return err
}
defer ff.Close()
_, err = ff.Write(fileBytes)
return err
}); err != nil {
return err
}
_, err = working.Add(".")
if err != nil {
return err
}
if _, err = working.Commit("Initial revision from "+fixture, &gogit.CommitOptions{
Author: &object.Signature{
Name: "Testbot",
Email: "test@example.com",
When: time.Now(),
},
}); err != nil {
return err
}
return nil
}
func TestRepoForFixture(t *testing.T) {
repo, err := gogit.Init(memory.NewStorage(), memfs.New())
if err != nil {
t.Fatal(err)
}
err = populateRepoFromFixture(repo, "testdata/pathconfig")
if err != nil {
t.Error(err)
}
}
func TestIgnoreBrokenSymlink(t *testing.T) {
// init a git repo in the filesystem so we can operate on files there
tmp, err := os.MkdirTemp("", "flux-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmp)
repo, err := gogit.PlainInit(tmp, false)
if err != nil {
t.Fatal(err)
}
err = populateRepoFromFixture(repo, "testdata/brokenlink")
if err != nil {
t.Fatal(err)
}
_, err = commitChangedManifests(logr.Discard(), repo, tmp, nil, nil, "unused")
if err != errNoChanges {
t.Fatalf("expected no changes but got: %v", err)
}
}
// this is a hook script that will reject a ref update for a branch
// that's not `main`
const rejectBranch = `
if [ "$1" != "refs/heads/main" ]; then
echo "*** Rejecting push to non-main branch $1" >&2
exit 1
fi
`
func TestPushRejected(t *testing.T) {
// Check that pushing to a repository which rejects a ref update
// results in an error. Why would a repo reject an update? If yu
// use e.g., branch protection in GitHub, this is what happens --
// see
// https://github.com/fluxcd/image-automation-controller/issues/194.
branch := "push-branch"
gitServer, err := gittestserver.NewTempGitServer()
if err != nil {
t.Fatal(err)
}
gitServer.AutoCreate()
gitServer.InstallUpdateHook(rejectBranch)
if err = gitServer.StartHTTP(); err != nil {
t.Fatal(err)
}
// this is currently defined in update_test.go, but handy right here ..
if err = initGitRepo(gitServer, "testdata/appconfig", "main", "/appconfig.git"); err != nil {
t.Fatal(err)
}
tmp, err := os.MkdirTemp("", "gotest-imageauto-git")
if err != nil {
t.Fatal(err)
}
repoURL := gitServer.HTTPAddress() + "/appconfig.git"
repo, err := gogit.PlainClone(tmp, false, &gogit.CloneOptions{
URL: repoURL,
ReferenceName: plumbing.NewBranchReferenceName("main"),
})
// This is here to guard against push in general being broken
err = push(context.TODO(), tmp, "main", repoAccess{
url: repoURL,
auth: nil,
})
if err != nil {
t.Fatal(err)
}
// This is not under test, but needed for the next bit
if err = switchBranch(repo, branch); err != nil {
t.Fatal(err)
}
// This is supposed to fail, because the hook rejects the branch
// pushed to.
err = push(context.TODO(), tmp, branch, repoAccess{
url: repoURL,
auth: nil,
})
if err == nil {
t.Error("push to a forbidden branch is expected to fail, but succeeded")
}
}

View File

@ -0,0 +1,839 @@
/*
Copyright 2020 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controllers
import (
"bytes"
"context"
"errors"
"fmt"
"math"
"os"
"path/filepath"
"strings"
"text/template"
"time"
"github.com/Masterminds/sprig/v3"
gogit "github.com/go-git/go-git/v5"
libgit2 "github.com/libgit2/git2go/v31"
"github.com/ProtonMail/go-crypto/openpgp"
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
kuberecorder "k8s.io/client-go/tools/record"
"k8s.io/client-go/tools/reference"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
imagev1_reflect "github.com/fluxcd/image-reflector-controller/api/v1beta1"
apiacl "github.com/fluxcd/pkg/apis/acl"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/acl"
"github.com/fluxcd/pkg/runtime/events"
"github.com/fluxcd/pkg/runtime/logger"
"github.com/fluxcd/pkg/runtime/metrics"
"github.com/fluxcd/pkg/runtime/predicates"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
"github.com/fluxcd/source-controller/pkg/git"
gitlibgit2 "github.com/fluxcd/source-controller/pkg/git/libgit2"
gitstrat "github.com/fluxcd/source-controller/pkg/git/strategy"
imagev1 "github.com/fluxcd/image-automation-controller/api/v1beta1"
"github.com/fluxcd/image-automation-controller/pkg/update"
)
const originRemote = "origin"
const defaultMessageTemplate = `Update from image update automation`
const repoRefKey = ".spec.gitRepository"
const signingSecretKey = "git.asc"
// TemplateData is the type of the value given to the commit message
// template.
type TemplateData struct {
AutomationObject types.NamespacedName
Updated update.Result
}
// ImageUpdateAutomationReconciler reconciles a ImageUpdateAutomation object
type ImageUpdateAutomationReconciler struct {
client.Client
Scheme *runtime.Scheme
EventRecorder kuberecorder.EventRecorder
ExternalEventRecorder *events.Recorder
MetricsRecorder *metrics.Recorder
NoCrossNamespaceRef bool
}
type ImageUpdateAutomationReconcilerOptions struct {
MaxConcurrentReconciles int
}
// +kubebuilder:rbac:groups=image.toolkit.fluxcd.io,resources=imageupdateautomations,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=image.toolkit.fluxcd.io,resources=imageupdateautomations/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=gitrepositories,verbs=get;list;watch
func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := ctrl.LoggerFrom(ctx)
debuglog := log.V(logger.DebugLevel)
tracelog := log.V(logger.TraceLevel)
now := time.Now()
var templateValues TemplateData
var auto imagev1.ImageUpdateAutomation
if err := r.Get(ctx, req.NamespacedName, &auto); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// record suspension metrics
defer r.recordSuspension(ctx, auto)
if auto.Spec.Suspend {
log.Info("ImageUpdateAutomation is suspended, skipping automation run")
return ctrl.Result{}, nil
}
templateValues.AutomationObject = req.NamespacedName
// Record readiness metric when exiting; if there's any points at
// which the readiness is updated _without also exiting_, they
// should also record the readiness.
defer r.recordReadinessMetric(ctx, &auto)
// Record reconciliation duration when exiting
if r.MetricsRecorder != nil {
objRef, err := reference.GetReference(r.Scheme, &auto)
if err != nil {
return ctrl.Result{}, err
}
defer r.MetricsRecorder.RecordDuration(*objRef, now)
}
// whatever else happens, we've now "seen" the reconcile
// annotation if it's there
if token, ok := meta.ReconcileAnnotationValue(auto.GetAnnotations()); ok {
auto.Status.SetLastHandledReconcileRequest(token)
if err := r.patchStatus(ctx, req, auto.Status); err != nil {
return ctrl.Result{Requeue: true}, err
}
}
// failWithError is a helper for bailing on the reconciliation.
failWithError := func(err error) (ctrl.Result, error) {
r.event(ctx, auto, events.EventSeverityError, err.Error())
imagev1.SetImageUpdateAutomationReadiness(&auto, metav1.ConditionFalse, meta.ReconciliationFailedReason, err.Error())
if err := r.patchStatus(ctx, req, auto.Status); err != nil {
log.Error(err, "failed to reconcile")
}
return ctrl.Result{Requeue: true}, err
}
// get the git repository object so it can be checked out
// only GitRepository objects are supported for now
if kind := auto.Spec.SourceRef.Kind; kind != sourcev1.GitRepositoryKind {
return failWithError(fmt.Errorf("source kind %q not supported", kind))
}
gitSpec := auto.Spec.GitSpec
if gitSpec == nil {
return failWithError(fmt.Errorf("source kind %s neccessitates field .spec.git", sourcev1.GitRepositoryKind))
}
var origin sourcev1.GitRepository
gitRepoNamespace := req.Namespace
if auto.Spec.SourceRef.Namespace != "" {
gitRepoNamespace = auto.Spec.SourceRef.Namespace
}
originName := types.NamespacedName{
Name: auto.Spec.SourceRef.Name,
Namespace: gitRepoNamespace,
}
debuglog.Info("fetching git repository", "gitrepository", originName)
if r.NoCrossNamespaceRef && gitRepoNamespace != auto.GetNamespace() {
err := acl.AccessDeniedError(fmt.Sprintf("can't access '%s/%s', cross-namespace references have been blocked",
auto.Spec.SourceRef.Kind, originName))
log.Error(err, "access denied to cross-namespaced resource")
imagev1.SetImageUpdateAutomationReadiness(&auto, metav1.ConditionFalse, apiacl.AccessDeniedReason,
err.Error())
if err := r.patchStatus(ctx, req, auto.Status); err != nil {
return ctrl.Result{Requeue: true}, err
}
r.event(ctx, auto, events.EventSeverityError, err.Error())
return ctrl.Result{}, nil
}
if err := r.Get(ctx, originName, &origin); err != nil {
if client.IgnoreNotFound(err) == nil {
imagev1.SetImageUpdateAutomationReadiness(&auto, metav1.ConditionFalse, imagev1.GitNotAvailableReason, "referenced git repository is missing")
log.Error(err, fmt.Sprintf("referenced git repository %s does not exist.", originName.String()))
if err := r.patchStatus(ctx, req, auto.Status); err != nil {
return ctrl.Result{Requeue: true}, err
}
return ctrl.Result{}, nil // and assume we'll hear about it when it arrives
}
return ctrl.Result{}, err
}
// validate the git spec and default any values needed later, before proceeding
var ref *sourcev1.GitRepositoryRef
if gitSpec.Checkout != nil {
ref = &gitSpec.Checkout.Reference
tracelog.Info("using git repository ref from .spec.git.checkout", "ref", ref)
} else if r := origin.Spec.Reference; r != nil {
ref = r
tracelog.Info("using git repository ref from GitRepository spec", "ref", ref)
} // else remain as `nil`, which is an acceptable value for cloneInto, later.
var pushBranch string
if gitSpec.Push != nil {
pushBranch = gitSpec.Push.Branch
tracelog.Info("using push branch from .spec.push.branch", "branch", pushBranch)
} else {
// Here's where it gets constrained. If there's no push branch
// given, then the checkout ref must include a branch, and
// that can be used.
if ref == nil || ref.Branch == "" {
return failWithError(fmt.Errorf("Push branch not given explicitly, and cannot be inferred from .spec.git.checkout.ref or GitRepository .spec.ref"))
}
pushBranch = ref.Branch
tracelog.Info("using push branch from $ref.branch", "branch", pushBranch)
}
tmp, err := os.MkdirTemp("", fmt.Sprintf("%s-%s", originName.Namespace, originName.Name))
if err != nil {
return failWithError(err)
}
defer func() {
if err := os.RemoveAll(tmp); err != nil {
log.Error(err, "failed to remove working directory", "path", tmp)
}
}()
// FIXME use context with deadline for at least the following ops
debuglog.Info("attempting to clone git repository", "gitrepository", originName, "ref", ref, "working", tmp)
access, err := r.getRepoAccess(ctx, &origin)
if err != nil {
return failWithError(err)
}
// Use the git operations timeout for the repo.
cloneCtx, cancel := context.WithTimeout(ctx, origin.Spec.Timeout.Duration)
defer cancel()
var repo *gogit.Repository
if repo, err = cloneInto(cloneCtx, access, ref, tmp); err != nil {
return failWithError(err)
}
// When there's a push spec, the pushed-to branch is where commits
// shall be made
if gitSpec.Push != nil {
// Use the git operations timeout for the repo.
fetchCtx, cancel := context.WithTimeout(ctx, origin.Spec.Timeout.Duration)
defer cancel()
if err := fetch(fetchCtx, tmp, pushBranch, access); err != nil && err != errRemoteBranchMissing {
return failWithError(err)
}
if err = switchBranch(repo, pushBranch); err != nil {
return failWithError(err)
}
}
switch {
case auto.Spec.Update != nil && auto.Spec.Update.Strategy == imagev1.UpdateStrategySetters:
// For setters we first want to compile a list of _all_ the
// policies in the same namespace (maybe in the future this
// could be filtered by the automation object).
var policies imagev1_reflect.ImagePolicyList
if err := r.List(ctx, &policies, &client.ListOptions{Namespace: req.NamespacedName.Namespace}); err != nil {
return failWithError(err)
}
manifestsPath := tmp
if auto.Spec.Update.Path != "" {
tracelog.Info("adjusting update path according to .spec.update.path", "base", tmp, "spec-path", auto.Spec.Update.Path)
p, err := securejoin.SecureJoin(tmp, auto.Spec.Update.Path)
if err != nil {
return failWithError(err)
}
manifestsPath = p
}
debuglog.Info("updating with setters according to image policies", "count", len(policies.Items), "manifests-path", manifestsPath)
if tracelog.Enabled() {
for _, item := range policies.Items {
tracelog.Info("found policy", "namespace", item.Namespace, "name", item.Name, "latest-image", item.Status.LatestImage)
}
}
if result, err := updateAccordingToSetters(ctx, tracelog, manifestsPath, policies.Items); err != nil {
return failWithError(err)
} else {
templateValues.Updated = result
}
default:
log.Info("no update strategy given in the spec")
// no sense rescheduling until this resource changes
r.event(ctx, auto, events.EventSeverityInfo, "no known update strategy in spec, failing trivially")
imagev1.SetImageUpdateAutomationReadiness(&auto, metav1.ConditionFalse, imagev1.NoStrategyReason, "no known update strategy is given for object")
return ctrl.Result{}, r.patchStatus(ctx, req, auto.Status)
}
debuglog.Info("ran updates to working dir", "working", tmp)
var statusMessage string
var signingEntity *openpgp.Entity
if gitSpec.Commit.SigningKey != nil {
if signingEntity, err = r.getSigningEntity(ctx, auto); err != nil {
failWithError(err)
}
}
// construct the commit message from template and values
message, err := templateMsg(gitSpec.Commit.MessageTemplate, &templateValues)
if err != nil {
return failWithError(err)
}
// The status message depends on what happens next. Since there's
// more than one way to succeed, there's some if..else below, and
// early returns only on failure.
author := &object.Signature{
Name: gitSpec.Commit.Author.Name,
Email: gitSpec.Commit.Author.Email,
When: time.Now(),
}
if rev, err := commitChangedManifests(tracelog, repo, tmp, signingEntity, author, message); err != nil {
if err != errNoChanges {
return failWithError(err)
}
debuglog.Info("no changes made in working directory; no commit")
statusMessage = "no updates made"
if auto.Status.LastPushTime != nil && len(auto.Status.LastPushCommit) >= 7 {
statusMessage = fmt.Sprintf("%s; last commit %s at %s", statusMessage, auto.Status.LastPushCommit[:7], auto.Status.LastPushTime.Format(time.RFC3339))
}
} else {
// Use the git operations timeout for the repo.
pushCtx, cancel := context.WithTimeout(ctx, origin.Spec.Timeout.Duration)
defer cancel()
if err := push(pushCtx, tmp, pushBranch, access); err != nil {
return failWithError(err)
}
r.event(ctx, auto, events.EventSeverityInfo, fmt.Sprintf("Committed and pushed change %s to %s\n%s", rev, pushBranch, message))
log.Info("pushed commit to origin", "revision", rev, "branch", pushBranch)
auto.Status.LastPushCommit = rev
auto.Status.LastPushTime = &metav1.Time{Time: now}
statusMessage = "committed and pushed " + rev + " to " + pushBranch
}
// Getting to here is a successful run.
auto.Status.LastAutomationRunTime = &metav1.Time{Time: now}
imagev1.SetImageUpdateAutomationReadiness(&auto, metav1.ConditionTrue, meta.ReconciliationSucceededReason, statusMessage)
if err := r.patchStatus(ctx, req, auto.Status); err != nil {
return ctrl.Result{Requeue: true}, err
}
// We're either in this method because something changed, or this
// object got requeued. Either way, once successful, we don't need
// to see the object again until Interval has passed, or something
// changes again.
interval := intervalOrDefault(&auto)
return ctrl.Result{RequeueAfter: interval}, nil
}
func (r *ImageUpdateAutomationReconciler) SetupWithManager(mgr ctrl.Manager, opts ImageUpdateAutomationReconcilerOptions) error {
ctx := context.Background()
// Index the git repository object that each I-U-A refers to
if err := mgr.GetFieldIndexer().IndexField(ctx, &imagev1.ImageUpdateAutomation{}, repoRefKey, func(obj client.Object) []string {
updater := obj.(*imagev1.ImageUpdateAutomation)
ref := updater.Spec.SourceRef
return []string{ref.Name}
}); err != nil {
return err
}
return ctrl.NewControllerManagedBy(mgr).
For(&imagev1.ImageUpdateAutomation{}, builder.WithPredicates(
predicate.Or(predicate.GenerationChangedPredicate{}, predicates.ReconcileRequestedPredicate{}))).
Watches(&source.Kind{Type: &sourcev1.GitRepository{}}, handler.EnqueueRequestsFromMapFunc(r.automationsForGitRepo)).
Watches(&source.Kind{Type: &imagev1_reflect.ImagePolicy{}}, handler.EnqueueRequestsFromMapFunc(r.automationsForImagePolicy)).
WithOptions(controller.Options{
MaxConcurrentReconciles: opts.MaxConcurrentReconciles,
}).
Complete(r)
}
func (r *ImageUpdateAutomationReconciler) patchStatus(ctx context.Context,
req ctrl.Request,
newStatus imagev1.ImageUpdateAutomationStatus) error {
var auto imagev1.ImageUpdateAutomation
if err := r.Get(ctx, req.NamespacedName, &auto); err != nil {
return err
}
patch := client.MergeFrom(auto.DeepCopy())
auto.Status = newStatus
return r.Status().Patch(ctx, &auto, patch)
}
// intervalOrDefault gives the interval specified, or if missing, the default
func intervalOrDefault(auto *imagev1.ImageUpdateAutomation) time.Duration {
if auto.Spec.Interval.Duration < time.Second {
return time.Second
}
return auto.Spec.Interval.Duration
}
// durationSinceLastRun calculates how long it's been since the last
// time the automation ran (which you can then use to find how long to
// wait until the next run).
func durationSinceLastRun(auto *imagev1.ImageUpdateAutomation, now time.Time) time.Duration {
last := auto.Status.LastAutomationRunTime
if last == nil {
return time.Duration(math.MaxInt64) // a fairly long time
}
return now.Sub(last.Time)
}
// automationsForGitRepo fetches all the automations that refer to a
// particular source.GitRepository object.
func (r *ImageUpdateAutomationReconciler) automationsForGitRepo(obj client.Object) []reconcile.Request {
ctx := context.Background()
var autoList imagev1.ImageUpdateAutomationList
if err := r.List(ctx, &autoList, client.InNamespace(obj.GetNamespace()),
client.MatchingFields{repoRefKey: obj.GetName()}); err != nil {
return nil
}
reqs := make([]reconcile.Request, len(autoList.Items), len(autoList.Items))
for i := range autoList.Items {
reqs[i].NamespacedName.Name = autoList.Items[i].GetName()
reqs[i].NamespacedName.Namespace = autoList.Items[i].GetNamespace()
}
return reqs
}
// automationsForImagePolicy fetches all the automation objects that
// might depend on a image policy object. Since the link is via
// markers in the git repo, _any_ automation object in the same
// namespace could be affected.
func (r *ImageUpdateAutomationReconciler) automationsForImagePolicy(obj client.Object) []reconcile.Request {
ctx := context.Background()
var autoList imagev1.ImageUpdateAutomationList
if err := r.List(ctx, &autoList, client.InNamespace(obj.GetNamespace())); err != nil {
return nil
}
reqs := make([]reconcile.Request, len(autoList.Items), len(autoList.Items))
for i := range autoList.Items {
reqs[i].NamespacedName.Name = autoList.Items[i].GetName()
reqs[i].NamespacedName.Namespace = autoList.Items[i].GetNamespace()
}
return reqs
}
// --- git ops
// Note: libgit2 is always used for network operations; for cloning,
// it will do a non-shallow clone, and for anything else, it doesn't
// matter what is used.
type repoAccess struct {
auth *git.AuthOptions
url string
}
func (r *ImageUpdateAutomationReconciler) getRepoAccess(ctx context.Context, repository *sourcev1.GitRepository) (repoAccess, error) {
var access repoAccess
access.url = repository.Spec.URL
if repository.Spec.SecretRef != nil {
name := types.NamespacedName{
Namespace: repository.GetNamespace(),
Name: repository.Spec.SecretRef.Name,
}
secret := &corev1.Secret{}
err := r.Client.Get(ctx, name, secret)
if err != nil {
err = fmt.Errorf("auth secret error: %w", err)
return access, err
}
access.auth, err = git.AuthOptionsFromSecret(access.url, secret)
if err != nil {
err = fmt.Errorf("auth error: %w", err)
return access, err
}
}
return access, nil
}
func (r repoAccess) remoteCallbacks(ctx context.Context) libgit2.RemoteCallbacks {
return gitlibgit2.RemoteCallbacks(ctx, r.auth)
}
// cloneInto clones the upstream repository at the `ref` given (which
// can be `nil`). It returns a `*gogit.Repository` since that is used
// for committing changes.
func cloneInto(ctx context.Context, access repoAccess, ref *sourcev1.GitRepositoryRef, path string) (*gogit.Repository, error) {
opts := git.CheckoutOptions{}
if ref != nil {
opts.Tag = ref.Tag
opts.SemVer = ref.SemVer
opts.Tag = ref.Tag
opts.Branch = ref.Branch
}
checkoutStrat, err := gitstrat.CheckoutStrategyForImplementation(ctx, sourcev1.LibGit2Implementation, opts)
if err == nil {
_, err = checkoutStrat.Checkout(ctx, path, access.url, access.auth)
}
if err != nil {
return nil, err
}
return gogit.PlainOpen(path)
}
// switchBranch switches the repo from the current branch to the
// branch given. If the branch does not exist, it is created using the
// head as the starting point.
func switchBranch(repo *gogit.Repository, pushBranch string) error {
localBranch := plumbing.NewBranchReferenceName(pushBranch)
// is the branch already present?
_, err := repo.Reference(localBranch, true)
var create bool
switch {
case err == plumbing.ErrReferenceNotFound:
// make a new branch, starting at HEAD
create = true
case err != nil:
return err
default:
// local branch found, great
break
}
tree, err := repo.Worktree()
if err != nil {
return err
}
return tree.Checkout(&gogit.CheckoutOptions{
Branch: localBranch,
Create: create,
})
}
var errNoChanges error = errors.New("no changes made to working directory")
func commitChangedManifests(tracelog logr.Logger, repo *gogit.Repository, absRepoPath string, ent *openpgp.Entity, author *object.Signature, message string) (string, error) {
working, err := repo.Worktree()
if err != nil {
return "", err
}
status, err := working.Status()
if err != nil {
return "", err
}
// go-git has [a bug](https://github.com/go-git/go-git/issues/253)
// whereby it thinks broken symlinks to absolute paths are
// modified. There's no circumstance in which we want to commit a
// change to a broken symlink: so, detect and skip those.
var changed bool
for file, _ := range status {
abspath := filepath.Join(absRepoPath, file)
info, err := os.Lstat(abspath)
if err != nil {
return "", fmt.Errorf("checking if %s is a symlink: %w", file, err)
}
if info.Mode()&os.ModeSymlink > 0 {
// symlinks are OK; broken symlinks are probably a result
// of the bug mentioned above, but not of interest in any
// case.
if _, err := os.Stat(abspath); os.IsNotExist(err) {
tracelog.Info("apparently broken symlink found; ignoring", "path", abspath)
continue
}
}
tracelog.Info("adding file", "file", file)
working.Add(file)
changed = true
}
if !changed {
return "", errNoChanges
}
var rev plumbing.Hash
if rev, err = working.Commit(message, &gogit.CommitOptions{
Author: author,
SignKey: ent,
}); err != nil {
return "", err
}
return rev.String(), nil
}
// getSigningEntity retrieves an OpenPGP entity referenced by the
// provided imagev1.ImageUpdateAutomation for git commit signing
func (r *ImageUpdateAutomationReconciler) getSigningEntity(ctx context.Context, auto imagev1.ImageUpdateAutomation) (*openpgp.Entity, error) {
// get kubernetes secret
secretName := types.NamespacedName{
Namespace: auto.GetNamespace(),
Name: auto.Spec.GitSpec.Commit.SigningKey.SecretRef.Name,
}
var secret corev1.Secret
if err := r.Get(ctx, secretName, &secret); err != nil {
return nil, fmt.Errorf("could not find signing key secret '%s': %w", secretName, err)
}
// get data from secret
data, ok := secret.Data[signingSecretKey]
if !ok {
return nil, fmt.Errorf("signing key secret '%s' does not contain a 'git.asc' key", secretName)
}
// read entity from secret value
entities, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("could not read signing key from secret '%s': %w", secretName, err)
}
if len(entities) > 1 {
return nil, fmt.Errorf("multiple entities read from secret '%s', could not determine which signing key to use", secretName)
}
return entities[0], nil
}
var errRemoteBranchMissing = errors.New("remote branch missing")
// fetch gets the remote branch given and updates the local branch
// head of the same name, so it can be switched to. If the fetch
// completes, it returns nil; if the remote branch is missing, it
// returns errRemoteBranchMissing (this is to work in sympathy with
// `switchBranch`, which will create the branch if it doesn't
// exist). For any other problem it will return the error.
func fetch(ctx context.Context, path string, branch string, access repoAccess) error {
refspec := fmt.Sprintf("refs/heads/%s:refs/heads/%s", branch, branch)
repo, err := libgit2.OpenRepository(path)
if err != nil {
return err
}
defer repo.Free()
origin, err := repo.Remotes.Lookup(originRemote)
if err != nil {
return err
}
defer origin.Free()
err = origin.Fetch(
[]string{refspec},
&libgit2.FetchOptions{
RemoteCallbacks: access.remoteCallbacks(ctx),
ProxyOptions: libgit2.ProxyOptions{Type: libgit2.ProxyTypeAuto},
}, "",
)
if err != nil && libgit2.IsErrorCode(err, libgit2.ErrorCodeNotFound) {
return errRemoteBranchMissing
}
return err
}
// push pushes the branch given to the origin using the git library
// indicated by `impl`. It's passed both the path to the repo and a
// gogit.Repository value, since the latter may as well be used if the
// implementation is GoGit.
func push(ctx context.Context, path, branch string, access repoAccess) error {
repo, err := libgit2.OpenRepository(path)
if err != nil {
return err
}
defer repo.Free()
origin, err := repo.Remotes.Lookup(originRemote)
if err != nil {
return err
}
defer origin.Free()
callbacks := access.remoteCallbacks(ctx)
// calling repo.Push will succeed even if a reference update is
// rejected; to detect this case, this callback is supplied.
var callbackErr error
callbacks.PushUpdateReferenceCallback = func(refname, status string) libgit2.ErrorCode {
if status != "" {
callbackErr = fmt.Errorf("ref %s rejected: %s", refname, status)
}
return libgit2.ErrorCodeOK
}
err = origin.Push([]string{fmt.Sprintf("refs/heads/%s:refs/heads/%s", branch, branch)}, &libgit2.PushOptions{
RemoteCallbacks: callbacks,
ProxyOptions: libgit2.ProxyOptions{Type: libgit2.ProxyTypeAuto},
})
if err != nil {
return libgit2PushError(err)
}
return callbackErr
}
func libgit2PushError(err error) error {
if err == nil {
return err
}
// libgit2 returns the whole output from stderr, and we only need
// the message. GitLab likes to return a banner, so as an
// heuristic, strip any lines that are just "remote:" and spaces
// or fencing.
msg := err.Error()
lines := strings.Split(msg, "\n")
if len(lines) == 1 {
return err
}
var b strings.Builder
// the following removes the prefix "remote:" from each line; to
// retain a bit of fidelity to the original error, start with it.
b.WriteString("remote: ")
var appending bool
for _, line := range lines {
m := strings.TrimPrefix(line, "remote:")
if m = strings.Trim(m, " \t="); m != "" {
if appending {
b.WriteString(" ")
}
b.WriteString(m)
appending = true
}
}
return errors.New(b.String())
}
// --- events, metrics
func (r *ImageUpdateAutomationReconciler) event(ctx context.Context, auto imagev1.ImageUpdateAutomation, severity, msg string) {
if r.EventRecorder != nil {
r.EventRecorder.Event(&auto, "Normal", severity, msg)
}
if r.ExternalEventRecorder != nil {
objRef, err := reference.GetReference(r.Scheme, &auto)
if err != nil {
ctrl.LoggerFrom(ctx).Error(err, "unable to send event")
return
}
if err := r.ExternalEventRecorder.Eventf(*objRef, nil, severity, severity, msg); err != nil {
ctrl.LoggerFrom(ctx).Error(err, "unable to send event")
return
}
}
}
func (r *ImageUpdateAutomationReconciler) recordReadinessMetric(ctx context.Context, auto *imagev1.ImageUpdateAutomation) {
if r.MetricsRecorder == nil {
return
}
objRef, err := reference.GetReference(r.Scheme, auto)
if err != nil {
ctrl.LoggerFrom(ctx).Error(err, "unable to record readiness metric")
return
}
if rc := apimeta.FindStatusCondition(auto.Status.Conditions, meta.ReadyCondition); rc != nil {
r.MetricsRecorder.RecordCondition(*objRef, *rc, !auto.DeletionTimestamp.IsZero())
} else {
r.MetricsRecorder.RecordCondition(*objRef, metav1.Condition{
Type: meta.ReadyCondition,
Status: metav1.ConditionUnknown,
}, !auto.DeletionTimestamp.IsZero())
}
}
// --- updates
// updateAccordingToSetters updates files under the root by treating
// the given image policies as kyaml setters.
func updateAccordingToSetters(ctx context.Context, tracelog logr.Logger, path string, policies []imagev1_reflect.ImagePolicy) (update.Result, error) {
return update.UpdateWithSetters(tracelog, path, path, policies)
}
func (r *ImageUpdateAutomationReconciler) recordSuspension(ctx context.Context, auto imagev1.ImageUpdateAutomation) {
if r.MetricsRecorder == nil {
return
}
log := ctrl.LoggerFrom(ctx)
objRef, err := reference.GetReference(r.Scheme, &auto)
if err != nil {
log.Error(err, "unable to record suspended metric")
return
}
if !auto.DeletionTimestamp.IsZero() {
r.MetricsRecorder.RecordSuspend(*objRef, false)
} else {
r.MetricsRecorder.RecordSuspend(*objRef, auto.Spec.Suspend)
}
}
// templateMsg renders a msg template, returning the message or an error.
func templateMsg(messageTemplate string, templateValues *TemplateData) (string, error) {
if messageTemplate == "" {
messageTemplate = defaultMessageTemplate
}
// Includes only functions that are guaranteed to always evaluate to the same result for given input.
// This removes the possibility of accidentally relying on where or when the template runs.
// https://github.com/Masterminds/sprig/blob/3ac42c7bc5e4be6aa534e036fb19dde4a996da2e/functions.go#L70
t, err := template.New("commit message").Funcs(sprig.HermeticTxtFuncMap()).Parse(messageTemplate)
if err != nil {
return "", fmt.Errorf("unable to create commit message template from spec: %w", err)
}
b := &strings.Builder{}
if err := t.Execute(b, *templateValues); err != nil {
return "", fmt.Errorf("failed to run template from spec: %w", err)
}
return b.String(), nil
}

116
controllers/suite_test.go Normal file
View File

@ -0,0 +1,116 @@
/*
Copyright 2020 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controllers
import (
"context"
"path/filepath"
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
imagev1_reflect "github.com/fluxcd/image-reflector-controller/api/v1beta1"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
imagev1 "github.com/fluxcd/image-automation-controller/api/v1beta1"
// +kubebuilder:scaffold:imports
)
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
var cfg *rest.Config
var k8sClient client.Client
var k8sManager ctrl.Manager
var imageAutoReconciler *ImageUpdateAutomationReconciler
var testEnv *envtest.Environment
var ctx context.Context
var cancel context.CancelFunc
func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecsWithDefaultAndCustomReporters(t,
"Controller Suite",
[]Reporter{printer.NewlineReporter{}})
}
var _ = BeforeSuite(func(done Done) {
ctrl.SetLogger(
zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)),
)
ctx, cancel = context.WithCancel(context.TODO())
By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{
filepath.Join("..", "config", "crd", "bases"),
filepath.Join("testdata", "crds"),
},
}
var err error
cfg, err = testEnv.Start()
Expect(err).ToNot(HaveOccurred())
Expect(cfg).ToNot(BeNil())
Expect(sourcev1.AddToScheme(scheme.Scheme)).To(Succeed())
Expect(imagev1_reflect.AddToScheme(scheme.Scheme)).To(Succeed())
Expect(imagev1.AddToScheme(scheme.Scheme)).To(Succeed())
// +kubebuilder:scaffold:scheme
k8sManager, err = ctrl.NewManager(cfg, ctrl.Options{
Scheme: scheme.Scheme,
})
Expect(err).ToNot(HaveOccurred())
imageAutoReconciler = &ImageUpdateAutomationReconciler{
Client: k8sManager.GetClient(),
Scheme: scheme.Scheme,
}
Expect(imageAutoReconciler.SetupWithManager(k8sManager, ImageUpdateAutomationReconcilerOptions{})).To(Succeed())
go func() {
defer GinkgoRecover()
err = k8sManager.Start(ctx)
Expect(err).ToNot(HaveOccurred())
}()
// Specifically an uncached client. Use <reconciler>.Get if you
// want to see what the reconcilers see.
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).ToNot(HaveOccurred())
Expect(k8sClient).ToNot(BeNil())
close(done)
}, 60)
var _ = AfterSuite(func() {
cancel()
By("tearing down the test environment")
err := testEnv.Stop()
Expect(err).ToNot(HaveOccurred())
})

View File

@ -7,4 +7,4 @@ spec:
spec:
containers:
- name: hello
image: helloworld:1.0.0 # SETTER_SITE
image: helloworld:1.2.0 # SETTER_SITE

1
controllers/testdata/brokenlink/bar.yaml vendored Symbolic link
View File

@ -0,0 +1 @@
/surely/does/not/exist

1415
controllers/update_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
<h1>Image update automation API reference v1beta1</h1>
<h1>Image update automation API reference</h1>
<p>Packages:</p>
<ul class="simple">
<li>
@ -207,8 +207,8 @@ string
<td>
<code>ref</code><br>
<em>
<a href="https://pkg.go.dev/github.com/fluxcd/source-controller/api/v1#GitRepositoryRef">
Source /v1.GitRepositoryRef
<a href="https://godoc.org/github.com/fluxcd/source-controller/api/v1beta1#GitRepositoryRef">
Source /v1beta1.GitRepositoryRef
</a>
</em>
</td>
@ -360,7 +360,7 @@ other kinds of source allowed.</p>
<td>
<code>interval</code><br>
<em>
<a href="https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
<a href="https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
Kubernetes meta/v1.Duration
</a>
</em>
@ -468,7 +468,7 @@ other kinds of source allowed.</p>
<td>
<code>interval</code><br>
<em>
<a href="https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
<a href="https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
Kubernetes meta/v1.Duration
</a>
</em>
@ -598,7 +598,7 @@ int64
<td>
<code>ReconcileRequestStatus</code><br>
<em>
<a href="https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#ReconcileRequestStatus">
<a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#ReconcileRequestStatus">
github.com/fluxcd/pkg/apis/meta.ReconcileRequestStatus
</a>
</em>
@ -638,42 +638,11 @@ string
</em>
</td>
<td>
<em>(Optional)</em>
<p>Branch specifies that commits should be pushed to the branch
named. The branch is created using <code>.spec.checkout.branch</code> as the
starting point, if it doesn&rsquo;t already exist.</p>
</td>
</tr>
<tr>
<td>
<code>refspec</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>Refspec specifies the Git Refspec to use for a push operation.
If both Branch and Refspec are provided, then the commit is pushed
to the branch and also using the specified refspec.
For more details about Git Refspecs, see:
<a href="https://git-scm.com/book/en/v2/Git-Internals-The-Refspec">https://git-scm.com/book/en/v2/Git-Internals-The-Refspec</a></p>
</td>
</tr>
<tr>
<td>
<code>options</code><br>
<em>
map[string]string
</em>
</td>
<td>
<em>(Optional)</em>
<p>Options specifies the push options that are sent to the Git
server when performing a push operation. For details, see:
<a href="https://git-scm.com/docs/git-push#Documentation/git-push.txt---push-optionltoptiongt">https://git-scm.com/docs/git-push#Documentation/git-push.txt&mdash;push-optionltoptiongt</a></p>
</td>
</tr>
</tbody>
</table>
</div>
@ -699,7 +668,7 @@ server when performing a push operation. For details, see:
<td>
<code>secretRef</code><br>
<em>
<a href="https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
<a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
github.com/fluxcd/pkg/apis/meta.LocalObjectReference
</a>
</em>

View File

@ -1,909 +0,0 @@
<h1>Image update automation API reference v1beta2</h1>
<p>Packages:</p>
<ul class="simple">
<li>
<a href="#image.toolkit.fluxcd.io%2fv1beta2">image.toolkit.fluxcd.io/v1beta2</a>
</li>
</ul>
<h2 id="image.toolkit.fluxcd.io/v1beta2">image.toolkit.fluxcd.io/v1beta2</h2>
<p>Package v1beta2 contains API types for the image API group, version
v1beta2. The types here are concerned with automated updates to
git, based on metadata from OCI image registries gathered by the
image-reflector-controller.</p>
Resource Types:
<ul class="simple"></ul>
<h3 id="image.toolkit.fluxcd.io/v1beta2.CommitSpec">CommitSpec
</h3>
<p>
(<em>Appears on:</em>
<a href="#image.toolkit.fluxcd.io/v1beta2.GitSpec">GitSpec</a>)
</p>
<p>CommitSpec specifies how to commit changes to the git repository</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>author</code><br>
<em>
<a href="#image.toolkit.fluxcd.io/v1beta2.CommitUser">
CommitUser
</a>
</em>
</td>
<td>
<p>Author gives the email and optionally the name to use as the
author of commits.</p>
</td>
</tr>
<tr>
<td>
<code>signingKey</code><br>
<em>
<a href="#image.toolkit.fluxcd.io/v1beta2.SigningKey">
SigningKey
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>SigningKey provides the option to sign commits with a GPG key</p>
</td>
</tr>
<tr>
<td>
<code>messageTemplate</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>MessageTemplate provides a template for the commit message,
into which will be interpolated the details of the change made.
Note: The <code>Updated</code> template field has been removed. Use <code>Changed</code> instead.</p>
</td>
</tr>
<tr>
<td>
<code>messageTemplateValues</code><br>
<em>
map[string]string
</em>
</td>
<td>
<em>(Optional)</em>
<p>MessageTemplateValues provides additional values to be available to the
templating rendering.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="image.toolkit.fluxcd.io/v1beta2.CommitUser">CommitUser
</h3>
<p>
(<em>Appears on:</em>
<a href="#image.toolkit.fluxcd.io/v1beta2.CommitSpec">CommitSpec</a>)
</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>name</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>Name gives the name to provide when making a commit.</p>
</td>
</tr>
<tr>
<td>
<code>email</code><br>
<em>
string
</em>
</td>
<td>
<p>Email gives the email to provide when making a commit.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="image.toolkit.fluxcd.io/v1beta2.CrossNamespaceSourceReference">CrossNamespaceSourceReference
</h3>
<p>
(<em>Appears on:</em>
<a href="#image.toolkit.fluxcd.io/v1beta2.ImageUpdateAutomationSpec">ImageUpdateAutomationSpec</a>)
</p>
<p>CrossNamespaceSourceReference contains enough information to let you locate the
typed Kubernetes resource object at cluster level.</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>apiVersion</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>API version of the referent.</p>
</td>
</tr>
<tr>
<td>
<code>kind</code><br>
<em>
string
</em>
</td>
<td>
<p>Kind of the referent.</p>
</td>
</tr>
<tr>
<td>
<code>name</code><br>
<em>
string
</em>
</td>
<td>
<p>Name of the referent.</p>
</td>
</tr>
<tr>
<td>
<code>namespace</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>Namespace of the referent, defaults to the namespace of the Kubernetes resource object that contains the reference.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="image.toolkit.fluxcd.io/v1beta2.GitCheckoutSpec">GitCheckoutSpec
</h3>
<p>
(<em>Appears on:</em>
<a href="#image.toolkit.fluxcd.io/v1beta2.GitSpec">GitSpec</a>)
</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>ref</code><br>
<em>
<a href="https://pkg.go.dev/github.com/fluxcd/source-controller/api/v1#GitRepositoryRef">
Source /v1.GitRepositoryRef
</a>
</em>
</td>
<td>
<p>Reference gives a branch, tag or commit to clone from the Git
repository.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="image.toolkit.fluxcd.io/v1beta2.GitSpec">GitSpec
</h3>
<p>
(<em>Appears on:</em>
<a href="#image.toolkit.fluxcd.io/v1beta2.ImageUpdateAutomationSpec">ImageUpdateAutomationSpec</a>)
</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>checkout</code><br>
<em>
<a href="#image.toolkit.fluxcd.io/v1beta2.GitCheckoutSpec">
GitCheckoutSpec
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>Checkout gives the parameters for cloning the git repository,
ready to make changes. If not present, the <code>spec.ref</code> field from the
referenced <code>GitRepository</code> or its default will be used.</p>
</td>
</tr>
<tr>
<td>
<code>commit</code><br>
<em>
<a href="#image.toolkit.fluxcd.io/v1beta2.CommitSpec">
CommitSpec
</a>
</em>
</td>
<td>
<p>Commit specifies how to commit to the git repository.</p>
</td>
</tr>
<tr>
<td>
<code>push</code><br>
<em>
<a href="#image.toolkit.fluxcd.io/v1beta2.PushSpec">
PushSpec
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>Push specifies how and where to push commits made by the
automation. If missing, commits are pushed (back) to
<code>.spec.checkout.branch</code> or its default.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="image.toolkit.fluxcd.io/v1beta2.ImageRef">ImageRef
</h3>
<p>ImageRef represents an image reference.</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>name</code><br>
<em>
string
</em>
</td>
<td>
<p>Name is the bare image&rsquo;s name.</p>
</td>
</tr>
<tr>
<td>
<code>tag</code><br>
<em>
string
</em>
</td>
<td>
<p>Tag is the image&rsquo;s tag.</p>
</td>
</tr>
<tr>
<td>
<code>digest</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>Digest is the image&rsquo;s digest.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="image.toolkit.fluxcd.io/v1beta2.ImageUpdateAutomation">ImageUpdateAutomation
</h3>
<p>ImageUpdateAutomation is the Schema for the imageupdateautomations API</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>metadata</code><br>
<em>
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta">
Kubernetes meta/v1.ObjectMeta
</a>
</em>
</td>
<td>
Refer to the Kubernetes API documentation for the fields of the
<code>metadata</code> field.
</td>
</tr>
<tr>
<td>
<code>spec</code><br>
<em>
<a href="#image.toolkit.fluxcd.io/v1beta2.ImageUpdateAutomationSpec">
ImageUpdateAutomationSpec
</a>
</em>
</td>
<td>
<br/>
<br/>
<table>
<tr>
<td>
<code>sourceRef</code><br>
<em>
<a href="#image.toolkit.fluxcd.io/v1beta2.CrossNamespaceSourceReference">
CrossNamespaceSourceReference
</a>
</em>
</td>
<td>
<p>SourceRef refers to the resource giving access details
to a git repository.</p>
</td>
</tr>
<tr>
<td>
<code>git</code><br>
<em>
<a href="#image.toolkit.fluxcd.io/v1beta2.GitSpec">
GitSpec
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>GitSpec contains all the git-specific definitions. This is
technically optional, but in practice mandatory until there are
other kinds of source allowed.</p>
</td>
</tr>
<tr>
<td>
<code>interval</code><br>
<em>
<a href="https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
Kubernetes meta/v1.Duration
</a>
</em>
</td>
<td>
<p>Interval gives an lower bound for how often the automation
run should be attempted.</p>
</td>
</tr>
<tr>
<td>
<code>policySelector</code><br>
<em>
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#labelselector-v1-meta">
Kubernetes meta/v1.LabelSelector
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>PolicySelector allows to filter applied policies based on labels.
By default includes all policies in namespace.</p>
</td>
</tr>
<tr>
<td>
<code>update</code><br>
<em>
<a href="#image.toolkit.fluxcd.io/v1beta2.UpdateStrategy">
UpdateStrategy
</a>
</em>
</td>
<td>
<p>Update gives the specification for how to update the files in
the repository. This can be left empty, to use the default
value.</p>
</td>
</tr>
<tr>
<td>
<code>suspend</code><br>
<em>
bool
</em>
</td>
<td>
<em>(Optional)</em>
<p>Suspend tells the controller to not run this automation, until
it is unset (or set to false). Defaults to false.</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<code>status</code><br>
<em>
<a href="#image.toolkit.fluxcd.io/v1beta2.ImageUpdateAutomationStatus">
ImageUpdateAutomationStatus
</a>
</em>
</td>
<td>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="image.toolkit.fluxcd.io/v1beta2.ImageUpdateAutomationSpec">ImageUpdateAutomationSpec
</h3>
<p>
(<em>Appears on:</em>
<a href="#image.toolkit.fluxcd.io/v1beta2.ImageUpdateAutomation">ImageUpdateAutomation</a>)
</p>
<p>ImageUpdateAutomationSpec defines the desired state of ImageUpdateAutomation</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>sourceRef</code><br>
<em>
<a href="#image.toolkit.fluxcd.io/v1beta2.CrossNamespaceSourceReference">
CrossNamespaceSourceReference
</a>
</em>
</td>
<td>
<p>SourceRef refers to the resource giving access details
to a git repository.</p>
</td>
</tr>
<tr>
<td>
<code>git</code><br>
<em>
<a href="#image.toolkit.fluxcd.io/v1beta2.GitSpec">
GitSpec
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>GitSpec contains all the git-specific definitions. This is
technically optional, but in practice mandatory until there are
other kinds of source allowed.</p>
</td>
</tr>
<tr>
<td>
<code>interval</code><br>
<em>
<a href="https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
Kubernetes meta/v1.Duration
</a>
</em>
</td>
<td>
<p>Interval gives an lower bound for how often the automation
run should be attempted.</p>
</td>
</tr>
<tr>
<td>
<code>policySelector</code><br>
<em>
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#labelselector-v1-meta">
Kubernetes meta/v1.LabelSelector
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>PolicySelector allows to filter applied policies based on labels.
By default includes all policies in namespace.</p>
</td>
</tr>
<tr>
<td>
<code>update</code><br>
<em>
<a href="#image.toolkit.fluxcd.io/v1beta2.UpdateStrategy">
UpdateStrategy
</a>
</em>
</td>
<td>
<p>Update gives the specification for how to update the files in
the repository. This can be left empty, to use the default
value.</p>
</td>
</tr>
<tr>
<td>
<code>suspend</code><br>
<em>
bool
</em>
</td>
<td>
<em>(Optional)</em>
<p>Suspend tells the controller to not run this automation, until
it is unset (or set to false). Defaults to false.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="image.toolkit.fluxcd.io/v1beta2.ImageUpdateAutomationStatus">ImageUpdateAutomationStatus
</h3>
<p>
(<em>Appears on:</em>
<a href="#image.toolkit.fluxcd.io/v1beta2.ImageUpdateAutomation">ImageUpdateAutomation</a>)
</p>
<p>ImageUpdateAutomationStatus defines the observed state of ImageUpdateAutomation</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>lastAutomationRunTime</code><br>
<em>
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#time-v1-meta">
Kubernetes meta/v1.Time
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>LastAutomationRunTime records the last time the controller ran
this automation through to completion (even if no updates were
made).</p>
</td>
</tr>
<tr>
<td>
<code>lastPushCommit</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>LastPushCommit records the SHA1 of the last commit made by the
controller, for this automation object</p>
</td>
</tr>
<tr>
<td>
<code>lastPushTime</code><br>
<em>
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#time-v1-meta">
Kubernetes meta/v1.Time
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>LastPushTime records the time of the last pushed change.</p>
</td>
</tr>
<tr>
<td>
<code>observedGeneration</code><br>
<em>
int64
</em>
</td>
<td>
<em>(Optional)</em>
</td>
</tr>
<tr>
<td>
<code>conditions</code><br>
<em>
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#condition-v1-meta">
[]Kubernetes meta/v1.Condition
</a>
</em>
</td>
<td>
<em>(Optional)</em>
</td>
</tr>
<tr>
<td>
<code>observedPolicies</code><br>
<em>
<a href="#image.toolkit.fluxcd.io/v1beta2.ObservedPolicies">
ObservedPolicies
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>ObservedPolicies is the list of observed ImagePolicies that were
considered by the ImageUpdateAutomation update process.</p>
</td>
</tr>
<tr>
<td>
<code>observedSourceRevision</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>ObservedPolicies []ObservedPolicy <code>json:&quot;observedPolicies,omitempty&quot;</code>
ObservedSourceRevision is the last observed source revision. This can be
used to determine if the source has been updated since last observation.</p>
</td>
</tr>
<tr>
<td>
<code>ReconcileRequestStatus</code><br>
<em>
<a href="https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#ReconcileRequestStatus">
github.com/fluxcd/pkg/apis/meta.ReconcileRequestStatus
</a>
</em>
</td>
<td>
<p>
(Members of <code>ReconcileRequestStatus</code> are embedded into this type.)
</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="image.toolkit.fluxcd.io/v1beta2.ObservedPolicies">ObservedPolicies
(<code>map[string]./api/v1beta2.ImageRef</code> alias)</h3>
<p>
(<em>Appears on:</em>
<a href="#image.toolkit.fluxcd.io/v1beta2.ImageUpdateAutomationStatus">ImageUpdateAutomationStatus</a>)
</p>
<p>ObservedPolicies is a map of policy name and ImageRef of their latest
ImageRef.</p>
<h3 id="image.toolkit.fluxcd.io/v1beta2.PushSpec">PushSpec
</h3>
<p>
(<em>Appears on:</em>
<a href="#image.toolkit.fluxcd.io/v1beta2.GitSpec">GitSpec</a>)
</p>
<p>PushSpec specifies how and where to push commits.</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>branch</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>Branch specifies that commits should be pushed to the branch
named. The branch is created using <code>.spec.checkout.branch</code> as the
starting point, if it doesn&rsquo;t already exist.</p>
</td>
</tr>
<tr>
<td>
<code>refspec</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>Refspec specifies the Git Refspec to use for a push operation.
If both Branch and Refspec are provided, then the commit is pushed
to the branch and also using the specified refspec.
For more details about Git Refspecs, see:
<a href="https://git-scm.com/book/en/v2/Git-Internals-The-Refspec">https://git-scm.com/book/en/v2/Git-Internals-The-Refspec</a></p>
</td>
</tr>
<tr>
<td>
<code>options</code><br>
<em>
map[string]string
</em>
</td>
<td>
<em>(Optional)</em>
<p>Options specifies the push options that are sent to the Git
server when performing a push operation. For details, see:
<a href="https://git-scm.com/docs/git-push#Documentation/git-push.txt---push-optionltoptiongt">https://git-scm.com/docs/git-push#Documentation/git-push.txt&mdash;push-optionltoptiongt</a></p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="image.toolkit.fluxcd.io/v1beta2.SigningKey">SigningKey
</h3>
<p>
(<em>Appears on:</em>
<a href="#image.toolkit.fluxcd.io/v1beta2.CommitSpec">CommitSpec</a>)
</p>
<p>SigningKey references a Kubernetes secret that contains a GPG keypair</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>secretRef</code><br>
<em>
<a href="https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
github.com/fluxcd/pkg/apis/meta.LocalObjectReference
</a>
</em>
</td>
<td>
<p>SecretRef holds the name to a secret that contains a &lsquo;git.asc&rsquo; key
corresponding to the ASCII Armored file containing the GPG signing
keypair as the value. It must be in the same namespace as the
ImageUpdateAutomation.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="image.toolkit.fluxcd.io/v1beta2.UpdateStrategy">UpdateStrategy
</h3>
<p>
(<em>Appears on:</em>
<a href="#image.toolkit.fluxcd.io/v1beta2.ImageUpdateAutomationSpec">ImageUpdateAutomationSpec</a>)
</p>
<p>UpdateStrategy is a union of the various strategies for updating
the Git repository. Parameters for each strategy (if any) can be
inlined here.</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>strategy</code><br>
<em>
<a href="#image.toolkit.fluxcd.io/v1beta2.UpdateStrategyName">
UpdateStrategyName
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>Strategy names the strategy to be used.</p>
</td>
</tr>
<tr>
<td>
<code>path</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>Path to the directory containing the manifests to be updated.
Defaults to &lsquo;None&rsquo;, which translates to the root path
of the GitRepositoryRef.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="image.toolkit.fluxcd.io/v1beta2.UpdateStrategyName">UpdateStrategyName
(<code>string</code> alias)</h3>
<p>
(<em>Appears on:</em>
<a href="#image.toolkit.fluxcd.io/v1beta2.UpdateStrategy">UpdateStrategy</a>)
</p>
<p>UpdateStrategyName is the type for names that go in
.update.strategy. NB the value in the const immediately below.</p>
<div class="admonition note">
<p class="last">This page was automatically generated with <code>gen-crd-api-reference-docs</code></p>
</div>

View File

@ -368,8 +368,8 @@ There is one condition maintained by the controller, which is the usual `ReadyCo
condition. This will be recorded as `True` when automation has run without errors, whether or not it
resulted in a commit.
[image-auto-guide]: https://fluxcd.io/flux/guides/image-update/#configure-image-update-for-custom-resources
[git-repo-ref]: https://fluxcd.io/flux/components/source/gitrepositories/
[image-auto-guide]: https://toolkit.fluxcd.io/guides/image-update/#configure-image-update-for-custom-resources
[git-repo-ref]: https://toolkit.fluxcd.io/components/source/gitrepositories/
[durations]: https://godoc.org/time#ParseDuration
[source-docs]: https://fluxcd.io/flux/components/source/gitrepositories/#git-implementation
[source-docs]: https://toolkit.fluxcd.io/components/source/gitrepositories/#git-implementation
[go-text-template]: https://golang.org/pkg/text/template/

View File

@ -591,8 +591,8 @@ spec:
branch: auto
```
[image-auto-guide]: https://fluxcd.io/flux/guides/image-update/#configure-image-update-for-custom-resources
[git-repo-ref]: https://fluxcd.io/flux/components/source/gitrepositories/#specification
[image-auto-guide]: https://toolkit.fluxcd.io/guides/image-update/#configure-image-update-for-custom-resources
[git-repo-ref]: https://toolkit.fluxcd.io/components/source/gitrepositories/#specification
[durations]: https://godoc.org/time#ParseDuration
[source-docs]: https://fluxcd.io/flux/components/source/gitrepositories/#git-implementation
[source-docs]: https://toolkit.fluxcd.io/components/source/gitrepositories/#git-implementation
[go-text-template]: https://golang.org/pkg/text/template/

View File

@ -1,8 +1,6 @@
<!-- -*- fill-column: 100 -*- -->
# Image Update Automations
<!-- menuweight:50 -->
The `ImageUpdateAutomation` type defines an automation process that will update a git repository,
based on image policy objects in the same namespace.
@ -113,8 +111,9 @@ To be able to commit changes back, the referenced `GitRepository` object must re
with write access; e.g., if using a GitHub deploy key, "Allow write access" should be checked when
creating it. Only the `url`, `ref`, and `secretRef` fields of the `GitRepository` are used.
The [`gitImplementation` field][source-docs] in the referenced `GitRepository` is ignored. All
reconciliations are executed using the `go-git` implementation.
The [`gitImplementation` field][source-docs] in the referenced `GitRepository` is ignored. The
automation controller cannot use shallow clones or submodules, so there is no reason to use the
go-git implementation rather than libgit2.
Other fields particular to how the Git repository is used are in the `git` field, [described
below](#git-specific-specification).
@ -170,9 +169,6 @@ When `checkout` is given, it overrides the analogous field in the `GitRepository
in `.spec.sourceRef`. You would use this to put automation commits on a different branch than that
you are syncing, for example.
By default the controller will only do shallow clones, but this can be disabled by starting the controller
with `--feature-gates=GitShallowClone=false`.
### Commit
The `.spec.git.commit` field gives details to use when making a commit to push to the Git repository:
@ -228,21 +224,7 @@ will result in commits with the author `Fluxbot <flux@example.com>`.
The optional `signingKey` field can be used to provide a key to sign commits with. It holds a
reference to a secret, which is expected to have a file called `git.asc` containing an
ASCII-armoured PGP key. If the private key is protected by a password, you can specify the same
in the secret using the `passphrase` key.
```yaml
---
apiVersion: v1
kind: Secret
metadata:
name: signing-key
namespace: default
stringData:
git.asc: |
<ARMOR ENCODED PGP KEY>
passphrase: <private-key-passphrase>
```
ASCII-armoured PGP key.
The `messageTemplate` field is a string which will be used as a template for the commit message. If
empty, there is a default message; but you will likely want to provide your own, especially if you
@ -266,7 +248,7 @@ The message template is a [Go text template][go-text-template]. The data availab
have this structure (not reproduced verbatim):
```go
// internal/controller/imageupdateautomation_controller.go
// controllers/imageupdateautomation_controller.go
// TemplateData is the type of the value given to the commit message
// template.
@ -398,7 +380,6 @@ spec:
name: fluxcdbot
```
There are over 70 available functions. Some of them are defined by the [Go template language](https://pkg.go.dev/text/template) itself. Most of the others are part of the [Sprig template library](http://masterminds.github.io/sprig/).
### Push
The optional `push` field defines how commits are pushed to the origin.
@ -409,51 +390,19 @@ type PushSpec struct {
// Branch specifies that commits should be pushed to the branch
// named. The branch is created using `.spec.checkout.branch` as the
// starting point, if it doesn't already exist.
// +optional
Branch string `json:"branch,omitempty"`
// Refspec specifies the Git Refspec to use for a push operation.
// If both Branch and Refspec are provided, then the commit is pushed
// to the branch and also using the specified refspec.
// For more details about Git Refspecs, see:
// https://git-scm.com/book/en/v2/Git-Internals-The-Refspec
// +optional
Refspec string `json:"refspec,omitempty"`
// Options specifies the push options that are sent to the Git
// server when performing a push operation. For details, see:
// https://git-scm.com/docs/git-push#Documentation/git-push.txt---push-optionltoptiongt
Options map[string]string `json:"options,omitempty"`
// +required
Branch string `json:"branch"`
}
```
If `.push` is not present, commits are made on the branch given in `.spec.git.checkout.branch` and
If `push` is not present, commits are made on the branch given in `.spec.git.checkout.branch` and
pushed to the same branch at the origin. If `.spec.git.checkout` is not present, it will fall back
to the branch given in the `GitRepository` referenced by `.spec.sourceRef`. If none of these yield a
branch name, the automation will fail.
If `.push.refspec` is present, the refspec specified is used to perform the push operation.
An example of a valid refspec is `refs/heads/branch:refs/heads/branch`. This allows users to
push to an arbitary destination reference.
If `.push.branch` is present, the specified branch is pushed to at the origin. The branch
When `push` is present, the `branch` field specifies a branch to push to at the origin. The branch
will be created locally if it does not already exist, starting from the checkout branch. If it does
already exist, it will be overwritten with the cloned version plus the changes made by the
controller. Alternatively, force push can be disabled by starting the controller with `--feature-gates=GitForcePushBranch=false`,
in which case the updates will be calculated on top of any commits already on the push branch.
Note that without force push in push branches, if the target branch is stale, the controller may not
be able to conclude the operation and will consistently fail until the branch is either deleted or
refreshed.
If both `.push.refspec` and `.push.branch` are specified, then the reconciler will perform
two push operations, one to the specified branch and another using the specified refspec.
This is particularly useful for working with Gerrit servers. For more information about this,
please refer to the [Gerrit](#gerrit) section.
**Note:** If both `.push.refspec` and `.push.branch` are essentially equal to
each other (for e.g.: `.push.refspec: refs/heads/main:refs/heads/main` and
`.push.branch: main`), then the reconciler might fail to perform the second push
operation and error out with an `already up-to-date` error.
already exist, updates will be calculated on top of any commits already on the branch.
In the following snippet, updates will be pushed as commits to the branch `auto`, and when that
branch does not exist at the origin, it will be created locally starting from the branch `main`, and
@ -469,117 +418,6 @@ spec:
branch: auto
```
In the following snippet, updates and commits will be made on the `main` branch locally.
The commits will be then pushed using the `refs/heads/main:refs/heads/auto` refspec:
```yaml
spec:
git:
checkout:
ref:
branch: main
push:
refspec: refs/heads/main:refs/heads/auto
```
To specify the [push options](https://git-scm.com/docs/git-push#Documentation/git-push.txt---push-optionltoptiongt)
to be sent to the upstream Git server, use `.push.options`. These options can be
used to perform operations as a result of the push. For example, using the below
push options will open a GitLab Merge Request to the `release` branch
automatically with the commit the controller pushed to the `dev` branch:
```yaml
spec:
git:
push:
branch: dev
options:
merge_request.create: ""
merge_request.target: release
```
#### Gerrit
[Gerrit](https://www.gerritcodereview.com/) operates differently from a
standard Git server. Rather than sending individual commits to a branch,
all changes are bundled into a single commit. This commit requires a distinct
identifier separate from the commit SHA. Additionally, instead of initiating
a Pull Request between branches, the commit is pushed using a refspec:
`HEAD:refs/for/main`.
As the image-automation-controller is primarily designed to work with
standard Git servers, these special characteristics necessitate a few
workarounds. The following is an example configuration that works
well with Gerrit:
```yaml
spec:
git:
checkout:
ref:
branch: main
commit:
author:
email: flux@localdomain
name: flux
messageTemplate: |
Perform automatic image update
Automation name: {{ .AutomationObject }}
Files:
{{ range $filename, $_ := .Updated.Files -}}
- {{ $filename }}
{{ end }}
Objects:
{{ range $resource, $_ := .Updated.Objects -}}
- {{ $resource.Kind }} {{ $resource.Name }}
{{ end }}
Images:
{{ range .Updated.Images -}}
- {{ . }}
{{ end }}
{{- $ChangeId := .AutomationObject -}}
{{- $ChangeId = printf "%s%s" $ChangeId ( .Updated.Files | toString ) -}}
{{- $ChangeId = printf "%s%s" $ChangeId ( .Updated.Objects | toString ) -}}
{{- $ChangeId = printf "%s%s" $ChangeId ( .Updated.Images | toString ) }}
Change-Id: {{ printf "I%s" ( sha256sum $ChangeId | trunc 40 ) }}
push:
branch: auto
refspec: refs/heads/auto:refs/heads/main
```
This instructs the image-automation-controller to clone the repository using the
`main` branch but execute its update logic and commit with the provided message
template on the `auto` branch. Commits are then pushed to the `auto` branch,
followed by pushing the `HEAD` of the `auto` branch to the `HEAD` of the remote
`main` branch. The message template ensures the inclusion of a [Change-Id](https://gerrit-review.googlesource.com/Documentation/concept-changes.html#change-id)
at the bottom of the commit message.
The initial branch push aims to prevent multiple
[Patch Sets](https://gerrit-review.googlesource.com/Documentation/concept-patch-sets.html).
If we exclude `.push.branch` and only specify
`.push.refspec: refs/heads/main:refs/heads/main`, the desired [Change](https://gerrit-review.googlesource.com/Documentation/concept-changes.html)
can be created as intended. However, when the controller freshly clones the
`main` branch while a Change is open, it executes its update logic on `main`,
leading to new commits being pushed with the same changes to the existing open
Change. Specifying `.push.branch` circumvents this by instructing the controller
to apply the update logic to the `auto` branch, already containing the desired
commit. This approach is also recommended in the
[Gerrit documentation](https://gerrit-review.googlesource.com/Documentation/intro-gerrit-walkthrough-github.html#create-change).
Another thing to note is the syntax of `.push.refspec`. Instead of it being
`HEAD:refs/for/main`, commonly used by Gerrit users, we specify the full
refname `refs/heads/auto` in the source part of the refpsec.
**Note:** A known limitation of using the image-automation-controller with
Gerrit involves handling multiple concurrent Changes. This is due to the
calculation of the Change-Id, relying on factors like file names and image
tags. If the controller introduces a new file or modifies a previously updated
image tag to a different one, it leads to a distinct Change-Id for the commit.
Consequently, this action will trigger the creation of an additional Change,
even when an existing Change containing outdated modifications remains open.
## Update strategy
The `.spec.update` field specifies how to carry out updates on the git repository. There is one
@ -830,8 +668,8 @@ spec:
branch: auto
```
[image-auto-guide]: https://fluxcd.io/flux/guides/image-update/#configure-image-update-for-custom-resources
[git-repo-ref]: https://fluxcd.io/flux/components/source/gitrepositories/#writing-a-gitrepository-spec
[image-auto-guide]: https://toolkit.fluxcd.io/guides/image-update/#configure-image-update-for-custom-resources
[git-repo-ref]: https://toolkit.fluxcd.io/components/source/gitrepositories/#specification
[durations]: https://godoc.org/time#ParseDuration
[source-docs]: https://fluxcd.io/flux/components/source/api/v1beta2/#source.toolkit.fluxcd.io/v1beta2.GitRepositorySpec
[source-docs]: https://toolkit.fluxcd.io/components/source/gitrepositories/#git-implementation
[go-text-template]: https://golang.org/pkg/text/template/

File diff suppressed because it is too large Load Diff

284
go.mod
View File

@ -1,200 +1,126 @@
module github.com/fluxcd/image-automation-controller
go 1.25.0
go 1.17
replace github.com/fluxcd/image-automation-controller/api => ./api
// Pin kustomize to v5.7.1
replace (
sigs.k8s.io/kustomize/api => sigs.k8s.io/kustomize/api v0.20.1
sigs.k8s.io/kustomize/kyaml => sigs.k8s.io/kustomize/kyaml v0.20.1
require (
github.com/Masterminds/sprig/v3 v3.2.2
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7
github.com/cyphar/filepath-securejoin v0.2.2
github.com/fluxcd/image-automation-controller/api v0.20.0
// If you bump this, change REFLECTOR_VER in the Makefile to match
github.com/fluxcd/image-reflector-controller/api v0.16.0
github.com/fluxcd/pkg/apis/acl v0.0.3
github.com/fluxcd/pkg/apis/meta v0.10.2
github.com/fluxcd/pkg/gittestserver v0.5.0
github.com/fluxcd/pkg/runtime v0.12.4
github.com/fluxcd/pkg/ssh v0.2.0
// If you bump this, change SOURCE_VER in the Makefile to match
github.com/fluxcd/source-controller v0.21.1
github.com/fluxcd/source-controller/api v0.21.1
github.com/go-git/go-billy/v5 v5.3.1
github.com/go-git/go-git/v5 v5.4.2
github.com/go-logr/logr v1.2.2
github.com/google/go-containerregistry v0.6.0
github.com/libgit2/git2go/v31 v31.7.6
github.com/onsi/ginkgo v1.16.5
github.com/onsi/gomega v1.17.0
github.com/otiai10/copy v1.7.0
github.com/spf13/pflag v1.0.5
k8s.io/api v0.23.1
k8s.io/apimachinery v0.23.1
k8s.io/client-go v0.23.1
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65
sigs.k8s.io/controller-runtime v0.11.0
sigs.k8s.io/kustomize/kyaml v0.13.0
)
require (
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6
github.com/Masterminds/sprig/v3 v3.3.0
github.com/ProtonMail/go-crypto v1.3.0
github.com/cyphar/filepath-securejoin v0.4.1
github.com/fluxcd/image-automation-controller/api v0.41.2
github.com/fluxcd/image-reflector-controller/api v0.35.2
github.com/fluxcd/pkg/apis/acl v0.9.0
github.com/fluxcd/pkg/apis/event v0.19.0
github.com/fluxcd/pkg/apis/meta v1.20.0
github.com/fluxcd/pkg/auth v0.29.0
github.com/fluxcd/pkg/cache v0.11.0
github.com/fluxcd/pkg/git v0.36.0
github.com/fluxcd/pkg/git/gogit v0.40.0
github.com/fluxcd/pkg/gittestserver v0.20.0
github.com/fluxcd/pkg/runtime v0.82.0
github.com/fluxcd/pkg/ssh v0.21.0
github.com/fluxcd/source-controller/api v1.6.2
github.com/go-git/go-billy/v5 v5.6.2
github.com/go-git/go-git/v5 v5.16.2
github.com/go-logr/logr v1.4.3
github.com/google/go-containerregistry v0.20.6
github.com/onsi/gomega v1.38.2
github.com/otiai10/copy v1.14.1
github.com/spf13/pflag v1.0.7
k8s.io/api v0.34.0
k8s.io/apimachinery v0.34.0
k8s.io/client-go v0.34.0
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d
sigs.k8s.io/controller-runtime v0.22.0
sigs.k8s.io/kustomize/kyaml v0.20.1
)
require (
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.8.0 // indirect
dario.cat/mergo v1.0.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
cloud.google.com/go v0.93.3 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/aws/aws-sdk-go-v2 v1.38.3 // indirect
github.com/aws/aws-sdk-go-v2/config v1.31.6 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ecr v1.50.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.37.2 // indirect
github.com/aws/aws-sdk-go-v2/service/eks v1.73.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 // indirect
github.com/aws/smithy-go v1.23.0 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/Microsoft/go-winio v0.5.0 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/bradleyfalzon/ghinstallation/v2 v2.16.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chai2010/gettext-go v1.0.3 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/docker/cli v28.2.2+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.3 // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fluxcd/cli-utils v0.36.0-flux.15 // indirect
github.com/fluxcd/gitkit v0.6.0 // indirect
github.com/fluxcd/pkg/version v0.10.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.1 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/fluxcd/pkg/gitutil v0.1.0 // indirect
github.com/fluxcd/pkg/version v0.1.0 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-logr/zapr v1.2.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.5 // indirect
github.com/go-openapi/swag v0.19.14 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-github/v72 v72.0.0 // indirect
github.com/google/go-querystring v1.1.0 // 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/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.2.0 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/hashicorp/go-cleanhttp v0.5.1 // indirect
github.com/hashicorp/go-retryablehttp v0.6.8 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mitchellh/copystructure v1.1.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/spdystream v0.5.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/mitchellh/reflectwalk v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // 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/otiai10/mint v1.6.3 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pjbgf/sha1cd v0.4.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/spf13/cast v1.8.0 // indirect
github.com/spf13/cobra v1.9.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.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.41.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/term v0.34.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/time v0.12.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
google.golang.org/api v0.248.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
google.golang.org/grpc v1.74.2 // indirect
google.golang.org/protobuf v1.36.7 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.11.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.28.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
github.com/satori/go.uuid v1.2.0 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/sosedoff/gitkit v0.2.1-0.20200818155723-72ebbcf5056d // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/stretchr/testify v1.7.0 // indirect
github.com/xanzy/ssh-agent v0.3.0 // indirect
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.19.1 // indirect
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
golang.org/x/net v0.0.0-20211215060638-4ddde0e984e9 // indirect
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect
golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8 // indirect
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.34.0 // indirect
k8s.io/cli-runtime v0.34.0 // indirect
k8s.io/component-base v0.34.0 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kubectl v0.34.0 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/kustomize/api v0.20.1 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
k8s.io/apiextensions-apiserver v0.23.0 // indirect
k8s.io/component-base v0.23.0 // indirect
k8s.io/klog/v2 v2.30.0 // indirect
k8s.io/utils v0.0.0-20211208161948-7d6a63dca704 // indirect
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.0 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)

1984
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@
"externalPackages": [
{
"typeMatchPrefix": "^k8s\\.io/apimachinery/pkg/apis/meta/v1\\.Duration$",
"docsURLTemplate": "https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration"
"docsURLTemplate": "https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#Duration"
},
{
"typeMatchPrefix": "^k8s\\.io/(api|apimachinery/pkg/apis)/",
@ -17,15 +17,15 @@
},
{
"typeMatchPrefix": "^github.com/fluxcd/pkg/runtime/dependency\\.CrossNamespaceDependencyReference$",
"docsURLTemplate": "https://pkg.go.dev/github.com/fluxcd/pkg/runtime/dependency#CrossNamespaceDependencyReference"
"docsURLTemplate": "https://godoc.org/github.com/fluxcd/pkg/runtime/dependency#CrossNamespaceDependencyReference"
},
{
"typeMatchPrefix": "^github.com/fluxcd/pkg/apis/meta",
"docsURLTemplate": "https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#{{ .TypeIdentifier }}"
"docsURLTemplate": "https://godoc.org/github.com/fluxcd/pkg/apis/meta#{{ .TypeIdentifier }}"
},
{
"typeMatchPrefix": "^github.com/fluxcd/source-controller/api/v1",
"docsURLTemplate": "https://pkg.go.dev/github.com/fluxcd/source-controller/api/v1#{{ .TypeIdentifier }}"
"typeMatchPrefix": "^github.com/fluxcd/source-controller/api/v1beta1",
"docsURLTemplate": "https://godoc.org/github.com/fluxcd/source-controller/api/v1beta1#{{ .TypeIdentifier }}"
}
],
"typeDisplayNamePrefixOverrides": {

View File

@ -1,10 +1,5 @@
{{ define "packages" }}
<h1>Image update automation API reference
{{- with (index .packages 0) -}}
{{ with (index .GoPackages 0 ) -}}
{{ printf " %s" .Name -}}
{{ end -}}
{{ end }}</h1>
<h1>Image update automation API reference</h1>
{{ with .packages}}
<p>Packages:</p>

View File

@ -1,5 +1,5 @@
/*
Copyright 2025 The Flux authors
Copyright 2020 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

71
hack/download-musl.sh Executable file
View File

@ -0,0 +1,71 @@
#!/usr/bin/env bash
set -eoux pipefail
MUSL_X86_64_FILENAME=x86_64-linux-musl-native.tgz
MUSL_X86_64_SHA512=44d441ad9aa11a06feddf3daa4c9f53ad7d9ca37af1f5a61379aca07793703d179410cea723c1b7fca94c4de19a321228bdb3656bc5cbdb5e3bea8e2d6dac6c7
MUSL_AARCH64_FILENAME=aarch64-linux-musl-native.tgz
MUSL_AARCH64_SHA512=16d544e09845c9dbba50f29e0cb04dd661e17eb63c56acad6a67fd2a78aa7596b792477c7177d3cd56d408a27dc291a90507df882f2b099c0f25511ce08fd3b5
MUSL_XX86_64_FILENAME=x86_64-linux-musl-cross.tgz
MUSL_XX86_64_SHA512=52abd1a56e670952116e35d1a62e048a9b6160471d988e16fa0e1611923dd108a581d2e00874af5eb04e4968b1ba32e0eb449a1f15c3e4d5240ebe09caf5a9f3
MUSL_XAARCH64_FILENAME=aarch64-linux-musl-cross.tgz
MUSL_XAARCH64_SHA512=8695ff86979cdf30fbbcd33061711f5b1ebc3c48a87822b9ca56cde6d3a22abd4dab30fdcd1789ac27c6febbaeb9e5bde59d79d66552fae53d54cc1377a19272
MUSL_XARMV7_FILENAME=armv7l-linux-musleabihf-cross.tgz
MUSL_XARMV7_SHA512=1bb399a61da425faac521df9b8d303e60ad101f6c7827469e0b4bc685ce1f3dedc606ac7b1e8e34d79f762a3bfe3e8ab479a97e97d9f36fbd9fc5dc9d7ed6fd1
TARGET_ARCH="${TARGET_ARCH:-$(uname -m)}"
ENV_FILE="${ENV_FILE:-false}"
MUSL_FILENAME=""
MUSL_SHA512=""
ROOT_DIR="${ROOT_DIR:-$(git rev-parse --show-toplevel)}"
MUSL_DIR="${ROOT_DIR}/build/musl"
if [ "${TARGET_ARCH}" = "$(uname -m)" ]; then
MUSL_FILENAME="${MUSL_X86_64_FILENAME}"
MUSL_SHA512="${MUSL_X86_64_SHA512}"
MUSL_PREFIX="${TARGET_ARCH}-linux-musl-native/bin/${TARGET_ARCH}-linux-musl"
if [ "${TARGET_ARCH}" = "arm64" ] || [ "${TARGET_ARCH}" = "aarch64" ]; then
MUSL_FILENAME="${MUSL_AARCH64_FILENAME}"
MUSL_SHA512="${MUSL_AARCH64_SHA512}"
fi
else
MUSL_FILENAME="${MUSL_XX86_64_FILENAME}"
MUSL_SHA512="${MUSL_XX86_64_SHA512}"
MUSL_PREFIX="${TARGET_ARCH}-linux-musl-cross/bin/${TARGET_ARCH}-linux-musl"
if [ "${TARGET_ARCH}" = "arm64" ] || [ "${TARGET_ARCH}" = "aarch64" ]; then
MUSL_FILENAME="${MUSL_XAARCH64_FILENAME}"
MUSL_SHA512="${MUSL_XAARCH64_SHA512}"
elif [ "${TARGET_ARCH}" = "arm" ] || [ "${TARGET_ARCH}" = "armv7" ]; then
MUSL_FILENAME="${MUSL_XARMV7_FILENAME}"
MUSL_SHA512="${MUSL_XARMV7_SHA512}"
MUSL_PREFIX=armv7l-linux-musleabihf-cross/bin/armv7l-linux-musleabihf
fi
fi
mkdir -p "${MUSL_DIR}"
if "${ENV_FILE}"; then
cat<<EOF > "${MUSL_DIR}/${TARGET_ARCH}.env"
CC="$(pwd)/build/musl/${MUSL_PREFIX}-gcc"
CXX="$(pwd)/build/musl/${MUSL_PREFIX}-g++"
AR="$(pwd)/build/musl/${MUSL_PREFIX}-ar"
EOF
fi
MUSL_AARCH64_URL="https://more.musl.cc/11.2.1/x86_64-linux-musl/${MUSL_FILENAME}"
if [ ! -f "${MUSL_DIR}/bin" ]; then
TARGET_FILE="${MUSL_DIR}/${MUSL_FILENAME}"
curl -o "${TARGET_FILE}" -LO "${MUSL_AARCH64_URL}"
if ! echo "${MUSL_SHA512} ${TARGET_FILE}" | sha512sum; then
echo "Checksum failed for ${MUSL_FILENAME}."
rm -rf "${MUSL_DIR}"
exit 1
fi
tar xzf "${TARGET_FILE}" -C "${MUSL_DIR}"
rm "${TARGET_FILE}"
fi

66
hack/extract-libraries.sh Executable file
View File

@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -euxo pipefail
IMG_TAG="${IMG_TAG:-.}"
function extract(){
PLATFORM=$1
DIR=$2
id=$(docker create --platform="${PLATFORM}" "${IMG_TAG}")
docker cp "${id}":/usr/local - > output.tar.gz
docker rm -v "${id}"
tar -xf output.tar.gz "local/${DIR}"
rm output.tar.gz
}
function setup() {
PLATFORM=$1
DIR=$2
extract "${PLATFORM}" "${DIR}"
NEW_DIR="$(/bin/pwd)/build/libgit2"
INSTALLED_DIR="/usr/local/${DIR}"
mkdir -p "./build"
# Make a few movements to account for the change in
# behaviour in tar between MacOS and Linux
mv "local/${DIR}/" "libgit2"
rm -rf "local"
mv "libgit2/" "./build/"
# Update the prefix paths included in the .pc files.
# This will make it easier to update to the location in which they will be used.
if [[ $OSTYPE == 'darwin'* ]]; then
# sed has a sight different behaviour in MacOS
find "${NEW_DIR}" -type f -name "*.pc" | xargs -I {} sed -i "" "s;${INSTALLED_DIR};${NEW_DIR};g" {}
else
find "${NEW_DIR}" -type f -name "*.pc" | xargs -I {} sed -i "s;${INSTALLED_DIR};${NEW_DIR};g" {}
fi
}
function setup_current() {
if [ -d "./build/libgit2" ]; then
echo "Skipping libgit2 setup as it already exists"
exit 0
fi
DIR="x86_64-alpine-linux-musl"
PLATFORM="linux/amd64"
if [[ "$(uname -m)" == armv7* ]]; then
DIR="armv7-alpine-linux-musleabihf"
PLATFORM="linux/arm/v7"
elif [ "$(uname -m)" = "arm64" ] || [ "$(uname -m)" = "aarch64" ]; then
DIR="aarch64-alpine-linux-musl"
PLATFORM="linux/arm64"
fi
setup "${PLATFORM}" "${DIR}"
}
setup_current

97
hack/install-libraries.sh Executable file
View File

@ -0,0 +1,97 @@
#!/usr/bin/env bash
set -euxo pipefail
IMG="${IMG:-}"
TAG="${TAG:-}"
IMG_TAG="${IMG}:${TAG}"
function extract(){
PLATFORM=$1
DIR=$2
id=$(docker create --platform="${PLATFORM}" "${IMG_TAG}" sh)
docker cp "${id}":/usr/local - > output.tar.gz
docker rm -v "${id}"
tar -xf output.tar.gz "local/${DIR}"
rm output.tar.gz
}
function setup() {
PLATFORM=$1
DIR=$2
extract "${PLATFORM}" "${DIR}"
NEW_DIR="$(/bin/pwd)/build/libgit2/${TAG}"
INSTALLED_DIR="/usr/local/${DIR}"
mv "local/${DIR}" "${TAG}"
rm -rf "local"
mv "${TAG}/" "./build/libgit2"
# Update the prefix paths included in the .pc files.
# This will make it easier to update to the location in which they will be used.
find "${NEW_DIR}" -type f -name "*.pc" | xargs -I {} sed -i "s;${INSTALLED_DIR};${NEW_DIR};g" {}
}
function setup_current() {
if [ -d "./build/libgit2/${TAG}" ]; then
echo "Skipping libgit2 setup as it already exists"
exit 0
fi
mkdir -p "./build/libgit2"
if [[ $OSTYPE == 'darwin'* ]]; then
# For MacOS development environments, download the amd64 static libraries released from from golang-with-libgit2.
curl -o output.tar.gz -LO "https://github.com/fluxcd/golang-with-libgit2/releases/download/${TAG}/darwin-libs.tar.gz"
DIR=libgit2-darwin
NEW_DIR="$(/bin/pwd)/build/libgit2/${TAG}"
INSTALLED_DIR="/Users/runner/work/golang-with-libgit2/golang-with-libgit2/build/${DIR}-amd64"
tar -xf output.tar.gz
rm output.tar.gz
mv "${DIR}" "${TAG}"
mv "${TAG}/" "./build/libgit2"
LIBGIT2_SED="s;-L/Applications/Xcode_.* ;;g"
LIBGIT2PC="$(/bin/pwd)/build/libgit2/${TAG}/lib/pkgconfig/libgit2.pc"
# Some macOS users may override their sed with gsed. If gsed is the PATH, use that instead.
if command -v gsed &> /dev/null; then
# Removes abs path from build machine, and let iconv be resolved automatically by default search paths.
gsed -i "${LIBGIT2_SED}" "${LIBGIT2PC}"
# Update the prefix paths included in the .pc files.
# This will make it easier to update to the location in which they will be used.
# sed has a sight different behaviour in MacOS
find "${NEW_DIR}" -type f -name "*.pc" | xargs -I {} gsed -i "s;${INSTALLED_DIR};${NEW_DIR};g" {}
else
# Removes abs path from build machine, and let iconv be resolved automatically by default search paths.
sed -i "" "${LIBGIT2_SED}" "${LIBGIT2PC}"
# Update the prefix paths included in the .pc files.
# This will make it easier to update to the location in which they will be used.
# sed has a sight different behaviour in MacOS
find "${NEW_DIR}" -type f -name "*.pc" | xargs -I {} sed -i "" "s;${INSTALLED_DIR};${NEW_DIR};g" {}
fi
else
# for linux development environments, use the static libraries from the official container images.
DIR="x86_64-alpine-linux-musl"
PLATFORM="linux/amd64"
if [[ "$(uname -m)" == armv7* ]]; then
DIR="armv7-alpine-linux-musleabihf"
PLATFORM="linux/arm/v7"
elif [ "$(uname -m)" = "arm64" ] || [ "$(uname -m)" = "aarch64" ]; then
DIR="aarch64-alpine-linux-musl"
PLATFORM="linux/arm64"
fi
setup "${PLATFORM}" "${DIR}"
fi
}
setup_current

73
hack/update-attributions.sh Executable file
View File

@ -0,0 +1,73 @@
#!/usr/bin/env bash
set -eoux pipefail
SPLIT="***"
cat <<EOF > ATTRIBUTIONS.md
# Attributions
This application uses Open Source components. You can find the source
code of their open source projects along with license information below.
We acknowledge and are grateful to these developers for their contributions
to open source.
## libssh2
Libssh2 was obtained in source-code form from its github repository:
https://github.com/libssh2/libssh2/
No changes were made to its original source code.
Copyright notice (https://raw.githubusercontent.com/libssh2/libssh2/master/COPYING):
$(curl --max-time 5 -L https://raw.githubusercontent.com/libssh2/libssh2/master/COPYING)
${SPLIT}
## libgit2
Libgit2 was obtained in source-code form from its github repository:
https://github.com/libgit2/libgit2/
No changes were made to its original source code.
Copyright notice (https://raw.githubusercontent.com/libgit2/libgit2/main/COPYING):
$(curl --max-time 5 -L https://raw.githubusercontent.com/libgit2/libgit2/main/COPYING)
${SPLIT}
## zlib
Zlib was obtained in binary form via official distribution channels.
No changes were made to its original source code.
Copyright notice (https://zlib.net/zlib_license.html):
/* zlib.h -- interface of the 'zlib' general purpose compression library
version 1.2.11, January 15th, 2017
Copyright (C) 1995-2017 Jean-loup Gailly and Mark Adler
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
Jean-loup Gailly Mark Adler
jloup@gzip.org madler@alumni.caltech.edu
*/
EOF

View File

@ -1,654 +0,0 @@
/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
"context"
"errors"
"fmt"
"time"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
kerrors "k8s.io/apimachinery/pkg/util/errors"
kuberecorder "k8s.io/client-go/tools/record"
"k8s.io/client-go/util/workqueue"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
reflectorv1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
aclapi "github.com/fluxcd/pkg/apis/acl"
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/cache"
"github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/runtime/acl"
"github.com/fluxcd/pkg/runtime/conditions"
helper "github.com/fluxcd/pkg/runtime/controller"
"github.com/fluxcd/pkg/runtime/patch"
"github.com/fluxcd/pkg/runtime/predicates"
runtimereconcile "github.com/fluxcd/pkg/runtime/reconcile"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
imagev1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
"github.com/fluxcd/image-automation-controller/internal/features"
"github.com/fluxcd/image-automation-controller/internal/policy"
"github.com/fluxcd/image-automation-controller/internal/source"
)
const repoRefKey = ".spec.gitRepository"
const readyMessage = "repository up-to-date"
// imageUpdateAutomationOwnedConditions is a list of conditions owned by the
// ImageUpdateAutomationReconciler.
var imageUpdateAutomationOwnedConditions = []string{
meta.ReadyCondition,
meta.ReconcilingCondition,
meta.StalledCondition,
}
// imageUpdateAutomationNegativeConditions is a list of negative polarity
// conditions owned by ImageUpdateAutomationReconciler. It is used in tests for
// compliance with kstatus.
var imageUpdateAutomationNegativeConditions = []string{
meta.StalledCondition,
meta.ReconcilingCondition,
}
var errParsePolicySelector = errors.New("failed to parse policy selector")
// getPatchOptions composes patch options based on the given parameters.
// It is used as the options used when patching an object.
func getPatchOptions(ownedConditions []string, controllerName string) []patch.Option {
return []patch.Option{
patch.WithOwnedConditions{Conditions: ownedConditions},
patch.WithFieldOwner(controllerName),
}
}
// +kubebuilder:rbac:groups=image.toolkit.fluxcd.io,resources=imageupdateautomations,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=image.toolkit.fluxcd.io,resources=imageupdateautomations/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=image.toolkit.fluxcd.io,resources=imagepolicies,verbs=get;list;watch
// +kubebuilder:rbac:groups=image.toolkit.fluxcd.io,resources=imagepolicies/status,verbs=get
// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=gitrepositories,verbs=get;list;watch
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
// ImageUpdateAutomationReconciler reconciles a ImageUpdateAutomation object
type ImageUpdateAutomationReconciler struct {
client.Client
kuberecorder.EventRecorder
helper.Metrics
ControllerName string
NoCrossNamespaceRef bool
features map[string]bool
patchOptions []patch.Option
tokenCache *cache.TokenCache
}
type ImageUpdateAutomationReconcilerOptions struct {
MaxConcurrentReconciles int
RateLimiter workqueue.TypedRateLimiter[reconcile.Request]
RecoverPanic bool
TokenCache *cache.TokenCache
}
func (r *ImageUpdateAutomationReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opts ImageUpdateAutomationReconcilerOptions) error {
r.patchOptions = getPatchOptions(imageUpdateAutomationOwnedConditions, r.ControllerName)
if r.features == nil {
r.features = features.FeatureGates()
}
r.tokenCache = opts.TokenCache
// Index the git repository object that each I-U-A refers to
if err := mgr.GetFieldIndexer().IndexField(ctx, &imagev1.ImageUpdateAutomation{}, repoRefKey, func(obj client.Object) []string {
updater := obj.(*imagev1.ImageUpdateAutomation)
ref := updater.Spec.SourceRef
ns := ref.Namespace
if ns == "" {
ns = obj.GetNamespace()
}
return []string{fmt.Sprintf("%s/%s", ns, ref.Name)}
}); err != nil {
return err
}
return ctrl.NewControllerManagedBy(mgr).
For(&imagev1.ImageUpdateAutomation{}, builder.WithPredicates(
predicate.Or(predicate.GenerationChangedPredicate{}, predicates.ReconcileRequestedPredicate{}))).
Watches(
&sourcev1.GitRepository{},
handler.EnqueueRequestsFromMapFunc(r.automationsForGitRepo),
builder.WithPredicates(sourceConfigChangePredicate{}),
).
Watches(
&reflectorv1.ImagePolicy{},
handler.EnqueueRequestsFromMapFunc(r.automationsForImagePolicy),
builder.WithPredicates(latestImageChangePredicate{}),
).
WithOptions(controller.Options{
RateLimiter: opts.RateLimiter,
}).
Complete(r)
}
// automationsForGitRepo fetches all the automations that refer to a
// particular source.GitRepository object.
func (r *ImageUpdateAutomationReconciler) automationsForGitRepo(ctx context.Context, obj client.Object) []reconcile.Request {
var autoList imagev1.ImageUpdateAutomationList
objKey := fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName())
if err := r.List(ctx, &autoList, client.MatchingFields{repoRefKey: objKey}); err != nil {
ctrl.LoggerFrom(ctx).Error(err, "failed to list ImageUpdateAutomations for GitRepository change")
return nil
}
reqs := make([]reconcile.Request, len(autoList.Items))
for i := range autoList.Items {
reqs[i].NamespacedName.Name = autoList.Items[i].GetName()
reqs[i].NamespacedName.Namespace = autoList.Items[i].GetNamespace()
}
return reqs
}
// automationsForImagePolicy fetches all the automation objects that
// might depend on a image policy object. Since the link is via
// markers in the git repo, _any_ automation object in the same
// namespace could be affected.
func (r *ImageUpdateAutomationReconciler) automationsForImagePolicy(ctx context.Context, obj client.Object) []reconcile.Request {
var autoList imagev1.ImageUpdateAutomationList
if err := r.List(ctx, &autoList, client.InNamespace(obj.GetNamespace())); err != nil {
ctrl.LoggerFrom(ctx).Error(err, "failed to list ImageUpdateAutomations for ImagePolicy change")
return nil
}
reqs := make([]reconcile.Request, len(autoList.Items))
for i := range autoList.Items {
reqs[i].NamespacedName.Name = autoList.Items[i].GetName()
reqs[i].NamespacedName.Namespace = autoList.Items[i].GetNamespace()
}
return reqs
}
func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) {
start := time.Now()
log := ctrl.LoggerFrom(ctx)
// Fetch the ImageUpdateAutomation.
obj := &imagev1.ImageUpdateAutomation{}
if err := r.Get(ctx, req.NamespacedName, obj); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// Initialize the patch helper with the current version of the object.
serialPatcher := patch.NewSerialPatcher(obj, r.Client)
// Always attempt to patch the object after each reconciliation.
defer func() {
// Create patch options for the final patch of the object.
patchOpts := runtimereconcile.AddPatchOptions(obj, r.patchOptions, imageUpdateAutomationOwnedConditions, r.ControllerName)
if err := serialPatcher.Patch(ctx, obj, patchOpts...); err != nil {
// Ignore patch error "not found" when the object is being deleted.
if !obj.GetDeletionTimestamp().IsZero() {
err = kerrors.FilterOut(err, func(e error) bool { return apierrors.IsNotFound(e) })
}
retErr = kerrors.NewAggregate([]error{retErr, err})
}
// When the reconciliation ends with an error, ensure that the Result is
// empty. This is to suppress the runtime warning when returning a
// non-zero Result and an error.
if retErr != nil {
result = ctrl.Result{}
}
// Always record suspend, readiness and duration metrics.
r.Metrics.RecordDuration(ctx, obj, start)
}()
// Examine if the object is under deletion.
if !obj.ObjectMeta.DeletionTimestamp.IsZero() {
return r.reconcileDelete(obj)
}
// Add finalizer first if it doesn't exist to avoid the race condition
// between init and delete.
// Note: Finalizers in general can only be added when the deletionTimestamp
// is not set.
if !controllerutil.ContainsFinalizer(obj, imagev1.ImageUpdateAutomationFinalizer) {
controllerutil.AddFinalizer(obj, imagev1.ImageUpdateAutomationFinalizer)
return ctrl.Result{Requeue: true}, nil
}
// Return if the object is suspended.
if obj.Spec.Suspend {
log.Info("reconciliation is suspended for this object")
return ctrl.Result{}, nil
}
result, retErr = r.reconcile(ctx, serialPatcher, obj, start)
return
}
func (r *ImageUpdateAutomationReconciler) reconcile(ctx context.Context, sp *patch.SerialPatcher,
obj *imagev1.ImageUpdateAutomation, startTime time.Time) (result ctrl.Result, retErr error) {
oldObj := obj.DeepCopy()
var pushResult *source.PushResult
// syncNeeded decides if full reconciliation with image update is needed.
syncNeeded := false
defer func() {
// Define the meaning of success based on the requeue interval.
isSuccess := func(res ctrl.Result, err error) bool {
if err != nil || res.RequeueAfter != obj.GetRequeueAfter() || res.Requeue {
return false
}
return true
}
rs := runtimereconcile.NewResultFinalizer(isSuccess, readyMessage)
retErr = rs.Finalize(obj, result, retErr)
// Presence of reconciling means that the reconciliation didn't succeed.
// Set the Reconciling reason to ProgressingWithRetry to indicate a
// failure retry.
if conditions.IsReconciling(obj) {
reconciling := conditions.Get(obj, meta.ReconcilingCondition)
reconciling.Reason = meta.ProgressingWithRetryReason
conditions.Set(obj, reconciling)
}
r.notify(ctx, oldObj, obj, pushResult, syncNeeded)
}()
// TODO: Maybe move this to Reconcile()'s defer and avoid passing startTime
// to reconcile()?
obj.Status.LastAutomationRunTime = &metav1.Time{Time: startTime}
// Set reconciling condition.
runtimereconcile.ProgressiveStatus(false, obj, meta.ProgressingReason, "reconciliation in progress")
var reconcileAtVal string
if v, ok := meta.ReconcileAnnotationValue(obj.GetAnnotations()); ok {
reconcileAtVal = v
}
// Persist reconciling if generation differs or reconciliation is requested.
switch {
case obj.Generation != obj.Status.ObservedGeneration:
runtimereconcile.ProgressiveStatus(false, obj, meta.ProgressingReason,
"processing object: new generation %d -> %d", obj.Status.ObservedGeneration, obj.Generation)
if err := sp.Patch(ctx, obj, r.patchOptions...); err != nil {
result, retErr = ctrl.Result{}, err
return
}
case reconcileAtVal != obj.Status.GetLastHandledReconcileRequest():
if err := sp.Patch(ctx, obj, r.patchOptions...); err != nil {
result, retErr = ctrl.Result{}, err
return
}
}
// List the policies and construct observed policies.
policies, err := getPolicies(ctx, r.Client, obj.Namespace, obj.Spec.PolicySelector)
if err != nil {
if errors.Is(err, errParsePolicySelector) {
conditions.MarkStalled(obj, imagev1.InvalidPolicySelectorReason, "%s", err)
result, retErr = ctrl.Result{}, nil
return
}
result, retErr = ctrl.Result{}, err
return
}
// Update any stale Ready=False condition from policies config failure.
if conditions.HasAnyReason(obj, meta.ReadyCondition, imagev1.InvalidPolicySelectorReason) {
conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "reconciliation in progress")
}
// Index the policies by their name.
observedPolicies := imagev1.ObservedPolicies{}
for _, policy := range policies {
observedPolicies[policy.Name] = imagev1.ImageRef{
Name: policy.Status.LatestRef.Name,
Tag: policy.Status.LatestRef.Tag,
Digest: policy.Status.LatestRef.Digest,
}
}
// If the policies have changed, require a full sync.
if observedPoliciesChanged(obj.Status.ObservedPolicies, observedPolicies) {
syncNeeded = true
}
// Create source manager with options.
smOpts := []source.SourceOption{
source.WithSourceOptionInvolvedObject(obj.GetName(), obj.GetNamespace()),
source.WithSourceOptionTokenCache(r.tokenCache),
}
if r.NoCrossNamespaceRef {
smOpts = append(smOpts, source.WithSourceOptionNoCrossNamespaceRef())
}
if r.features[features.GitAllBranchReferences] {
smOpts = append(smOpts, source.WithSourceOptionGitAllBranchReferences())
}
sm, err := source.NewSourceManager(ctx, r.Client, obj, smOpts...)
if err != nil {
if acl.IsAccessDenied(err) {
conditions.MarkStalled(obj, aclapi.AccessDeniedReason, "%s", err)
result, retErr = ctrl.Result{}, nil
return
}
if errors.Is(err, source.ErrInvalidSourceConfiguration) {
conditions.MarkStalled(obj, imagev1.InvalidSourceConfigReason, "%s", err)
result, retErr = ctrl.Result{}, nil
return
}
e := fmt.Errorf("failed configuring source manager: %w", err)
conditions.MarkFalse(obj, meta.ReadyCondition, imagev1.SourceManagerFailedReason, "%s", e)
result, retErr = ctrl.Result{}, e
return
}
defer func() {
if err := sm.Cleanup(); err != nil {
retErr = err
}
}()
// Update any stale Ready=False condition from SourceManager failure.
if conditions.HasAnyReason(obj, meta.ReadyCondition, aclapi.AccessDeniedCondition, imagev1.InvalidSourceConfigReason, imagev1.SourceManagerFailedReason) {
conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "reconciliation in progress")
}
// When the checkout and push branches are different or a refspec is
// defined, always perform a full sync.
// This can be worked around in the future by also querying the HEAD of push
// branch to detech if it has drifted.
if sm.SwitchBranch() || obj.Spec.GitSpec.HasRefspec() {
syncNeeded = true
}
// Build checkout options.
checkoutOpts := []source.CheckoutOption{}
if r.features[features.GitShallowClone] {
checkoutOpts = append(checkoutOpts, source.WithCheckoutOptionShallowClone())
}
if r.features[features.GitSparseCheckout] && obj.Spec.Update.Path != "" {
checkoutOpts = append(checkoutOpts, source.WithCheckoutOptionSparseCheckoutDirectories(obj.Spec.Update.Path))
}
// If full sync is still not needed, configure last observed commit to
// perform optimized clone and obtain a non-concrete commit if the remote
// has not changed.
if !syncNeeded && obj.Status.ObservedSourceRevision != "" {
checkoutOpts = append(checkoutOpts, source.WithCheckoutOptionLastObserved(obj.Status.ObservedSourceRevision))
}
commit, err := sm.CheckoutSource(ctx, checkoutOpts...)
if err != nil {
e := fmt.Errorf("failed to checkout source: %w", err)
conditions.MarkFalse(obj, meta.ReadyCondition, imagev1.GitOperationFailedReason, "%s", e)
result, retErr = ctrl.Result{}, e
return
}
// Update any stale Ready=False condition from checkout failure.
if conditions.HasAnyReason(obj, meta.ReadyCondition, imagev1.GitOperationFailedReason) {
conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "reconciliation in progress")
}
// If it's a partial commit, the reconciliation can be skipped. The last
// observed commit is only configured above when full sync is not needed.
// No change in the policies and remote git repository. Skip reconciliation.
if !git.IsConcreteCommit(*commit) {
// Remove any stale Ready condition, most likely False, set above. Its value
// is derived from the overall result of the reconciliation in the deferred
// block at the very end.
conditions.Delete(obj, meta.ReadyCondition)
result, retErr = ctrl.Result{RequeueAfter: obj.GetRequeueAfter()}, nil
return
} else {
// Concrete commit indicates full sync is needed due to new remote
// revision.
syncNeeded = true
}
// Continue with full sync with a concrete commit.
// Apply the policies and check if there's anything to update.
policyResult, err := policy.ApplyPolicies(ctx, sm.WorkDirectory(), obj, policies)
if err != nil {
if errors.Is(err, policy.ErrNoUpdateStrategy) || errors.Is(err, policy.ErrUnsupportedUpdateStrategy) {
conditions.MarkStalled(obj, imagev1.InvalidUpdateStrategyReason, "%s", err)
result, retErr = ctrl.Result{}, nil
return
}
e := fmt.Errorf("failed to apply policies: %w", err)
conditions.MarkFalse(obj, meta.ReadyCondition, imagev1.UpdateFailedReason, "%s", e)
result, retErr = ctrl.Result{}, e
return
}
// Update any stale Ready=False condition from apply policies failure.
if conditions.HasAnyReason(obj, meta.ReadyCondition, imagev1.InvalidUpdateStrategyReason, imagev1.UpdateFailedReason) {
conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "reconciliation in progress")
}
if len(policyResult.FileChanges) == 0 {
// Remove any stale Ready condition, most likely False, set above. Its
// value is derived from the overall result of the reconciliation in the
// deferred block at the very end.
conditions.Delete(obj, meta.ReadyCondition)
// Persist observations.
obj.Status.ObservedSourceRevision = commit.String()
obj.Status.ObservedPolicies = observedPolicies
result, retErr = ctrl.Result{RequeueAfter: obj.GetRequeueAfter()}, nil
return
}
// Build push config.
pushCfg := []source.PushConfig{}
// Enable force only when branch is changed for push.
if r.features[features.GitForcePushBranch] && sm.SwitchBranch() {
pushCfg = append(pushCfg, source.WithPushConfigForce())
}
// Include any push options.
if obj.Spec.GitSpec.Push != nil && obj.Spec.GitSpec.Push.Options != nil {
pushCfg = append(pushCfg, source.WithPushConfigOptions(obj.Spec.GitSpec.Push.Options))
}
pushResult, err = sm.CommitAndPush(ctx, obj, policyResult, pushCfg...)
if err != nil {
// Check if error is due to removed template field usage.
// Set Stalled condition and return nil error to prevent requeue, allowing user to fix template.
if errors.Is(err, source.ErrRemovedTemplateField) {
conditions.MarkStalled(obj, imagev1.RemovedTemplateFieldReason, "%s", err)
result, retErr = ctrl.Result{}, nil
return
}
e := fmt.Errorf("failed to update source: %w", err)
conditions.MarkFalse(obj, meta.ReadyCondition, imagev1.GitOperationFailedReason, "%s", e)
result, retErr = ctrl.Result{}, e
return
}
// Update any stale Ready=False condition from commit and push failure.
if conditions.HasAnyReason(obj, meta.ReadyCondition, imagev1.GitOperationFailedReason) {
conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "reconciliation in progress")
}
if pushResult == nil {
// NOTE: This should not happen. This exists as a legacy behavior from
// the old implementation where no commit is made due to no stagged
// files. If nothing is pushed, the repository is up-to-date. Persist
// observations and return with successful result.
conditions.Delete(obj, meta.ReadyCondition)
obj.Status.ObservedSourceRevision = commit.String()
obj.Status.ObservedPolicies = observedPolicies
result, retErr = ctrl.Result{RequeueAfter: obj.GetRequeueAfter()}, nil
return
}
// Persist observations.
obj.Status.ObservedSourceRevision = pushResult.Commit().String()
// If the push branch is different, store the checkout branch commit as the
// observed source revision.
if pushResult.SwitchBranch() {
obj.Status.ObservedSourceRevision = commit.String()
}
obj.Status.ObservedPolicies = observedPolicies
obj.Status.LastPushCommit = pushResult.Commit().Hash.String()
obj.Status.LastPushTime = pushResult.Time()
// Remove any stale Ready condition, most likely False, set above. Its value
// is derived from the overall result of the reconciliation in the deferred
// block at the very end.
conditions.Delete(obj, meta.ReadyCondition)
result, retErr = ctrl.Result{RequeueAfter: obj.GetRequeueAfter()}, nil
return
}
// reconcileDelete handles the deletion of the object.
func (r *ImageUpdateAutomationReconciler) reconcileDelete(obj *imagev1.ImageUpdateAutomation) (ctrl.Result, error) {
// Remove our finalizer from the list.
controllerutil.RemoveFinalizer(obj, imagev1.ImageUpdateAutomationFinalizer)
// Cleanup caches.
r.tokenCache.DeleteEventsForObject(imagev1.ImageUpdateAutomationKind,
obj.GetName(), obj.GetNamespace(), cache.OperationReconcile)
// Stop reconciliation as the object is being deleted.
return ctrl.Result{}, nil
}
// getPolicies returns list of policies in the given namespace that have latest
// image.
func getPolicies(ctx context.Context, kclient client.Client, namespace string, selector *metav1.LabelSelector) ([]reflectorv1.ImagePolicy, error) {
policySelector := labels.Everything()
var err error
if selector != nil {
if policySelector, err = metav1.LabelSelectorAsSelector(selector); err != nil {
return nil, fmt.Errorf("%w: %w", errParsePolicySelector, err)
}
}
var policies reflectorv1.ImagePolicyList
if err := kclient.List(ctx, &policies, &client.ListOptions{Namespace: namespace, LabelSelector: policySelector}); err != nil {
return nil, fmt.Errorf("failed to list policies: %w", err)
}
readyPolicies := []reflectorv1.ImagePolicy{}
for _, policy := range policies.Items {
// Ignore the policies that don't have a latest image.
if policy.Status.LatestRef == nil {
continue
}
readyPolicies = append(readyPolicies, policy)
}
return readyPolicies, nil
}
// observedPoliciesChanged returns if the previous and current observedPolicies
// have changed.
func observedPoliciesChanged(previous, current imagev1.ObservedPolicies) bool {
if len(previous) != len(current) {
return true
}
for name, imageRef := range current {
oldImageRef, ok := previous[name]
if !ok {
// Changed if an entry is not found.
return true
}
if oldImageRef != imageRef {
return true
}
}
return false
}
// notify emits notifications and events based on the state of the object and
// the given PushResult. It tries to always send the PushResult commit message
// if there has been any update. Otherwise, a generic up-to-date message. In
// case of any failure, the failure message is read from the Ready condition and
// included in the event.
func (r *ImageUpdateAutomationReconciler) notify(ctx context.Context, oldObj, newObj conditions.Setter, result *source.PushResult, syncNeeded bool) {
// Use the Ready message as the notification message by default.
ready := conditions.Get(newObj, meta.ReadyCondition)
msg := ready.Message
// If there's a PushResult, use the summary as the notification message.
if result != nil {
msg = result.Summary()
}
// Was ready before and is ready now, with new push result,
if conditions.IsReady(oldObj) && conditions.IsReady(newObj) && result != nil {
eventLogf(ctx, r.EventRecorder, newObj, corev1.EventTypeNormal, ready.Reason, "%s", msg)
return
}
// Emit events when reconciliation fails or recovers from failure.
// Became ready from not ready.
if !conditions.IsReady(oldObj) && conditions.IsReady(newObj) {
eventLogf(ctx, r.EventRecorder, newObj, corev1.EventTypeNormal, ready.Reason, "%s", msg)
return
}
// Not ready, failed. Use the failure message from ready condition.
if !conditions.IsReady(newObj) {
eventLogf(ctx, r.EventRecorder, newObj, corev1.EventTypeWarning, ready.Reason, "%s", ready.Message)
return
}
// No change.
if !syncNeeded {
// Full reconciliation skipped.
msg = "no change since last reconciliation"
}
eventLogf(ctx, r.EventRecorder, newObj, eventv1.EventTypeTrace, meta.SucceededReason, "%s", msg)
}
// eventLogf records events, and logs at the same time.
//
// This log is different from the debug log in the EventRecorder, in the sense
// that this is a simple log. While the debug log contains complete details
// about the event.
func eventLogf(ctx context.Context, r kuberecorder.EventRecorder, obj runtime.Object, eventType string, reason string, messageFmt string, args ...interface{}) {
msg := fmt.Sprintf(messageFmt, args...)
// Log and emit event.
if eventType == corev1.EventTypeWarning {
ctrl.LoggerFrom(ctx).Error(errors.New(reason), msg)
} else {
ctrl.LoggerFrom(ctx).Info(msg)
}
r.Eventf(obj, eventType, reason, msg)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,88 +0,0 @@
/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
reflectorv1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
)
// latestImageChangePredicate implements a predicate for latest image change.
// This can be used to filter events from ImagePolicies for change in the latest
// image.
type latestImageChangePredicate struct {
predicate.Funcs
}
func (latestImageChangePredicate) Create(e event.CreateEvent) bool {
return false
}
func (latestImageChangePredicate) Delete(e event.DeleteEvent) bool {
return false
}
func (latestImageChangePredicate) Update(e event.UpdateEvent) bool {
if e.ObjectOld == nil || e.ObjectNew == nil {
return false
}
oldSource, ok := e.ObjectOld.(*reflectorv1.ImagePolicy)
if !ok {
return false
}
newSource, ok := e.ObjectNew.(*reflectorv1.ImagePolicy)
if !ok {
return false
}
if newSource.Status.LatestRef == nil {
return false
}
if oldSource.Status.LatestRef == nil || *oldSource.Status.LatestRef != *newSource.Status.LatestRef {
return true
}
return false
}
// sourceConfigChangePredicate implements a predicate for source configuration
// change. This can be used to filter events from source objects for change in
// source configuration.
type sourceConfigChangePredicate struct {
predicate.Funcs
}
func (sourceConfigChangePredicate) Create(e event.CreateEvent) bool {
return false
}
func (sourceConfigChangePredicate) Delete(e event.DeleteEvent) bool {
return false
}
func (sourceConfigChangePredicate) Update(e event.UpdateEvent) bool {
if e.ObjectOld == nil || e.ObjectNew == nil {
return false
}
return e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration()
}

View File

@ -1,120 +0,0 @@
/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
"testing"
. "github.com/onsi/gomega"
"sigs.k8s.io/controller-runtime/pkg/event"
reflectorv1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
)
func Test_latestImageChangePredicate_Update(t *testing.T) {
tests := []struct {
name string
beforeFunc func(oldObj, newObj *reflectorv1.ImagePolicy)
want bool
}{
{
name: "no latest image",
beforeFunc: func(oldObj, newObj *reflectorv1.ImagePolicy) {
oldObj.Status.LatestRef = nil
newObj.Status.LatestRef = nil
},
want: false,
},
{
name: "new image, no old image",
beforeFunc: func(oldObj, newObj *reflectorv1.ImagePolicy) {
oldObj.Status.LatestRef = nil
newObj.Status.LatestRef = &reflectorv1.ImageRef{Name: "foo"}
},
want: true,
},
{
name: "different old and new image",
beforeFunc: func(oldObj, newObj *reflectorv1.ImagePolicy) {
oldObj.Status.LatestRef = &reflectorv1.ImageRef{Name: "bar"}
newObj.Status.LatestRef = &reflectorv1.ImageRef{Name: "foo"}
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
oldObj := &reflectorv1.ImagePolicy{}
newObj := oldObj.DeepCopy()
if tt.beforeFunc != nil {
tt.beforeFunc(oldObj, newObj)
}
e := event.UpdateEvent{
ObjectOld: oldObj,
ObjectNew: newObj,
}
p := latestImageChangePredicate{}
g.Expect(p.Update(e)).To(Equal(tt.want))
})
}
}
func Test_sourceConfigChangePredicate_Update(t *testing.T) {
tests := []struct {
name string
beforeFunc func(oldObj, newObj *sourcev1.GitRepository)
want bool
}{
{
name: "no generation change, same config",
beforeFunc: func(oldObj, newObj *sourcev1.GitRepository) {
oldObj.Generation = 0
newObj.Generation = 0
},
want: false,
},
{
name: "new generation, config change",
beforeFunc: func(oldObj, newObj *sourcev1.GitRepository) {
oldObj.Generation = 1
newObj.Generation = 2
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
oldObj := &sourcev1.GitRepository{}
newObj := oldObj.DeepCopy()
if tt.beforeFunc != nil {
tt.beforeFunc(oldObj, newObj)
}
e := event.UpdateEvent{
ObjectOld: oldObj,
ObjectNew: newObj,
}
p := sourceConfigChangePredicate{}
g.Expect(p.Update(e)).To(Equal(tt.want))
})
}
}

View File

@ -1,115 +0,0 @@
/*
Copyright 2020 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
"fmt"
"math/rand"
"os"
"path/filepath"
"testing"
"time"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
reflectorv1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
"github.com/fluxcd/pkg/runtime/controller"
"github.com/fluxcd/pkg/runtime/testenv"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
imagev1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
// +kubebuilder:scaffold:imports
)
// These tests make use of plain Go using Gomega for assertions.
// At the beginning of every (sub)test Gomega can be initialized
// using gomega.NewWithT.
// Refer to http://onsi.github.io/gomega/ to learn more about
// Gomega.
var (
k8sClient client.Client
testEnv *testenv.Environment
ctx = ctrl.SetupSignalHandler()
)
func init() {
rand.Seed(time.Now().UnixNano())
}
func TestMain(m *testing.M) {
utilruntime.Must(reflectorv1.AddToScheme(scheme.Scheme))
utilruntime.Must(sourcev1.AddToScheme(scheme.Scheme))
utilruntime.Must(imagev1.AddToScheme(scheme.Scheme))
code := runTestsWithFeatures(m, nil)
if code != 0 {
fmt.Println("failed with default feature values")
}
os.Exit(code)
}
func runTestsWithFeatures(m *testing.M, feats map[string]bool) int {
testEnv = testenv.New(
testenv.WithCRDPath(
filepath.Join("..", "..", "config", "crd", "bases"),
filepath.Join("testdata", "crds"),
),
testenv.WithMaxConcurrentReconciles(2),
)
var err error
// Initialize a cacheless client for tests that need the latest objects.
k8sClient, err = client.New(testEnv.Config, client.Options{Scheme: scheme.Scheme})
if err != nil {
panic(fmt.Sprintf("failed to create k8s client: %v", err))
}
controllerName := "image-automation-controller"
if err := (&ImageUpdateAutomationReconciler{
Client: testEnv,
EventRecorder: record.NewFakeRecorder(32),
features: feats,
ControllerName: controllerName,
}).SetupWithManager(ctx, testEnv, ImageUpdateAutomationReconcilerOptions{
RateLimiter: controller.GetDefaultRateLimiter(),
}); err != nil {
panic(fmt.Sprintf("failed to start ImageUpdateAutomationReconciler: %v", err))
}
go func() {
fmt.Println("Starting the test environment")
if err := testEnv.Start(ctx); err != nil {
panic(fmt.Sprintf("failed to start the test environment manager: %v", err))
}
}()
<-testEnv.Manager.Elected()
code := m.Run()
fmt.Println("Stopping the test environment")
if err := testEnv.Stop(); err != nil {
panic(fmt.Sprintf("failed to stop the test environment: %v", err))
}
return code
}

View File

@ -1,87 +0,0 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package features sets the feature gates that
// image-automation-controller supports, and their default
// states.
package features
import (
"github.com/fluxcd/pkg/auth"
feathelper "github.com/fluxcd/pkg/runtime/features"
)
const (
// GitForcePushBranch enables the use of "force push" when push branches
// are configured.
GitForcePushBranch = "GitForcePushBranch"
// GitShallowClone enables the use of shallow clones when pulling source from
// Git repositories.
GitShallowClone = "GitShallowClone"
// GitAllBranchReferences enables the download of all branch head references
// when push branches are configured. When enabled fixes fluxcd/flux2#3384.
GitAllBranchReferences = "GitAllBranchReferences"
// GitSparseCheckout enables the use of sparse checkout when pulling source from
// Git repositories.
GitSparseCheckout = "GitSparseCheckout"
// CacheSecretsAndConfigMaps controls whether Secrets and ConfigMaps should
// be cached.
//
// When enabled, it will cache both object types, resulting in increased
// memory usage and cluster-wide RBAC permissions (list and watch).
CacheSecretsAndConfigMaps = "CacheSecretsAndConfigMaps"
)
var features = map[string]bool{
// GitForcePushBranch
// opt-out from v0.27
GitForcePushBranch: true,
// GitShallowClone
// opt-out from v0.28
GitShallowClone: true,
// GitAllBranchReferences
// opt-out from v0.28
GitAllBranchReferences: true,
// GitSparseCheckout
// opt-in from v0.42
GitSparseCheckout: false,
// CacheSecretsAndConfigMaps
// opt-in from v0.29
CacheSecretsAndConfigMaps: false,
}
func init() {
auth.SetFeatureGates(features)
}
// FeatureGates contains a list of all supported feature gates and
// their default values.
func FeatureGates() map[string]bool {
return features
}
// Enabled verifies whether the feature is enabled or not.
//
// This is only a wrapper around the Enabled func in
// pkg/runtime/features, so callers won't need to import
// both packages for checking whether a feature is enabled.
func Enabled(feature string) (bool, error) {
return feathelper.Enabled(feature)
}

View File

@ -1,66 +0,0 @@
/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package policy
import (
"context"
"errors"
"fmt"
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/fluxcd/pkg/runtime/logger"
"sigs.k8s.io/controller-runtime/pkg/log"
reflectorv1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
imagev1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
"github.com/fluxcd/image-automation-controller/internal/update"
)
var (
// ErrNoUpdateStrategy is an update error when the update strategy is not
// specified.
ErrNoUpdateStrategy = errors.New("no update strategy")
// ErrUnsupportedUpdateStrategy is an update error when the provided update
// strategy is not supported.
ErrUnsupportedUpdateStrategy = errors.New("unsupported update strategy")
)
// ApplyPolicies applies the given set of policies on the source present in the
// workDir based on the provided ImageUpdateAutomation configuration.
func ApplyPolicies(ctx context.Context, workDir string, obj *imagev1.ImageUpdateAutomation, policies []reflectorv1.ImagePolicy) (update.Result, error) {
var result update.Result
if obj.Spec.Update == nil {
return result, ErrNoUpdateStrategy
}
if obj.Spec.Update.Strategy != imagev1.UpdateStrategySetters {
return result, fmt.Errorf("%w: %s", ErrUnsupportedUpdateStrategy, obj.Spec.Update.Strategy)
}
// Resolve the path to the manifests to apply policies on.
manifestPath := workDir
if obj.Spec.Update.Path != "" {
p, err := securejoin.SecureJoin(workDir, obj.Spec.Update.Path)
if err != nil {
return result, fmt.Errorf("failed to secure join manifest path: %w", err)
}
manifestPath = p
}
tracelog := log.FromContext(ctx).V(logger.TraceLevel)
return update.UpdateWithSetters(tracelog, manifestPath, manifestPath, policies)
}

View File

@ -1,170 +0,0 @@
/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package policy
import (
"context"
"path/filepath"
"testing"
. "github.com/onsi/gomega"
"github.com/otiai10/copy"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
reflectorv1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
imagev1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
"github.com/fluxcd/image-automation-controller/internal/testutil"
)
func testdataPath(path string) string {
return filepath.Join("testdata", path)
}
func Test_applyPolicies(t *testing.T) {
tests := []struct {
name string
updateStrategy *imagev1.UpdateStrategy
policyLatestImages map[string]string
targetPolicyName string
replaceMarkerFunc func(g *WithT, path string, policyKey types.NamespacedName)
inputPath string
expectedPath string
wantErr bool
}{
{
name: "valid update strategy and one policy",
updateStrategy: &imagev1.UpdateStrategy{
Strategy: imagev1.UpdateStrategySetters,
},
policyLatestImages: map[string]string{
"policy1": "helloworld:1.0.1",
},
targetPolicyName: "policy1",
inputPath: testdataPath("appconfig"),
expectedPath: testdataPath("appconfig-setters-expected"),
wantErr: false,
},
{
name: "no update strategy",
updateStrategy: nil,
wantErr: true,
},
{
name: "unknown update strategy",
updateStrategy: &imagev1.UpdateStrategy{
Strategy: "foo",
},
wantErr: true,
},
{
name: "valid update strategy and multiple policies",
updateStrategy: &imagev1.UpdateStrategy{
Strategy: imagev1.UpdateStrategySetters,
},
policyLatestImages: map[string]string{
"policy1": "foo:1.1.1",
"policy2": "helloworld:1.0.1",
"policy3": "bar:2.2.2",
},
targetPolicyName: "policy2",
inputPath: testdataPath("appconfig"),
expectedPath: testdataPath("appconfig-setters-expected"),
wantErr: false,
},
{
name: "valid update strategy with update path",
updateStrategy: &imagev1.UpdateStrategy{
Strategy: imagev1.UpdateStrategySetters,
Path: "./yes",
},
policyLatestImages: map[string]string{
"policy1": "helloworld:1.0.1",
},
targetPolicyName: "policy1",
replaceMarkerFunc: func(g *WithT, path string, policyKey types.NamespacedName) {
g.Expect(testutil.ReplaceMarker(filepath.Join(path, "yes", "deploy.yaml"), policyKey)).ToNot(HaveOccurred())
g.Expect(testutil.ReplaceMarker(filepath.Join(path, "no", "deploy.yaml"), policyKey)).ToNot(HaveOccurred())
},
inputPath: testdataPath("pathconfig"),
expectedPath: testdataPath("pathconfig-expected"),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
testNS := "test-ns"
workDir := t.TempDir()
// Create all the policy objects.
policyList := []reflectorv1.ImagePolicy{}
for name, image := range tt.policyLatestImages {
policy := &reflectorv1.ImagePolicy{}
policy.Name = name
policy.Namespace = testNS
policy.Status = reflectorv1.ImagePolicyStatus{
LatestRef: testutil.ImageToRef(image),
}
policyList = append(policyList, *policy)
}
targetPolicyKey := types.NamespacedName{
Name: tt.targetPolicyName, Namespace: testNS,
}
if tt.inputPath != "" {
g.Expect(copy.Copy(tt.inputPath, workDir)).ToNot(HaveOccurred())
// Update the test files with the target policy.
if tt.replaceMarkerFunc != nil {
tt.replaceMarkerFunc(g, workDir, targetPolicyKey)
} else {
g.Expect(testutil.ReplaceMarker(filepath.Join(workDir, "deploy.yaml"), targetPolicyKey)).ToNot(HaveOccurred())
}
}
updateAuto := &imagev1.ImageUpdateAutomation{}
updateAuto.Name = "test-update"
updateAuto.Namespace = testNS
updateAuto.Spec = imagev1.ImageUpdateAutomationSpec{
Update: tt.updateStrategy,
}
scheme := runtime.NewScheme()
reflectorv1.AddToScheme(scheme)
imagev1.AddToScheme(scheme)
_, err := ApplyPolicies(context.TODO(), workDir, updateAuto, policyList)
g.Expect(err != nil).To(Equal(tt.wantErr))
// Check the results if there wasn't any error.
if !tt.wantErr {
expected := t.TempDir()
copy.Copy(tt.expectedPath, expected)
// Update the markers in the expected test data.
if tt.replaceMarkerFunc != nil {
tt.replaceMarkerFunc(g, expected, targetPolicyKey)
} else {
g.Expect(testutil.ReplaceMarker(filepath.Join(expected, "deploy.yaml"), targetPolicyKey)).ToNot(HaveOccurred())
}
testutil.ExpectMatchingDirectories(g, workDir, expected)
}
})
}
}

View File

@ -1,10 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: update-no
spec:
template:
spec:
containers:
- name: hello
image: helloworld:1.0.0 # SETTER_SITE

View File

@ -1,10 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: update-yes
spec:
template:
spec:
containers:
- name: hello
image: helloworld:1.0.1 # SETTER_SITE

View File

@ -1,10 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: update-no
spec:
template:
spec:
containers:
- name: hello
image: helloworld:1.0.0 # SETTER_SITE

View File

@ -1,10 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: update-yes
spec:
template:
spec:
containers:
- name: hello
image: helloworld:1.0.0 # SETTER_SITE

View File

@ -1,339 +0,0 @@
/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package source
import (
"bytes"
"context"
"errors"
"fmt"
"net/url"
"time"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/fluxcd/pkg/runtime/secrets"
"github.com/go-git/go-git/v5/plumbing/transport"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/fluxcd/pkg/auth"
authutils "github.com/fluxcd/pkg/auth/utils"
"github.com/fluxcd/pkg/cache"
"github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/github"
"github.com/fluxcd/pkg/git/gogit"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
imagev1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
)
const (
signingSecretKey = "git.asc"
signingPassphraseKey = "passphrase"
)
// gitSrcCfg contains all the Git configurations related to a source derived
// from the given configurations and the environment.
type gitSrcCfg struct {
srcKey types.NamespacedName
url string
pushBranch string
switchBranch bool
timeout *metav1.Duration
checkoutRef *sourcev1.GitRepositoryRef
authOpts *git.AuthOptions
clientOpts []gogit.ClientOption
signingEntity *openpgp.Entity
}
func buildGitConfig(ctx context.Context, c client.Client, originKey, srcKey types.NamespacedName, gitSpec *imagev1.GitSpec, opts SourceOptions) (*gitSrcCfg, error) {
var err error
cfg := &gitSrcCfg{
srcKey: srcKey,
}
// Get the repo.
repo := &sourcev1.GitRepository{}
if err = c.Get(ctx, srcKey, repo); err != nil {
if client.IgnoreNotFound(err) == nil {
return nil, fmt.Errorf("referenced git repository does not exist: %w", err)
}
}
cfg.url = repo.Spec.URL
// Configure Git operation timeout from the GitRepository configuration.
if repo.Spec.Timeout != nil {
cfg.timeout = repo.Spec.Timeout
} else {
cfg.timeout = &metav1.Duration{Duration: time.Minute}
}
// Get the checkout ref for the source, prioritizing the image automation
// object gitSpec checkout reference and falling back to the GitRepository
// reference if not provided.
// var checkoutRef *sourcev1.GitRepositoryRef
if gitSpec.Checkout != nil {
cfg.checkoutRef = &gitSpec.Checkout.Reference
} else if repo.Spec.Reference != nil {
cfg.checkoutRef = repo.Spec.Reference
} // else remain as `nil` and git.DefaultBranch will be used.
// Configure push first as the client options below depend on the push
// configuration.
if err = configurePush(cfg, gitSpec, cfg.checkoutRef); err != nil {
return nil, err
}
var proxyURL *url.URL
var proxyOpts *transport.ProxyOptions
// Check if a proxy secret reference is provided in the GitRepository spec.
if repo.Spec.ProxySecretRef != nil {
secretRef := types.NamespacedName{
Name: repo.Spec.ProxySecretRef.Name,
Namespace: repo.GetNamespace(),
}
// Get the proxy URL from runtime/secret
proxyURL, err = secrets.ProxyURLFromSecretRef(ctx, c, secretRef)
if err != nil {
return nil, err
}
proxyOpts = &transport.ProxyOptions{URL: proxyURL.String()}
}
cfg.authOpts, err = getAuthOpts(ctx, c, repo, opts, proxyURL)
if err != nil {
return nil, err
}
cfg.clientOpts = []gogit.ClientOption{gogit.WithDiskStorage()}
if cfg.authOpts.Transport == git.HTTP {
cfg.clientOpts = append(cfg.clientOpts, gogit.WithInsecureCredentialsOverHTTP())
}
if proxyOpts != nil {
cfg.clientOpts = append(cfg.clientOpts, gogit.WithProxy(*proxyOpts))
}
// If the push branch is different from the checkout ref, we need to
// have all the references downloaded at clone time, to ensure that
// SwitchBranch will have access to the target branch state. fluxcd/flux2#3384
//
// To always overwrite the push branch, the feature gate
// GitAllBranchReferences can be set to false, which will cause
// the SwitchBranch operation to ignore the remote branch state.
if cfg.switchBranch {
cfg.clientOpts = append(cfg.clientOpts, gogit.WithSingleBranch(!opts.gitAllBranchReferences))
}
if gitSpec.Commit.SigningKey != nil {
if cfg.signingEntity, err = getSigningEntity(ctx, c, originKey.Namespace, gitSpec); err != nil {
return nil, err
}
}
return cfg, nil
}
func configurePush(cfg *gitSrcCfg, gitSpec *imagev1.GitSpec, checkoutRef *sourcev1.GitRepositoryRef) error {
if gitSpec.Push != nil && gitSpec.Push.Branch != "" {
cfg.pushBranch = gitSpec.Push.Branch
if checkoutRef != nil {
if cfg.pushBranch != checkoutRef.Branch {
cfg.switchBranch = true
}
} else {
// Compare with the git default branch when no checkout ref is
// explicitly defined.
if cfg.pushBranch != git.DefaultBranch {
cfg.switchBranch = true
}
}
return nil
}
// If no push branch is configured above, use the branch from checkoutRef.
// Here's where it gets constrained. If there's no push branch
// given, then the checkout ref must include a branch, and
// that can be used.
if checkoutRef == nil || checkoutRef.Branch == "" {
return errors.New("push spec not provided, and cannot be inferred from .spec.git.checkout.ref or GitRepository .spec.ref")
}
cfg.pushBranch = checkoutRef.Branch
return nil
}
func getAuthOpts(ctx context.Context, c client.Client, repo *sourcev1.GitRepository,
srcOpts SourceOptions, proxyURL *url.URL) (*git.AuthOptions, error) {
var secret *corev1.Secret
var data map[string][]byte
var err error
if repo.Spec.SecretRef != nil {
secret, err = getSecret(ctx, c, repo.Spec.SecretRef.Name, repo.GetNamespace())
if err != nil {
return nil, fmt.Errorf("failed to get auth secret '%s/%s': %w", repo.GetNamespace(), repo.Spec.SecretRef.Name, err)
}
data = secret.Data
}
u, err := url.Parse(repo.Spec.URL)
if err != nil {
return nil, fmt.Errorf("failed to parse URL '%s': %w", repo.Spec.URL, err)
}
opts, err := git.NewAuthOptions(*u, data)
if err != nil {
return nil, fmt.Errorf("failed to configure authentication options: %w", err)
}
var getCreds func() (*authutils.GitCredentials, error)
switch provider := repo.GetProvider(); provider {
case sourcev1.GitProviderAzure: // If AWS or GCP are added in the future they can be added here separated by a comma.
getCreds = func() (*authutils.GitCredentials, error) {
opts := []auth.Option{
auth.WithClient(c),
auth.WithServiceAccountNamespace(srcOpts.objNamespace),
}
if srcOpts.tokenCache != nil {
involvedObject := cache.InvolvedObject{
Kind: imagev1.ImageUpdateAutomationKind,
Name: srcOpts.objName,
Namespace: srcOpts.objNamespace,
Operation: cache.OperationReconcile,
}
opts = append(opts, auth.WithCache(*srcOpts.tokenCache, involvedObject))
}
if proxyURL != nil {
opts = append(opts, auth.WithProxyURL(*proxyURL))
}
return authutils.GetGitCredentials(ctx, provider, opts...)
}
case sourcev1.GitProviderGitHub:
// if provider is github, but secret ref is not specified
if repo.Spec.SecretRef == nil {
return nil, fmt.Errorf("secretRef with github app data must be specified when provider is set to github: %w", ErrInvalidSourceConfiguration)
}
authMethods, err := secrets.AuthMethodsFromSecret(ctx, secret, secrets.WithTLSSystemCertPool())
if err != nil {
return nil, err
}
if !authMethods.HasGitHubAppData() {
return nil, fmt.Errorf("secretRef with github app data must be specified when provider is set to github: %w", ErrInvalidSourceConfiguration)
}
getCreds = func() (*authutils.GitCredentials, error) {
var appOpts []github.OptFunc
appOpts = append(appOpts, github.WithAppData(authMethods.GitHubAppData))
if proxyURL != nil {
appOpts = append(appOpts, github.WithProxyURL(proxyURL))
}
if srcOpts.tokenCache != nil {
appOpts = append(appOpts, github.WithCache(srcOpts.tokenCache, imagev1.ImageUpdateAutomationKind,
srcOpts.objName, srcOpts.objNamespace, cache.OperationReconcile))
}
if authMethods.HasTLS() {
appOpts = append(appOpts, github.WithTLSConfig(authMethods.TLS))
}
username, password, err := github.GetCredentials(ctx, appOpts...)
if err != nil {
return nil, err
}
return &authutils.GitCredentials{
Username: username,
Password: password,
}, nil
}
default:
// analyze secret, if it has github app data, perhaps provider should have been github.
if appID := data[github.KeyAppID]; len(appID) != 0 {
return nil, fmt.Errorf("secretRef '%s/%s' has github app data but provider is not set to github: %w", repo.GetNamespace(), repo.Spec.SecretRef.Name, ErrInvalidSourceConfiguration)
}
}
if getCreds != nil {
creds, err := getCreds()
if err != nil {
return nil, fmt.Errorf("failed to configure authentication options: %w", err)
}
opts.BearerToken = creds.BearerToken
opts.Username = creds.Username
opts.Password = creds.Password
}
return opts, nil
}
func getSigningEntity(ctx context.Context, c client.Client, namespace string, gitSpec *imagev1.GitSpec) (*openpgp.Entity, error) {
secretName := gitSpec.Commit.SigningKey.SecretRef.Name
secretData, err := getSecretData(ctx, c, secretName, namespace)
if err != nil {
return nil, fmt.Errorf("could not find signing key secret '%s': %w", secretName, err)
}
data, ok := secretData[signingSecretKey]
if !ok {
return nil, fmt.Errorf("signing key secret '%s' does not contain a 'git.asc' key", secretName)
}
// Read entity from secret value
entities, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("could not read signing key from secret '%s': %w", secretName, err)
}
if len(entities) > 1 {
return nil, fmt.Errorf("multiple entities read from secret '%s', could not determine which signing key to use", secretName)
}
entity := entities[0]
if entity.PrivateKey != nil && entity.PrivateKey.Encrypted {
passphrase, ok := secretData[signingPassphraseKey]
if !ok {
return nil, fmt.Errorf("can not use passphrase protected signing key without '%s' field present in secret %s",
"passphrase", secretName)
}
if err = entity.PrivateKey.Decrypt([]byte(passphrase)); err != nil {
return nil, fmt.Errorf("could not decrypt private key of the signing key present in secret %s: %w", secretName, err)
}
}
return entity, nil
}
func getSecretData(ctx context.Context, c client.Client, name, namespace string) (map[string][]byte, error) {
secret, err := getSecret(ctx, c, name, namespace)
if err != nil {
return nil, err
}
return secret.Data, nil
}
func getSecret(ctx context.Context, c client.Client, name, namespace string) (*corev1.Secret, error) {
key := types.NamespacedName{
Namespace: namespace,
Name: name,
}
secret := &corev1.Secret{}
if err := c.Get(ctx, key, secret); err != nil {
return nil, err
}
return secret, nil
}

View File

@ -1,616 +0,0 @@
/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package source
import (
"context"
"fmt"
"testing"
"time"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/github"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
imagev1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
"github.com/fluxcd/image-automation-controller/internal/testutil"
)
func Test_getAuthOpts(t *testing.T) {
namespace := "default"
invalidAuthSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "invalid-auth",
Namespace: namespace,
},
Data: map[string][]byte{
"password": []byte("pass"),
},
}
validAuthSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "valid-auth",
Namespace: namespace,
},
Data: map[string][]byte{
"username": []byte("user"),
"password": []byte("pass"),
},
}
tests := []struct {
name string
url string
secretName string
want *git.AuthOptions
wantErr bool
}{
{
name: "non-existing secret",
secretName: "non-existing",
want: nil,
wantErr: true,
},
{
name: "invalid secret",
url: "https://example.com",
secretName: "invalid-auth",
want: nil,
wantErr: true,
},
{
name: "valid secret",
url: "https://example.com",
secretName: "valid-auth",
want: &git.AuthOptions{
Transport: git.HTTPS,
Host: "example.com",
Username: "user",
Password: "pass",
},
wantErr: false,
},
{
name: "no secret",
url: "https://example.com",
want: &git.AuthOptions{
Transport: git.HTTPS,
Host: "example.com",
},
wantErr: false,
},
{
name: "invalid URL",
url: "://example.com",
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
clientBuilder := fakeclient.NewClientBuilder().
WithScheme(scheme.Scheme).
WithObjects(invalidAuthSecret, validAuthSecret)
c := clientBuilder.Build()
gitRepo := &sourcev1.GitRepository{}
gitRepo.Namespace = namespace
gitRepo.Spec = sourcev1.GitRepositorySpec{
URL: tt.url,
}
if tt.secretName != "" {
gitRepo.Spec.SecretRef = &meta.LocalObjectReference{Name: tt.secretName}
}
got, err := getAuthOpts(context.TODO(), c, gitRepo, SourceOptions{}, nil)
if (err != nil) != tt.wantErr {
g.Fail(fmt.Sprintf("unexpected error: %v", err))
return
}
g.Expect(got).To(Equal(tt.want))
})
}
}
func Test_getAuthOpts_providerAuth(t *testing.T) {
tests := []struct {
name string
url string
secret *corev1.Secret
beforeFunc func(obj *sourcev1.GitRepository)
wantErr string
}{
{
name: "azure provider",
url: "https://dev.azure.com/foo/bar/_git/baz",
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Provider = sourcev1.GitProviderAzure
},
wantErr: "ManagedIdentityCredential",
},
{
name: "github provider with no secret ref",
url: "https://github.com/org/repo.git",
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Provider = sourcev1.GitProviderGitHub
},
wantErr: "secretRef with github app data must be specified when provider is set to github: invalid source configuration",
},
{
name: "github provider with secret ref that does not exist",
url: "https://github.com/org/repo.git",
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Provider = sourcev1.GitProviderGitHub
obj.Spec.SecretRef = &meta.LocalObjectReference{
Name: "githubAppSecret",
}
},
wantErr: "failed to get auth secret '/githubAppSecret': secrets \"githubAppSecret\" not found",
},
{
name: "github provider with github app data in secret",
url: "https://example.com/org/repo",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "githubAppSecret",
},
Data: map[string][]byte{
github.KeyAppID: []byte("123"),
github.KeyAppInstallationID: []byte("456"),
github.KeyAppPrivateKey: []byte("abc"),
},
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Provider = sourcev1.GitProviderGitHub
obj.Spec.SecretRef = &meta.LocalObjectReference{
Name: "githubAppSecret",
}
},
wantErr: "Key must be a PEM encoded PKCS1 or PKCS8 key",
},
{
name: "github provider with basic auth in secret",
url: "https://example.com/org/repo",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "basic-auth-secret",
},
Data: map[string][]byte{
"username": []byte("abc"),
"password": []byte(""),
},
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Provider = sourcev1.GitProviderGitHub
obj.Spec.SecretRef = &meta.LocalObjectReference{
Name: "basic-auth-secret",
}
},
wantErr: "secretRef with github app data must be specified when provider is set to github",
},
{
name: "generic provider with github app data in secret",
url: "https://example.com/org/repo",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "githubAppSecret",
},
Data: map[string][]byte{
github.KeyAppID: []byte("123"),
},
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Provider = sourcev1.GitProviderGeneric
obj.Spec.SecretRef = &meta.LocalObjectReference{
Name: "githubAppSecret",
}
},
wantErr: "secretRef '/githubAppSecret' has github app data but provider is not set to github: invalid source configuration",
},
{
name: "generic provider",
url: "https://example.com/org/repo",
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Provider = sourcev1.GitProviderGeneric
},
},
{
name: "no provider",
url: "https://example.com/org/repo",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
clientBuilder := fakeclient.NewClientBuilder().
WithScheme(scheme.Scheme).
WithStatusSubresource(&sourcev1.GitRepository{})
if tt.secret != nil {
clientBuilder.WithObjects(tt.secret)
}
c := clientBuilder.Build()
obj := &sourcev1.GitRepository{
Spec: sourcev1.GitRepositorySpec{
URL: tt.url,
},
}
if tt.beforeFunc != nil {
tt.beforeFunc(obj)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
opts, err := getAuthOpts(ctx, c, obj, SourceOptions{}, nil)
if tt.wantErr != "" {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
} else {
g.Expect(err).ToNot(HaveOccurred())
g.Expect(opts).ToNot(BeNil())
g.Expect(opts.BearerToken).To(BeEmpty())
g.Expect(opts.Username).To(BeEmpty())
g.Expect(opts.Password).To(BeEmpty())
}
})
}
}
func Test_getSigningEntity(t *testing.T) {
g := NewWithT(t)
namespace := "default"
passphrase := "abcde12345"
_, keyEncrypted := testutil.GetSigningKeyPair(g, passphrase)
encryptedKeySecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "encrypted-key",
Namespace: namespace,
},
Data: map[string][]byte{
signingSecretKey: keyEncrypted,
signingPassphraseKey: []byte(passphrase),
},
}
_, keyUnencrypted := testutil.GetSigningKeyPair(g, "")
unencryptedKeySecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "unencrypted-key",
Namespace: namespace,
},
Data: map[string][]byte{
signingSecretKey: keyUnencrypted,
},
}
tests := []struct {
name string
secretName string
wantErr bool
}{
{
name: "non-existing secret",
secretName: "non-existing",
wantErr: true,
},
{
name: "unencrypted key",
secretName: "unencrypted-key",
wantErr: false,
},
{
name: "encrypted key",
secretName: "encrypted-key",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
clientBuilder := fakeclient.NewClientBuilder().
WithScheme(scheme.Scheme).
WithObjects(encryptedKeySecret, unencryptedKeySecret)
c := clientBuilder.Build()
gitSpec := &imagev1.GitSpec{}
if tt.secretName != "" {
gitSpec.Commit = imagev1.CommitSpec{
SigningKey: &imagev1.SigningKey{
SecretRef: meta.LocalObjectReference{Name: tt.secretName},
},
}
}
_, err := getSigningEntity(context.TODO(), c, namespace, gitSpec)
if (err != nil) != tt.wantErr {
g.Fail(fmt.Sprintf("unexpected error: %v", err))
return
}
})
}
}
func Test_buildGitConfig(t *testing.T) {
testGitRepoName := "test-gitrepo"
namespace := "foo-ns"
testTimeout := &metav1.Duration{Duration: time.Minute}
testGitURL := "https://example.com"
tests := []struct {
name string
gitSpec *imagev1.GitSpec
gitRepoName string
gitRepoRef *sourcev1.GitRepositoryRef
gitRepoTimeout *metav1.Duration
gitRepoURL string
gitRepoProxyData map[string][]byte
srcOpts SourceOptions
wantErr bool
wantCheckoutRef *sourcev1.GitRepositoryRef
wantPushBranch string
wantSwitchBranch bool
wantTimeout *metav1.Duration
}{
{
name: "same branch, gitSpec checkoutRef",
gitSpec: &imagev1.GitSpec{
Checkout: &imagev1.GitCheckoutSpec{
Reference: sourcev1.GitRepositoryRef{Branch: "aaa"},
},
},
gitRepoName: testGitRepoName,
gitRepoURL: testGitURL,
wantErr: false,
wantCheckoutRef: &sourcev1.GitRepositoryRef{
Branch: "aaa",
},
wantPushBranch: "aaa",
wantSwitchBranch: false,
wantTimeout: testTimeout,
},
{
name: "different branch, gitSpec checkoutRef",
gitSpec: &imagev1.GitSpec{
Checkout: &imagev1.GitCheckoutSpec{
Reference: sourcev1.GitRepositoryRef{Branch: "aaa"},
},
Push: &imagev1.PushSpec{
Branch: "bbb",
},
},
gitRepoName: testGitRepoName,
gitRepoURL: testGitURL,
wantErr: false,
wantCheckoutRef: &sourcev1.GitRepositoryRef{
Branch: "aaa",
},
wantPushBranch: "bbb",
wantSwitchBranch: true,
wantTimeout: testTimeout,
},
{
name: "same branch, gitrepo checkoutRef",
gitSpec: &imagev1.GitSpec{},
gitRepoName: testGitRepoName,
gitRepoURL: testGitURL,
gitRepoRef: &sourcev1.GitRepositoryRef{
Branch: "ccc",
},
wantErr: false,
wantCheckoutRef: &sourcev1.GitRepositoryRef{
Branch: "ccc",
},
wantPushBranch: "ccc",
wantSwitchBranch: false,
wantTimeout: testTimeout,
},
{
name: "different branch, gitrepo checkoutRef",
gitSpec: &imagev1.GitSpec{
Push: &imagev1.PushSpec{
Branch: "ddd",
},
},
gitRepoName: testGitRepoName,
gitRepoURL: testGitURL,
gitRepoRef: &sourcev1.GitRepositoryRef{
Branch: "ccc",
},
wantErr: false,
wantCheckoutRef: &sourcev1.GitRepositoryRef{
Branch: "ccc",
},
wantPushBranch: "ddd",
wantSwitchBranch: true,
wantTimeout: testTimeout,
},
{
name: "no checkoutRef defined",
gitSpec: &imagev1.GitSpec{
Push: &imagev1.PushSpec{
Branch: "aaa",
},
},
gitRepoName: testGitRepoName,
gitRepoURL: testGitURL,
wantErr: false,
wantCheckoutRef: nil, // Use the git default checkout branch.
wantPushBranch: "aaa",
wantSwitchBranch: true,
wantTimeout: testTimeout,
},
{
name: "gitSpec override gitRepo checkout config",
gitSpec: &imagev1.GitSpec{
Checkout: &imagev1.GitCheckoutSpec{
Reference: sourcev1.GitRepositoryRef{Branch: "aaa"},
},
Push: &imagev1.PushSpec{
Branch: "bbb",
},
},
gitRepoName: testGitRepoName,
gitRepoURL: testGitURL,
gitRepoRef: &sourcev1.GitRepositoryRef{
Branch: "ccc",
},
wantErr: false,
wantCheckoutRef: &sourcev1.GitRepositoryRef{
Branch: "aaa",
},
wantPushBranch: "bbb",
wantSwitchBranch: true,
wantTimeout: testTimeout,
},
{
name: "non-existing gitRepo",
gitSpec: &imagev1.GitSpec{},
wantErr: true,
},
{
name: "use gitrepo timeout",
gitSpec: &imagev1.GitSpec{},
gitRepoName: testGitRepoName,
gitRepoURL: testGitURL,
gitRepoRef: &sourcev1.GitRepositoryRef{
Branch: "ccc",
},
gitRepoTimeout: &metav1.Duration{Duration: 30 * time.Second},
wantErr: false,
wantCheckoutRef: &sourcev1.GitRepositoryRef{
Branch: "ccc",
},
wantPushBranch: "ccc",
wantSwitchBranch: false,
wantTimeout: &metav1.Duration{Duration: 30 * time.Second},
},
{
name: "bad git URL",
gitSpec: &imagev1.GitSpec{},
gitRepoName: testGitRepoName,
gitRepoURL: "://example.com",
gitRepoRef: &sourcev1.GitRepositoryRef{
Branch: "ccc",
},
wantErr: true,
},
{
name: "proxy config",
gitSpec: &imagev1.GitSpec{},
gitRepoName: testGitRepoName,
gitRepoURL: testGitURL,
gitRepoRef: &sourcev1.GitRepositoryRef{
Branch: "ccc",
},
gitRepoProxyData: map[string][]byte{
"address": []byte("http://example.com"),
},
wantErr: false,
wantCheckoutRef: &sourcev1.GitRepositoryRef{
Branch: "ccc",
},
wantPushBranch: "ccc",
wantSwitchBranch: false,
wantTimeout: testTimeout,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
testObjects := []client.Object{}
var proxySecret *corev1.Secret
if tt.gitRepoProxyData != nil {
proxySecret = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "valid-proxy",
Namespace: namespace,
},
Data: tt.gitRepoProxyData,
}
testObjects = append(testObjects, proxySecret)
}
var gitRepo *sourcev1.GitRepository
if tt.gitRepoName != "" {
gitRepo = &sourcev1.GitRepository{}
gitRepo.Name = testGitRepoName
gitRepo.Namespace = namespace
gitRepo.Spec = sourcev1.GitRepositorySpec{}
if tt.gitRepoURL != "" {
gitRepo.Spec.URL = tt.gitRepoURL
}
if tt.gitRepoRef != nil {
gitRepo.Spec.Reference = tt.gitRepoRef
}
if tt.gitRepoTimeout != nil {
gitRepo.Spec.Timeout = tt.gitRepoTimeout
}
if proxySecret != nil {
gitRepo.Spec.ProxySecretRef = &meta.LocalObjectReference{Name: proxySecret.Name}
}
testObjects = append(testObjects, gitRepo)
}
clientBuilder := fakeclient.NewClientBuilder().
WithScheme(scheme.Scheme).
WithObjects(testObjects...)
c := clientBuilder.Build()
gitRepoKey := types.NamespacedName{
Namespace: namespace,
Name: tt.gitRepoName,
}
updateAutoKey := types.NamespacedName{
Namespace: namespace,
Name: "test-update",
}
gitSrcCfg, err := buildGitConfig(context.TODO(), c, updateAutoKey, gitRepoKey, tt.gitSpec, tt.srcOpts)
if (err != nil) != tt.wantErr {
g.Fail(fmt.Sprintf("unexpected error: %v", err))
return
}
if err == nil {
g.Expect(gitSrcCfg.checkoutRef).To(Equal(tt.wantCheckoutRef), "unexpected checkoutRef")
g.Expect(gitSrcCfg.pushBranch).To(Equal(tt.wantPushBranch), "unexpected push branch")
g.Expect(gitSrcCfg.switchBranch).To(Equal(tt.wantSwitchBranch), "unexpected switch branch")
g.Expect(gitSrcCfg.timeout).To(Equal(tt.wantTimeout), "unexpected git operation timeout")
}
})
}
}

View File

@ -1,483 +0,0 @@
/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package source
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"text/template"
"time"
"github.com/Masterminds/sprig/v3"
"github.com/fluxcd/pkg/cache"
"github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/gogit"
"github.com/fluxcd/pkg/git/repository"
"github.com/fluxcd/pkg/runtime/acl"
"github.com/go-git/go-git/v5/plumbing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
"github.com/fluxcd/pkg/runtime/logger"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
imagev1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
"github.com/fluxcd/image-automation-controller/internal/update"
)
// ErrInvalidSourceConfiguration is an error for invalid source configuration.
var ErrInvalidSourceConfiguration = errors.New("invalid source configuration")
// RemovedTemplateFieldError represents an error when a removed template field is used.
type RemovedTemplateFieldError struct {
Field string
}
func (e *RemovedTemplateFieldError) Error() string {
switch e.Field {
case ".Updated":
return "template uses removed '.Updated' field. Please use '.Changed' instead. See: https://fluxcd.io/flux/components/image/imageupdateautomations/#message-template"
case ".Changed.ImageResult":
return "template uses removed '.Changed.ImageResult' field. Please use '.Changed.FileChanges' or '.Changed.Objects' instead. See: https://fluxcd.io/flux/components/image/imageupdateautomations/#message-template"
default:
return fmt.Sprintf("template uses removed '%s' field. See: https://fluxcd.io/flux/components/image/imageupdateautomations/#message-template", e.Field)
}
}
func (e *RemovedTemplateFieldError) Is(target error) bool {
return errors.Is(target, ErrRemovedTemplateField)
}
// ErrRemovedTemplateField is a sentinel error for removed template field usage.
var ErrRemovedTemplateField = &RemovedTemplateFieldError{}
const defaultMessageTemplate = `Update from image update automation`
// TemplateData is the type of the value given to the commit message
// template.
type TemplateData struct {
AutomationObject types.NamespacedName
Changed update.Result
Values map[string]string
}
// SourceManager manages source.
type SourceManager struct {
srcCfg *gitSrcCfg
automationObjKey types.NamespacedName
gitClient *gogit.Client
workingDir string
}
// SourceOptions contains the optional attributes of SourceManager.
type SourceOptions struct {
noCrossNamespaceRef bool
gitAllBranchReferences bool
tokenCache *cache.TokenCache
objName string
objNamespace string
}
// SourceOption configures the SourceManager options.
type SourceOption func(*SourceOptions)
// WithSourceOptionNoCrossNamespaceRef configures the SourceManager to disable
// cross namespace references.
func WithSourceOptionNoCrossNamespaceRef() SourceOption {
return func(so *SourceOptions) {
so.noCrossNamespaceRef = true
}
}
// WithSourceOptionGitAllBranchReferences configures the SourceManager to fetch
// all the Git branch references that are present in the remote repository.
func WithSourceOptionGitAllBranchReferences() SourceOption {
return func(so *SourceOptions) {
so.gitAllBranchReferences = true
}
}
// WithSourceOptionTokenCache configures the SourceManager to use the provided
// token cache.
func WithSourceOptionTokenCache(tc *cache.TokenCache) SourceOption {
return func(so *SourceOptions) {
so.tokenCache = tc
}
}
// WithSourceOptionInvolvedObject configures the SourceManager to use the
// provided ImageUpdateAutomation object.
func WithSourceOptionInvolvedObject(name, namespace string) SourceOption {
return func(so *SourceOptions) {
so.objName = name
so.objNamespace = namespace
}
}
// NewSourceManager takes all the provided inputs, validates them and returns a
// SourceManager which can be used to operate on the configured source.
func NewSourceManager(ctx context.Context, c client.Client, obj *imagev1.ImageUpdateAutomation, options ...SourceOption) (*SourceManager, error) {
opts := &SourceOptions{}
for _, o := range options {
o(opts)
}
// Only GitRepository source is supported.
if obj.Spec.SourceRef.Kind != sourcev1.GitRepositoryKind {
return nil, fmt.Errorf("source kind '%s' not supported: %w", obj.Spec.SourceRef.Kind, ErrInvalidSourceConfiguration)
}
if obj.Spec.GitSpec == nil {
return nil, fmt.Errorf("source kind '%s' necessitates field .spec.git: %w", sourcev1.GitRepositoryKind, ErrInvalidSourceConfiguration)
}
// Build source reference configuration to fetch and validate it.
srcNamespace := obj.GetNamespace()
if obj.Spec.SourceRef.Namespace != "" {
srcNamespace = obj.Spec.SourceRef.Namespace
}
// srcKey is the GitRepository object key.
srcKey := types.NamespacedName{Name: obj.Spec.SourceRef.Name, Namespace: srcNamespace}
// originKey is the update automation object key.
originKey := client.ObjectKeyFromObject(obj)
// Check if the source is accessible.
if opts.noCrossNamespaceRef && srcKey.Namespace != obj.GetNamespace() {
return nil, acl.AccessDeniedError(fmt.Sprintf("can't access '%s/%s', cross-namespace references have been blocked", sourcev1.GitRepositoryKind, srcKey))
}
gitSrcCfg, err := buildGitConfig(ctx, c, originKey, srcKey, obj.Spec.GitSpec, *opts)
if err != nil {
return nil, err
}
workDir, err := os.MkdirTemp("", fmt.Sprintf("%s-%s", gitSrcCfg.srcKey.Namespace, gitSrcCfg.srcKey.Name))
if err != nil {
return nil, err
}
sm := &SourceManager{
srcCfg: gitSrcCfg,
automationObjKey: originKey,
workingDir: workDir,
}
return sm, nil
}
// CreateWorkingDirectory creates a working directory for the SourceManager.
func (sm SourceManager) WorkDirectory() string {
return sm.workingDir
}
// Cleanup deletes the working directory of the SourceManager.
func (sm SourceManager) Cleanup() error {
return os.RemoveAll(sm.workingDir)
}
// SwitchBranch returns if the checkout branch and push branch are different.
func (sm SourceManager) SwitchBranch() bool {
return sm.srcCfg.switchBranch
}
// CheckoutOption allows configuring the checkout options.
type CheckoutOption func(*repository.CloneConfig)
// WithCheckoutOptionLastObserved is a CheckoutOption option to configure the
// last observed commit.
func WithCheckoutOptionLastObserved(commit string) CheckoutOption {
return func(cc *repository.CloneConfig) {
cc.LastObservedCommit = commit
}
}
// WithCheckoutOptionShallowClone is a CheckoutOption option to configure
// shallow clone.
func WithCheckoutOptionShallowClone() CheckoutOption {
return func(cc *repository.CloneConfig) {
cc.ShallowClone = true
}
}
// WithCheckoutOptionSparseCheckoutDirectories is a CheckoutOption option to configure
// SparseCheckoutDirectories.
func WithCheckoutOptionSparseCheckoutDirectories(updatePath string) CheckoutOption {
return func(cc *repository.CloneConfig) {
cleanedPath := filepath.Clean(updatePath)
if cleanedPath == "." {
// Do not set SparseCheckoutDirectories if repository root is specified
return
}
cc.SparseCheckoutDirectories = []string{cleanedPath}
}
}
// CheckoutSource clones and checks out the source. If a push branch is
// configured that doesn't match with the checkout branch, a checkout to the
// push branch is also performed. This ensures any change and push operation
// following the checkout happens on the push branch.
func (sm *SourceManager) CheckoutSource(ctx context.Context, options ...CheckoutOption) (*git.Commit, error) {
// Configuration clone options.
cloneCfg := repository.CloneConfig{}
if sm.srcCfg.checkoutRef != nil {
cloneCfg.Tag = sm.srcCfg.checkoutRef.Tag
cloneCfg.SemVer = sm.srcCfg.checkoutRef.SemVer
cloneCfg.Commit = sm.srcCfg.checkoutRef.Commit
cloneCfg.Branch = sm.srcCfg.checkoutRef.Branch
}
// Apply checkout configurations.
for _, o := range options {
o(&cloneCfg)
}
var err error
sm.gitClient, err = gogit.NewClient(sm.workingDir, sm.srcCfg.authOpts, sm.srcCfg.clientOpts...)
if err != nil {
return nil, err
}
gitOpCtx, cancel := context.WithTimeout(ctx, sm.srcCfg.timeout.Duration)
defer cancel()
commit, err := sm.gitClient.Clone(gitOpCtx, sm.srcCfg.url, cloneCfg)
if err != nil {
return nil, err
}
if sm.srcCfg.switchBranch {
if err := sm.gitClient.SwitchBranch(gitOpCtx, sm.srcCfg.pushBranch); err != nil {
return nil, err
}
}
return commit, nil
}
// PushConfig configures the options used in push operation.
type PushConfig func(*repository.PushConfig)
// WithPushConfigForce configures the PushConfig to use force.
func WithPushConfigForce() PushConfig {
return func(pc *repository.PushConfig) {
pc.Force = true
}
}
// WithPushConfigOptions configures the PushConfig Options that are used in
// push.
func WithPushConfigOptions(opts map[string]string) PushConfig {
return func(pc *repository.PushConfig) {
pc.Options = opts
}
}
// CommitAndPush performs a commit in the source and pushes it to the remote
// repository.
func (sm SourceManager) CommitAndPush(ctx context.Context, obj *imagev1.ImageUpdateAutomation, policyResult update.Result, pushOptions ...PushConfig) (*PushResult, error) {
tracelog := log.FromContext(ctx).V(logger.TraceLevel)
// Make sure there were file changes that need to be committed.
if len(policyResult.FileChanges) == 0 {
return nil, nil
}
// Perform a Git commit.
templateValues := &TemplateData{
AutomationObject: sm.automationObjKey,
Changed: policyResult,
Values: obj.Spec.GitSpec.Commit.MessageTemplateValues,
}
commitMsg, err := templateMsg(obj.Spec.GitSpec.Commit.MessageTemplate, templateValues)
if err != nil {
return nil, err
}
signature := git.Signature{
Name: obj.Spec.GitSpec.Commit.Author.Name,
Email: obj.Spec.GitSpec.Commit.Author.Email,
When: time.Now(),
}
var rev string
var commitErr error
rev, commitErr = sm.gitClient.Commit(
git.Commit{
Author: signature,
Message: commitMsg,
},
repository.WithSigner(sm.srcCfg.signingEntity),
)
if commitErr != nil {
if !errors.Is(commitErr, git.ErrNoStagedFiles) {
return nil, commitErr
}
log.FromContext(ctx).Info("no changes made in the source; no commit")
return nil, nil
}
// Push the commit to push branch.
gitOpCtx, cancel := context.WithTimeout(ctx, sm.srcCfg.timeout.Duration)
defer cancel()
pushConfig := repository.PushConfig{}
for _, po := range pushOptions {
po(&pushConfig)
}
if err := sm.gitClient.Push(gitOpCtx, pushConfig); err != nil {
return nil, err
}
tracelog.Info("pushed commit to push branch", "revision", rev, "branch", sm.srcCfg.pushBranch)
// Push to any provided refspec.
if obj.Spec.GitSpec.HasRefspec() {
pushConfig.Refspecs = append(pushConfig.Refspecs, obj.Spec.GitSpec.Push.Refspec)
if err := sm.gitClient.Push(gitOpCtx, pushConfig); err != nil {
return nil, err
}
tracelog.Info("pushed commit to refspec", "revision", rev, "refspecs", pushConfig.Refspecs)
}
// Construct the result of the push operation and return.
prOpts := []PushResultOption{WithPushResultRefspec(pushConfig.Refspecs)}
if sm.srcCfg.switchBranch {
prOpts = append(prOpts, WithPushResultSwitchBranch())
}
return NewPushResult(sm.srcCfg.pushBranch, rev, commitMsg, prOpts...)
}
// templateMsg renders a msg template, returning the message or an error.
func templateMsg(messageTemplate string, templateValues *TemplateData) (string, error) {
if messageTemplate == "" {
messageTemplate = defaultMessageTemplate
}
// Includes only functions that are guaranteed to always evaluate to the same result for given input.
// This removes the possibility of accidentally relying on where or when the template runs.
// https://github.com/Masterminds/sprig/blob/3ac42c7bc5e4be6aa534e036fb19dde4a996da2e/functions.go#L70
t, err := template.New("commit message").Funcs(sprig.HermeticTxtFuncMap()).Parse(messageTemplate)
if err != nil {
return "", fmt.Errorf("unable to create commit message template from spec: %w", err)
}
b := &strings.Builder{}
if err := t.Execute(b, *templateValues); err != nil {
if removedFieldErr := checkRemovedTemplateField(err); removedFieldErr != nil {
return "", removedFieldErr
}
return "", fmt.Errorf("failed to run template from spec: %w", err)
}
return b.String(), nil
}
// checkRemovedTemplateField checks if the template error is due to removed fields
func checkRemovedTemplateField(err error) error {
removedFieldChecks := []struct {
fieldName string
errorPattern string
}{
{".Updated", "can't evaluate field Updated in type source.TemplateData"},
{".Changed.ImageResult", "can't evaluate field ImageResult in type update.Result"},
}
for _, check := range removedFieldChecks {
if strings.Contains(err.Error(), check.errorPattern) {
return &RemovedTemplateFieldError{Field: check.fieldName}
}
}
return nil
}
// PushResultOption allows configuring the options of PushResult.
type PushResultOption func(*PushResult)
// WithPushResultSwitchBranch marks the PushResult with switchBranch.
func WithPushResultSwitchBranch() func(*PushResult) {
return func(pr *PushResult) {
pr.switchBranch = true
}
}
// WithPushResultRefspec sets the refspecs in the PushResult.
func WithPushResultRefspec(refspecs []string) func(*PushResult) {
return func(pr *PushResult) {
pr.refspecs = append(pr.refspecs, refspecs...)
}
}
// PushResult is the result of a push operation.
type PushResult struct {
commit *git.Commit
switchBranch bool
branch string
refspecs []string
creationTime *metav1.Time
}
// NewPushResult returns a new PushResult.
func NewPushResult(branch string, rev string, commitMsg string, opts ...PushResultOption) (*PushResult, error) {
if rev == "" {
return nil, errors.New("empty push commit revision")
}
pr := &PushResult{}
for _, o := range opts {
o(pr)
}
pr.commit = &git.Commit{
Hash: git.ExtractHashFromRevision(rev),
Reference: plumbing.NewBranchReferenceName(branch).String(),
Message: commitMsg,
}
pr.branch = branch
pr.creationTime = &metav1.Time{Time: time.Now()}
return pr, nil
}
// Commit returns the revision of the pushed commit.
func (pr PushResult) Commit() *git.Commit {
return pr.commit
}
// Time returns the time at which the push was performed.
func (pr PushResult) Time() *metav1.Time {
return pr.creationTime
}
// SwitchBranch returns if the source has different checkout and push branch.
func (pr PushResult) SwitchBranch() bool {
return pr.switchBranch
}
// Summary returns a summary of the PushResult.
func (pr PushResult) Summary() string {
var summary strings.Builder
shortCommitHash := pr.Commit().Hash.String()
if len(shortCommitHash) > 7 {
shortCommitHash = shortCommitHash[:7]
}
summary.WriteString(fmt.Sprintf("pushed commit '%s' to branch '%s'", shortCommitHash, pr.branch))
if len(pr.refspecs) > 0 {
summary.WriteString(fmt.Sprintf(" and refspecs '%s'", strings.Join(pr.refspecs, "', '")))
}
if pr.Commit().Message != "" {
summary.WriteString(fmt.Sprintf("\n%s", pr.Commit().Message))
}
return summary.String()
}

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: test
spec:
template:
spec:
containers:
- name: hello
image: helloworld:1.0.0 # SETTER_SITE

View File

@ -1,69 +0,0 @@
/*
Copyright 2020 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package testutil
import (
"testing"
. "github.com/onsi/gomega"
)
func TestExpectMatchingDirectories(t *testing.T) {
tests := []struct {
name string
actualRoot string
expectedRoot string
}{
{
name: "same directory",
actualRoot: "testdata/base",
expectedRoot: "testdata/base",
},
{
name: "different equivalent directories",
actualRoot: "testdata/base",
expectedRoot: "testdata/equiv",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
ExpectMatchingDirectories(g, tt.actualRoot, tt.expectedRoot)
})
}
}
func TestDiffDirectories(t *testing.T) {
g := NewWithT(t)
// Finds files in actual a/ that weren't expected from b/.
actualonly, _, _ := DiffDirectories("testdata/diff/a", "testdata/diff/b")
g.Expect(actualonly).To(Equal([]string{"/only", "/onlyhere.yaml"}))
// Finds files in expected from a/ but not in actual b/.
_, expectedonly, _ := DiffDirectories("testdata/diff/b", "testdata/diff/a") // NB change in order
g.Expect(expectedonly).To(Equal([]string{"/only", "/onlyhere.yaml"}))
// Finds files that are different in a and b.
_, _, diffs := DiffDirectories("testdata/diff/a", "testdata/diff/b")
var diffpaths []string
for _, d := range diffs {
diffpaths = append(diffpaths, d.Path())
}
g.Expect(diffpaths).To(Equal([]string{"/different/content.yaml", "/dirfile"}))
}

View File

@ -1,481 +0,0 @@
/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package testutil
import (
"bytes"
"context"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/go-git/go-billy/v5/osfs"
extgogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/cache"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/storage/filesystem"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/rand"
"github.com/fluxcd/pkg/gittestserver"
reflectorv1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
"github.com/fluxcd/image-automation-controller/internal/constants"
)
const (
signingSecretKey = "git.asc"
signingPassphraseKey = "passphrase"
)
func CheckoutBranch(g *WithT, repo *extgogit.Repository, branch string) {
g.THelper()
wt, err := repo.Worktree()
g.Expect(err).ToNot(HaveOccurred())
err = wt.Checkout(&extgogit.CheckoutOptions{
Branch: plumbing.NewBranchReferenceName(branch),
})
g.Expect(err).ToNot(HaveOccurred())
}
func ReplaceMarker(path string, policyKey types.NamespacedName) error {
return ReplaceMarkerWithMarker(path, policyKey, "SETTER_SITE")
}
func ReplaceMarkerWithMarker(path string, policyKey types.NamespacedName, marker string) error {
filebytes, err := os.ReadFile(path)
if err != nil {
return err
}
newfilebytes := bytes.ReplaceAll(filebytes, []byte(marker), []byte(setterRef(policyKey)))
if err = os.WriteFile(path, newfilebytes, os.FileMode(0666)); err != nil {
return err
}
return nil
}
func setterRef(name types.NamespacedName) string {
return fmt.Sprintf(`{"%s": "%s:%s"}`, constants.SetterShortHand, name.Namespace, name.Name)
}
func CommitInRepo(ctx context.Context, g *WithT, repoURL, branch, remote, msg string, changeFiles func(path string)) plumbing.Hash {
g.THelper()
repo, cloneDir, err := Clone(ctx, repoURL, branch, remote)
g.Expect(err).ToNot(HaveOccurred())
defer func() { os.RemoveAll(cloneDir) }()
wt, err := repo.Worktree()
g.Expect(err).ToNot(HaveOccurred())
changeFiles(wt.Filesystem.Root())
id := CommitWorkDir(g, repo, branch, msg)
origin, err := repo.Remote(remote)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(origin.Push(&extgogit.PushOptions{
RemoteName: remote,
RefSpecs: []config.RefSpec{config.RefSpec(BranchRefName(branch))},
})).To(Succeed())
return id
}
func WaitForNewHead(g *WithT, repo *extgogit.Repository, branch, remote, preChangeHash string) {
g.THelper()
var commitToResetTo *object.Commit
origin, err := repo.Remote(remote)
g.Expect(err).ToNot(HaveOccurred())
// Now try to fetch new commits from that remote branch
g.Eventually(func() bool {
err := origin.Fetch(&extgogit.FetchOptions{
RemoteName: remote,
RefSpecs: []config.RefSpec{config.RefSpec(BranchRefName(branch))},
})
if err != nil {
return false
}
wt, err := repo.Worktree()
if err != nil {
return false
}
err = wt.Checkout(&extgogit.CheckoutOptions{
Branch: plumbing.NewBranchReferenceName(branch),
})
if err != nil {
return false
}
remoteHeadRef, err := repo.Head()
if err != nil {
return false
}
remoteHeadHash := remoteHeadRef.Hash()
if preChangeHash != remoteHeadHash.String() {
commitToResetTo, _ = repo.CommitObject(remoteHeadHash)
return true
}
return false
}, 10*time.Second, time.Second).Should(BeTrue())
if commitToResetTo != nil {
wt, err := repo.Worktree()
g.Expect(err).ToNot(HaveOccurred())
// New commits in the remote branch -- reset the working tree head
// to that. Note this does not create a local branch tracking the
// remote, so it is a detached head.
g.Expect(wt.Reset(&extgogit.ResetOptions{
Commit: commitToResetTo.Hash,
Mode: extgogit.HardReset,
})).To(Succeed())
}
}
// Initialise a git server with a repo including the files in dir.
func InitGitRepo(g *WithT, gitServer *gittestserver.GitServer, fixture, branch, repoPath string) *extgogit.Repository {
g.THelper()
workDir, err := securejoin.SecureJoin(gitServer.Root(), repoPath)
g.Expect(err).ToNot(HaveOccurred())
repo := InitGitRepoPlain(g, fixture, workDir)
headRef, err := repo.Head()
g.Expect(err).ToNot(HaveOccurred())
ref := plumbing.NewHashReference(
plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", branch)),
headRef.Hash())
g.Expect(repo.Storer.SetReference(ref)).ToNot(HaveOccurred())
return repo
}
func InitGitRepoPlain(g *WithT, fixture, repoPath string) *extgogit.Repository {
g.THelper()
wt := osfs.New(repoPath)
dot := osfs.New(filepath.Join(repoPath, extgogit.GitDirName))
storer := filesystem.NewStorage(dot, cache.NewObjectLRUDefault())
repo, err := extgogit.Init(storer, wt)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(copyDir(fixture, repoPath)).ToNot(HaveOccurred())
_ = CommitWorkDir(g, repo, "main", "Initial commit")
g.Expect(err).ToNot(HaveOccurred())
return repo
}
func HeadFromBranch(repo *extgogit.Repository, branchName string) (*object.Commit, error) {
ref, err := repo.Storer.Reference(plumbing.ReferenceName("refs/heads/" + branchName))
if err != nil {
return nil, err
}
return repo.CommitObject(ref.Hash())
}
func CommitWorkDir(g *WithT, repo *extgogit.Repository, branchName, message string) plumbing.Hash {
g.THelper()
wt, err := repo.Worktree()
g.Expect(err).ToNot(HaveOccurred())
// Checkout to an existing branch. If this is the first commit,
// this is a no-op.
_ = wt.Checkout(&extgogit.CheckoutOptions{
Branch: plumbing.ReferenceName("refs/heads/" + branchName),
})
status, err := wt.Status()
g.Expect(err).ToNot(HaveOccurred())
for file := range status {
wt.Add(file)
}
sig := mockSignature(time.Now())
c, err := wt.Commit(message, &extgogit.CommitOptions{
All: true,
Author: sig,
Committer: sig,
})
g.Expect(err).ToNot(HaveOccurred())
_, err = repo.Branch(branchName)
if err == extgogit.ErrBranchNotFound {
ref := plumbing.NewHashReference(
plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", branchName)), c)
err = repo.Storer.SetReference(ref)
}
g.Expect(err).ToNot(HaveOccurred())
// Now the target branch exists, we can checkout to it.
err = wt.Checkout(&extgogit.CheckoutOptions{
Branch: plumbing.ReferenceName("refs/heads/" + branchName),
})
g.Expect(err).ToNot(HaveOccurred())
return c
}
func TagCommit(g *WithT, repo *extgogit.Repository, commit plumbing.Hash, annotated bool, tag string, time time.Time) (*plumbing.Reference, error) {
g.THelper()
var opts *extgogit.CreateTagOptions
if annotated {
opts = &extgogit.CreateTagOptions{
Tagger: mockSignature(time),
Message: "Annotated tag for: " + tag,
}
}
return repo.CreateTag(tag, commit, opts)
}
func copyDir(src string, dest string) error {
file, err := os.Stat(src)
if err != nil {
return err
}
if !file.IsDir() {
return fmt.Errorf("source %q must be a directory", file.Name())
}
if err = os.MkdirAll(dest, 0o755); err != nil {
return err
}
files, err := ioutil.ReadDir(src)
if err != nil {
return err
}
for _, f := range files {
srcFile := filepath.Join(src, f.Name())
destFile := filepath.Join(dest, f.Name())
if f.IsDir() {
if err = copyDir(srcFile, destFile); err != nil {
return err
}
}
if !f.IsDir() {
// ignore symlinks
if f.Mode()&os.ModeSymlink == os.ModeSymlink {
continue
}
content, err := os.ReadFile(srcFile)
if err != nil {
return err
}
if err = os.WriteFile(destFile, content, 0o755); err != nil {
return err
}
}
}
return nil
}
func BranchRefName(branch string) string {
return fmt.Sprintf("refs/heads/%s:refs/heads/%s", branch, branch)
}
func mockSignature(time time.Time) *object.Signature {
return &object.Signature{
Name: "Jane Doe",
Email: "author@example.com",
When: time,
}
}
func Clone(ctx context.Context, repoURL, branchName, remote string) (*extgogit.Repository, string, error) {
dir, err := os.MkdirTemp("", "iac-clone-*")
if err != nil {
return nil, "", err
}
opts := &extgogit.CloneOptions{
URL: repoURL,
RemoteName: remote,
ReferenceName: plumbing.NewBranchReferenceName(branchName),
}
wt := osfs.New(dir, osfs.WithBoundOS())
dot := osfs.New(filepath.Join(dir, extgogit.GitDirName), osfs.WithBoundOS())
storer := filesystem.NewStorage(dot, cache.NewObjectLRUDefault())
repo, err := extgogit.Clone(storer, wt, opts)
if err != nil {
return nil, "", err
}
w, err := repo.Worktree()
if err != nil {
return nil, "", err
}
err = w.Checkout(&extgogit.CheckoutOptions{
Branch: plumbing.NewBranchReferenceName(branchName),
Create: false,
})
if err != nil {
return nil, "", err
}
return repo, dir, nil
}
func CommitIdFromBranch(repo *extgogit.Repository, branchName string) string {
commitId := ""
head, err := HeadFromBranch(repo, branchName)
if err == nil {
commitId = head.Hash.String()
}
return commitId
}
func GetRemoteHead(repo *extgogit.Repository, branchName, remote string) (plumbing.Hash, error) {
rmt, err := repo.Remote(remote)
if err != nil {
return plumbing.ZeroHash, err
}
err = rmt.Fetch(&extgogit.FetchOptions{
RemoteName: remote,
RefSpecs: []config.RefSpec{config.RefSpec(BranchRefName(branchName))},
})
if err != nil && !errors.Is(err, extgogit.NoErrAlreadyUpToDate) {
return plumbing.ZeroHash, err
}
remoteHeadRef, err := HeadFromBranch(repo, branchName)
if err != nil {
return plumbing.ZeroHash, err
}
return remoteHeadRef.Hash, nil
}
// SetUpGitTestServer creates and returns a git test server. The caller must
// ensure it's stopped and cleaned up.
func SetUpGitTestServer(g *WithT) *gittestserver.GitServer {
g.THelper()
gitServer, err := gittestserver.NewTempGitServer()
g.Expect(err).ToNot(HaveOccurred())
username := rand.String(5)
password := rand.String(5)
gitServer.Auth(username, password)
gitServer.AutoCreate()
g.Expect(gitServer.StartHTTP()).ToNot(HaveOccurred())
gitServer.KeyDir(filepath.Join(gitServer.Root(), "keys"))
g.Expect(gitServer.ListenSSH()).ToNot(HaveOccurred())
return gitServer
}
func GetSigningKeyPairSecret(g *WithT, name, namespace string) (*corev1.Secret, *openpgp.Entity) {
g.THelper()
passphrase := "abcde12345"
pgpEntity, key := GetSigningKeyPair(g, passphrase)
// Create the secret containing signing key.
sec := &corev1.Secret{
Data: map[string][]byte{
signingSecretKey: key,
signingPassphraseKey: []byte(passphrase),
},
}
sec.Name = name
sec.Namespace = namespace
return sec, pgpEntity
}
func GetSigningKeyPair(g *WithT, passphrase string) (*openpgp.Entity, []byte) {
g.THelper()
pgpEntity, err := openpgp.NewEntity("", "", "", nil)
g.Expect(err).ToNot(HaveOccurred())
// Configure OpenPGP armor encoder.
b := bytes.NewBuffer(nil)
w, err := armor.Encode(b, openpgp.PrivateKeyType, nil)
g.Expect(err).ToNot(HaveOccurred())
// Serialize private key.
g.Expect(pgpEntity.SerializePrivate(w, nil)).To(Succeed())
g.Expect(w.Close()).To(Succeed())
if passphrase != "" {
g.Expect(pgpEntity.PrivateKey.Encrypt([]byte(passphrase))).To(Succeed())
}
return pgpEntity, b.Bytes()
}
func ImageToRef(image string) *reflectorv1.ImageRef {
var digest string
if idx := strings.LastIndex(image, "@"); idx != -1 {
image, digest = image[:idx], image[idx+1:]
}
var tag string
if idx := strings.LastIndex(image, ":"); idx != -1 {
image, tag = image[:idx], image[idx+1:]
}
return &reflectorv1.ImageRef{
Name: image,
Tag: tag,
Digest: digest,
}
}

View File

@ -1,55 +0,0 @@
/*
Copyright 2025 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package update
import (
"testing"
"github.com/go-logr/logr"
. "github.com/onsi/gomega"
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
)
func TestScreeningLocalReader(t *testing.T) {
g := NewWithT(t)
r := ScreeningLocalReader{
Path: "testdata/setters/original",
Token: "$imagepolicy",
Trace: logr.Discard(),
}
nodes, err := r.Read()
g.Expect(err).ToNot(HaveOccurred())
// the test fixture has four files that contain the marker:
// - marked.yaml
// - otherns.yaml
// - kustomization.yml
// - Kustomization
g.Expect(len(nodes)).To(Equal(4))
filesSeen := map[string]struct{}{}
for i := range nodes {
path, _, err := kioutil.GetFileAnnotations(nodes[i])
g.Expect(err).ToNot(HaveOccurred())
filesSeen[path] = struct{}{}
}
g.Expect(filesSeen).To(Equal(map[string]struct{}{
"marked.yaml": {},
"otherns.yaml": {},
"kustomization.yml": {},
"Kustomization": {},
}))
}

View File

@ -1,122 +0,0 @@
/*
Copyright 2025 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package update
import (
"testing"
"github.com/go-logr/logr"
. "github.com/onsi/gomega"
"k8s.io/kube-openapi/pkg/validation/spec"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
func TestSetAllCallbackAccept(t *testing.T) {
tests := []struct {
name string
object *yaml.RNode
settersSchema *spec.Schema
expectedError bool
}{
{
name: "Accept - Scalar Node",
object: yaml.NewRNode(&yaml.Node{
Kind: yaml.ScalarNode,
Value: "test",
}),
settersSchema: &spec.Schema{},
expectedError: false,
},
{
name: "Accept - Scalar Node - Error",
object: yaml.NewRNode(&yaml.Node{
Kind: yaml.ScalarNode,
Value: "test",
}),
settersSchema: nil,
expectedError: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
callbackInstance := SetAllCallback{
SettersSchema: test.settersSchema,
Trace: logr.Discard(),
}
err := accept(&callbackInstance, test.object, "", test.settersSchema)
g := NewWithT(t)
if test.expectedError {
g.Expect(err).To(HaveOccurred())
} else {
g.Expect(err).ToNot(HaveOccurred())
}
})
}
}
func TestGetExtFromSchema(t *testing.T) {
tests := []struct {
name string
schema *spec.Schema
expectedExtension *extension
expectedError bool
}{
{
name: "Extension Present",
schema: &spec.Schema{
VendorExtensible: spec.VendorExtensible{
Extensions: map[string]interface{}{
K8sCliExtensionKey: &extension{
Setter: &setter{
Name: "testSetter",
Value: "testValue",
},
},
},
},
},
expectedExtension: &extension{
Setter: &setter{
Name: "testSetter",
Value: "testValue",
},
},
expectedError: false,
},
{
name: "Extension Not Present",
schema: &spec.Schema{},
expectedError: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
g := NewWithT(t)
ext, err := getExtFromSchema(test.schema)
if test.expectedError {
g.Expect(err).To(HaveOccurred())
} else {
g.Expect(err).ToNot(HaveOccurred())
g.Expect(ext).To(Equal(test.expectedExtension))
}
})
}
}

View File

@ -1,133 +0,0 @@
/*
Copyright 2025 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package update
import (
"github.com/google/go-containerregistry/pkg/name"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// ImageRef represents the image reference used to replace a field
// value in an update.
type ImageRef interface {
// String returns a string representation of the image ref as it
// is used in the update; e.g., "helloworld:v1.0.1"
String() string
// Identifier returns the tag or digest; e.g., "v1.0.1"
Identifier() string
// Repository returns the repository component of the ImageRef,
// with an implied defaults, e.g., "library/helloworld"
Repository() string
// Registry returns the registry component of the ImageRef, e.g.,
// "index.docker.io"
Registry() string
// Name gives the fully-qualified reference name, e.g.,
// "index.docker.io/library/helloworld:v1.0.1"
Name() string
// Policy gives the namespaced name of the image policy that led
// to the update.
Policy() types.NamespacedName
}
type imageRef struct {
name.Reference
policy types.NamespacedName
}
// Policy gives the namespaced name of the policy that led to the
// update.
func (i imageRef) Policy() types.NamespacedName {
return i.policy
}
// Repository gives the repository component of the image ref.
func (i imageRef) Repository() string {
return i.Context().RepositoryStr()
}
// Registry gives the registry component of the image ref.
func (i imageRef) Registry() string {
return i.Context().Registry.String()
}
// ObjectIdentifier holds the identifying data for a particular
// object. This won't always have a name (e.g., a kustomization.yaml).
type ObjectIdentifier struct {
yaml.ResourceIdentifier
}
// Result contains the file changes made during the update. It contains
// details about the exact changes made to the files and the objects in them.
// It has a nested structure file->objects->changes.
type Result struct {
FileChanges map[string]ObjectChanges
}
// ObjectChanges contains all the changes made to objects.
type ObjectChanges map[ObjectIdentifier][]Change
// Change contains the setter that resulted in a Change, the old and the new
// value after the Change.
type Change struct {
OldValue string
NewValue string
Setter string
}
// AddChange adds changes to Result for a given file, object and changes
// associated with it.
func (r *Result) AddChange(file string, objectID ObjectIdentifier, changes ...Change) {
if r.FileChanges == nil {
r.FileChanges = map[string]ObjectChanges{}
}
// Create an entry for the file if not present.
_, ok := r.FileChanges[file]
if !ok {
r.FileChanges[file] = ObjectChanges{}
}
// Append to the changes for the object.
r.FileChanges[file][objectID] = append(r.FileChanges[file][objectID], changes...)
}
// Changes returns all the changes that were made in at least one update.
func (r Result) Changes() []Change {
seen := make(map[Change]struct{})
var result []Change
for _, objChanges := range r.FileChanges {
for _, changes := range objChanges {
for _, change := range changes {
if _, ok := seen[change]; !ok {
seen[change] = struct{}{}
result = append(result, change)
}
}
}
}
return result
}
// Objects returns ObjectChanges, regardless of which file they appear in.
func (r Result) Objects() ObjectChanges {
result := make(ObjectChanges)
for _, objChanges := range r.FileChanges {
for obj, change := range objChanges {
result[obj] = change
}
}
return result
}

View File

@ -1,136 +0,0 @@
/*
Copyright 2025 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package update
import (
"testing"
"github.com/google/go-containerregistry/pkg/name"
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// mustRef creates an imageRef for use in tests. It panics if the ref
// given is invalid.
func mustRef(ref string) imageRef {
r, err := name.ParseReference(ref)
if err != nil {
panic(err)
}
return imageRef{r, types.NamespacedName{}}
}
func TestMustRef(t *testing.T) {
g := NewWithT(t)
t.Run("gives each component of an image ref", func(t *testing.T) {
ref := mustRef("helloworld:v1.0.1")
g.Expect(ref.String()).To(Equal("helloworld:v1.0.1"))
g.Expect(ref.Identifier()).To(Equal("v1.0.1"))
g.Expect(ref.Repository()).To(Equal("library/helloworld"))
g.Expect(ref.Registry()).To(Equal("index.docker.io"))
g.Expect(ref.Name()).To(Equal("index.docker.io/library/helloworld:v1.0.1"))
})
t.Run("deals with hostnames and digests", func(t *testing.T) {
image := "localhost:5000/org/helloworld@sha256:6745aaad46d795c9836632e1fb62f24b7e7f4c843144da8e47a5465c411a14be"
ref := mustRef(image)
g.Expect(ref.String()).To(Equal(image))
g.Expect(ref.Identifier()).To(Equal("sha256:6745aaad46d795c9836632e1fb62f24b7e7f4c843144da8e47a5465c411a14be"))
g.Expect(ref.Repository()).To(Equal("org/helloworld"))
g.Expect(ref.Registry()).To(Equal("localhost:5000"))
g.Expect(ref.Name()).To(Equal(image))
})
}
func TestResult(t *testing.T) {
g := NewWithT(t)
var result Result
objectNames := []ObjectIdentifier{
{yaml.ResourceIdentifier{
NameMeta: yaml.NameMeta{Namespace: "ns", Name: "foo"},
}},
{yaml.ResourceIdentifier{
NameMeta: yaml.NameMeta{Namespace: "ns", Name: "bar"},
}},
}
result.AddChange("foo.yaml", objectNames[0], Change{
OldValue: "aaa",
NewValue: "bbb",
Setter: "foo-ns:policy:name",
})
result.AddChange("bar.yaml", objectNames[1], Change{
OldValue: "cccc:v1.0",
NewValue: "cccc:v1.2",
Setter: "foo-ns:policy",
})
result = Result{
FileChanges: map[string]ObjectChanges{
"foo.yaml": {
objectNames[0]: []Change{
{
OldValue: "aaa",
NewValue: "bbb",
Setter: "foo-ns:policy:name",
},
},
},
"bar.yaml": {
objectNames[1]: []Change{
{
OldValue: "cccc:v1.0",
NewValue: "cccc:v1.2",
Setter: "foo-ns:policy",
},
},
},
},
}
g.Expect(result.Changes()).To(ContainElements([]Change{
{
OldValue: "aaa",
NewValue: "bbb",
Setter: "foo-ns:policy:name",
},
{
OldValue: "cccc:v1.0",
NewValue: "cccc:v1.2",
Setter: "foo-ns:policy",
},
}))
g.Expect(result.Objects()).To(Equal(ObjectChanges{
objectNames[0]: []Change{
{
OldValue: "aaa",
NewValue: "bbb",
Setter: "foo-ns:policy:name",
},
},
objectNames[1]: []Change{
{
OldValue: "cccc:v1.0",
NewValue: "cccc:v1.2",
Setter: "foo-ns:policy",
},
},
}))
}

View File

@ -1,9 +0,0 @@
# This is not intended to be a working kustomization
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- irrelevant.yaml
images:
- name: container
newName: index.repo.fake/updated # {"$imagepolicy": "automation-ns:policy:name"}
newTag: v1.0.1 # {"$imagepolicy": "automation-ns:policy:tag"}

View File

@ -1,28 +0,0 @@
# This is not intended to be a working kustomization
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- unimportant.yaml
images:
- name: container
newName: index.repo.fake/updated # {"$imagepolicy": "automation-ns:policy:name"}
newTag: v1.0.1 # {"$imagepolicy": "automation-ns:policy:tag"}
newDigest: sha256:6745aaad46d795c9836632e1fb62f24b7e7f4c843144da8e47a5465c411a14be # {"$imagepolicy": "automation-ns:policy-with-digest:digest"}
# Prove fix for https://github.com/fluxcd/flux2/issues/3284
patches:
- patch: |
- op: replace
path: /spec/template/spec/containers/0/volumeMounts
value:
- mountPath: /usr/share/wildfly/wildfly/standalone/configuration/appConfigMaps/WF
name: wf-config
- mountPath: /usr/share/wildfly/wildfly/standalone/configuration/appConfigMaps/xxx_config
name: xxx-config
- mountPath: /usr/share/wildfly/wildfly/standalone/log
name: wildfly-standalone-log
target:
group: apps
version: v1
kind: Deployment
name: sxxxxdadminservice
image: image:v1.0.0@sha256:6745aaad46d795c9836632e1fb62f24b7e7f4c843144da8e47a5465c411a14be # {"$imagepolicy": "automation-ns:policy-with-digest"}

View File

@ -1,9 +0,0 @@
# This is not intended to be a working kustomization
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- irrelevant.yaml
images:
- name: container
newName: replaced # {"$imagepolicy": "automation-ns:policy:name"}
newTag: v1 # {"$imagepolicy": "automation-ns:policy:tag"}

View File

@ -1,28 +0,0 @@
# This is not intended to be a working kustomization
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- unimportant.yaml
images:
- name: container
newName: replaced # {"$imagepolicy": "automation-ns:policy:name"}
newTag: v1 # {"$imagepolicy": "automation-ns:policy:tag"}
newDigest: sha256:1234567890abcdef # {"$imagepolicy": "automation-ns:policy-with-digest:digest"}
# Prove fix for https://github.com/fluxcd/flux2/issues/3284
patches:
- patch: |
- op: replace
path: /spec/template/spec/containers/0/volumeMounts
value:
- mountPath: /usr/share/wildfly/wildfly/standalone/configuration/appConfigMaps/WF
name: wf-config
- mountPath: /usr/share/wildfly/wildfly/standalone/configuration/appConfigMaps/xxx_config
name: xxx-config
- mountPath: /usr/share/wildfly/wildfly/standalone/log
name: wildfly-standalone-log
target:
group: apps
version: v1
kind: Deployment
name: sxxxxdadminservice
image: image # {"$imagepolicy": "automation-ns:policy-with-digest"}

View File

@ -1,141 +0,0 @@
/*
Copyright 2025 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package update
import (
"testing"
"github.com/go-logr/logr"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/kustomize/kyaml/yaml"
reflectorv1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
"github.com/fluxcd/image-automation-controller/internal/testutil"
)
func TestUpdateWithSetters(t *testing.T) {
g := NewWithT(t)
policies := []reflectorv1.ImagePolicy{
{
ObjectMeta: metav1.ObjectMeta{
Namespace: "automation-ns",
Name: "policy",
},
Status: reflectorv1.ImagePolicyStatus{
LatestRef: testutil.ImageToRef("index.repo.fake/updated:v1.0.1"),
},
},
{
ObjectMeta: metav1.ObjectMeta{
Namespace: "automation-ns",
Name: "unchanged",
},
Status: reflectorv1.ImagePolicyStatus{
LatestRef: testutil.ImageToRef("image:v1.0.0"),
},
},
{
ObjectMeta: metav1.ObjectMeta{
Namespace: "automation-ns",
Name: "policy-with-digest",
},
Status: reflectorv1.ImagePolicyStatus{
LatestRef: testutil.ImageToRef("image:v1.0.0@sha256:6745aaad46d795c9836632e1fb62f24b7e7f4c843144da8e47a5465c411a14be"),
},
},
}
// Test Result.
tmp := t.TempDir()
result, err := UpdateWithSetters(logr.Discard(), "testdata/setters/original", tmp, policies)
g.Expect(err).ToNot(HaveOccurred())
testutil.ExpectMatchingDirectories(g, tmp, "testdata/setters/expected")
kustomizeResourceID := ObjectIdentifier{yaml.ResourceIdentifier{
TypeMeta: yaml.TypeMeta{
APIVersion: "kustomize.config.k8s.io/v1beta1",
Kind: "Kustomization",
},
}}
markedResourceID := ObjectIdentifier{yaml.ResourceIdentifier{
TypeMeta: yaml.TypeMeta{
APIVersion: "batch/v1beta1",
Kind: "CronJob",
},
NameMeta: yaml.NameMeta{
Namespace: "bar",
Name: "foo",
},
}}
expectedResult := Result{
FileChanges: map[string]ObjectChanges{
"kustomization.yml": {
kustomizeResourceID: []Change{
{
OldValue: "replaced",
NewValue: "index.repo.fake/updated",
Setter: "automation-ns:policy:name",
},
{
OldValue: "v1",
NewValue: "v1.0.1",
Setter: "automation-ns:policy:tag",
},
{
OldValue: "sha256:1234567890abcdef",
NewValue: "sha256:6745aaad46d795c9836632e1fb62f24b7e7f4c843144da8e47a5465c411a14be",
Setter: "automation-ns:policy-with-digest:digest",
},
{
OldValue: "image",
NewValue: "image:v1.0.0@sha256:6745aaad46d795c9836632e1fb62f24b7e7f4c843144da8e47a5465c411a14be",
Setter: "automation-ns:policy-with-digest",
},
},
},
"Kustomization": {
kustomizeResourceID: []Change{
{
OldValue: "replaced",
NewValue: "index.repo.fake/updated",
Setter: "automation-ns:policy:name",
},
{
OldValue: "v1",
NewValue: "v1.0.1",
Setter: "automation-ns:policy:tag",
},
},
},
"marked.yaml": {
markedResourceID: []Change{
{
OldValue: "image:v1.0.0",
NewValue: "index.repo.fake/updated:v1.0.1",
Setter: "automation-ns:policy",
},
},
},
},
}
g.Expect(result).To(Equal(expectedResult))
}

189
main.go
View File

@ -21,46 +21,30 @@ import (
"os"
flag "github.com/spf13/pflag"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
"k8s.io/utils/pointer"
ctrl "sigs.k8s.io/controller-runtime"
ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
ctrlcfg "sigs.k8s.io/controller-runtime/pkg/config"
ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
reflectorv1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
"github.com/fluxcd/pkg/auth"
cache "github.com/fluxcd/pkg/cache"
imagev1_reflect "github.com/fluxcd/image-reflector-controller/api/v1beta1"
"github.com/fluxcd/pkg/runtime/acl"
"github.com/fluxcd/pkg/runtime/client"
helper "github.com/fluxcd/pkg/runtime/controller"
"github.com/fluxcd/pkg/runtime/events"
feathelper "github.com/fluxcd/pkg/runtime/features"
"github.com/fluxcd/pkg/runtime/leaderelection"
"github.com/fluxcd/pkg/runtime/logger"
"github.com/fluxcd/pkg/runtime/metrics"
"github.com/fluxcd/pkg/runtime/pprof"
"github.com/fluxcd/pkg/runtime/probes"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
"github.com/fluxcd/pkg/git"
imagev1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
"github.com/fluxcd/image-automation-controller/internal/features"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
imagev1 "github.com/fluxcd/image-automation-controller/api/v1beta1"
// +kubebuilder:scaffold:imports
"github.com/fluxcd/image-automation-controller/internal/controller"
"github.com/fluxcd/image-automation-controller/controllers"
)
const (
controllerName = "image-automation-controller"
)
const controllerName = "image-automation-controller"
var (
scheme = runtime.NewScheme()
@ -69,17 +53,13 @@ var (
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(reflectorv1.AddToScheme(scheme))
utilruntime.Must(imagev1_reflect.AddToScheme(scheme))
utilruntime.Must(sourcev1.AddToScheme(scheme))
utilruntime.Must(imagev1.AddToScheme(scheme))
// +kubebuilder:scaffold:scheme
}
func main() {
const (
tokenCacheDefaultMaxSize = 100
)
var (
metricsAddr string
eventsAddr string
@ -88,165 +68,74 @@ func main() {
aclOptions acl.Options
logOptions logger.Options
leaderElectionOptions leaderelection.Options
rateLimiterOptions helper.RateLimiterOptions
featureGates feathelper.FeatureGates
watchOptions helper.WatchOptions
watchAllNamespaces bool
concurrent int
tokenCacheOptions cache.TokenFlags
defaultServiceAccount string
)
flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
flag.StringVar(&eventsAddr, "events-addr", "", "The address of the events receiver.")
flag.StringVar(&healthAddr, "health-addr", ":9440", "The address the health endpoint binds to.")
flag.BoolVar(&watchAllNamespaces, "watch-all-namespaces", true,
"Watch for custom resources in all namespaces, if set to false it will only watch the runtime namespace.")
flag.IntVar(&concurrent, "concurrent", 4, "The number of concurrent resource reconciles.")
flag.StringVar(&defaultServiceAccount, auth.ControllerFlagDefaultServiceAccount,
"", "Default service account to use for workload identity when not specified in resources.")
flag.StringSliceVar(&git.KexAlgos, "ssh-kex-algos", []string{},
"The list of key exchange algorithms to use for ssh connections, arranged from most preferred to the least.")
flag.StringSliceVar(&git.HostKeyAlgos, "ssh-hostkey-algos", []string{},
"The list of hostkey algorithms to use for ssh connections, arranged from most preferred to the least.")
clientOptions.BindFlags(flag.CommandLine)
logOptions.BindFlags(flag.CommandLine)
leaderElectionOptions.BindFlags(flag.CommandLine)
aclOptions.BindFlags(flag.CommandLine)
rateLimiterOptions.BindFlags(flag.CommandLine)
featureGates.BindFlags(flag.CommandLine)
watchOptions.BindFlags(flag.CommandLine)
tokenCacheOptions.BindFlags(flag.CommandLine, tokenCacheDefaultMaxSize)
flag.Parse()
logger.SetLogger(logger.NewLogger(logOptions))
log := logger.NewLogger(logOptions)
ctrl.SetLogger(log)
err := featureGates.WithLogger(setupLog).
SupportedFeatures(features.FeatureGates())
if err != nil {
setupLog.Error(err, "unable to load feature gates")
os.Exit(1)
var eventRecorder *events.Recorder
if eventsAddr != "" {
if er, err := events.NewRecorder(eventsAddr, controllerName); err != nil {
setupLog.Error(err, "unable to create event recorder")
os.Exit(1)
} else {
eventRecorder = er
}
}
switch enabled, err := features.Enabled(auth.FeatureGateObjectLevelWorkloadIdentity); {
case err != nil:
setupLog.Error(err, "unable to check feature gate "+auth.FeatureGateObjectLevelWorkloadIdentity)
os.Exit(1)
case enabled:
auth.EnableObjectLevelWorkloadIdentity()
}
if defaultServiceAccount != "" {
auth.SetDefaultServiceAccount(defaultServiceAccount)
}
if auth.InconsistentObjectLevelConfiguration() {
setupLog.Error(auth.ErrInconsistentObjectLevelConfiguration, "invalid configuration")
os.Exit(1)
}
metricsRecorder := metrics.NewRecorder()
ctrlmetrics.Registry.MustRegister(metricsRecorder.Collectors()...)
watchNamespace := ""
if !watchOptions.AllNamespaces {
if !watchAllNamespaces {
watchNamespace = os.Getenv("RUNTIME_NAMESPACE")
}
var disableCacheFor []ctrlclient.Object
shouldCache, err := features.Enabled(features.CacheSecretsAndConfigMaps)
if err != nil {
setupLog.Error(err, "unable to check feature gate "+features.CacheSecretsAndConfigMaps)
os.Exit(1)
}
if !shouldCache {
disableCacheFor = append(disableCacheFor, &corev1.Secret{}, &corev1.ConfigMap{})
}
restConfig := client.GetConfigOrDie(clientOptions)
watchSelector, err := helper.GetWatchSelector(watchOptions)
if err != nil {
setupLog.Error(err, "unable to configure watch label selector for manager")
os.Exit(1)
}
leaderElectionID := fmt.Sprintf("%s-leader-election", controllerName)
if watchOptions.LabelSelector != "" {
leaderElectionID = leaderelection.GenerateID(leaderElectionID, watchOptions.LabelSelector)
}
mgrConfig := ctrl.Options{
mgr, err := ctrl.NewManager(restConfig, ctrl.Options{
Scheme: scheme,
MetricsBindAddress: metricsAddr,
HealthProbeBindAddress: healthAddr,
Port: 9443,
LeaderElection: leaderElectionOptions.Enable,
LeaderElectionReleaseOnCancel: leaderElectionOptions.ReleaseOnCancel,
LeaseDuration: &leaderElectionOptions.LeaseDuration,
RenewDeadline: &leaderElectionOptions.RenewDeadline,
RetryPeriod: &leaderElectionOptions.RetryPeriod,
LeaderElectionID: leaderElectionID,
Client: ctrlclient.Options{
Cache: &ctrlclient.CacheOptions{
DisableFor: disableCacheFor,
},
},
Cache: ctrlcache.Options{
ByObject: map[ctrlclient.Object]ctrlcache.ByObject{
&imagev1.ImageUpdateAutomation{}: {Label: watchSelector},
},
},
Metrics: metricsserver.Options{
BindAddress: metricsAddr,
ExtraHandlers: pprof.GetHandlers(),
},
Controller: ctrlcfg.Controller{
RecoverPanic: pointer.Bool(true),
MaxConcurrentReconciles: concurrent,
},
}
if watchNamespace != "" {
mgrConfig.Cache.DefaultNamespaces = map[string]ctrlcache.Config{
watchNamespace: ctrlcache.Config{},
}
}
mgr, err := ctrl.NewManager(restConfig, mgrConfig)
LeaderElectionID: fmt.Sprintf("%s-leader-election", controllerName),
Namespace: watchNamespace,
})
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}
probes.SetupChecks(mgr, setupLog)
pprof.SetupHandlers(mgr, setupLog)
var eventRecorder *events.Recorder
if eventRecorder, err = events.NewRecorder(mgr, ctrl.Log, eventsAddr, controllerName); err != nil {
setupLog.Error(err, "unable to create event recorder")
os.Exit(1)
}
metricsH := helper.NewMetrics(mgr, metrics.MustMakeRecorder(), imagev1.ImageUpdateAutomationFinalizer)
var tokenCache *cache.TokenCache
if tokenCacheOptions.MaxSize > 0 {
var err error
tokenCache, err = cache.NewTokenCache(tokenCacheOptions.MaxSize,
cache.WithMaxDuration(tokenCacheOptions.MaxDuration),
cache.WithMetricsRegisterer(ctrlmetrics.Registry),
cache.WithMetricsPrefix("gotk_token_"))
if err != nil {
setupLog.Error(err, "unable to create token cache")
os.Exit(1)
}
}
ctx := ctrl.SetupSignalHandler()
if err := (&controller.ImageUpdateAutomationReconciler{
Client: mgr.GetClient(),
EventRecorder: eventRecorder,
Metrics: metricsH,
NoCrossNamespaceRef: aclOptions.NoCrossNamespaceRefs,
ControllerName: controllerName,
}).SetupWithManager(ctx, mgr, controller.ImageUpdateAutomationReconcilerOptions{
RateLimiter: helper.GetRateLimiter(rateLimiterOptions),
TokenCache: tokenCache,
if err = (&controllers.ImageUpdateAutomationReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
EventRecorder: mgr.GetEventRecorderFor(controllerName),
ExternalEventRecorder: eventRecorder,
MetricsRecorder: metricsRecorder,
NoCrossNamespaceRef: aclOptions.NoCrossNamespaceRefs,
}).SetupWithManager(mgr, controllers.ImageUpdateAutomationReconcilerOptions{
MaxConcurrentReconciles: concurrent,
}); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ImageUpdateAutomation")
os.Exit(1)
@ -254,7 +143,7 @@ func main() {
// +kubebuilder:scaffold:builder
setupLog.Info("starting manager")
if err := mgr.Start(ctx); err != nil {
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package testutil
package test
import (
"os"
@ -24,80 +24,74 @@ import (
. "github.com/onsi/gomega"
)
// ExpectMatchingDirectories compares two directories, recursively, and
// expects them to match. It will fail if there are files that are
// present in one directory but not the other, or if files that are
// present in both directories have different contents.
func ExpectMatchingDirectories(g *WithT, actualRoot, expectedRoot string) {
g.Expect(actualRoot).To(BeADirectory())
g.Expect(expectedRoot).To(BeADirectory())
// TODO rewrite this as just doing the diff, so I can test that it
// fails at the right times too.
func ExpectMatchingDirectories(actualRoot, expectedRoot string) {
Expect(actualRoot).To(BeADirectory())
Expect(expectedRoot).To(BeADirectory())
actualonly, expectedonly, different := DiffDirectories(actualRoot, expectedRoot)
g.Expect(actualonly).To(BeEmpty(), "Expect no files in %s but not in %s", actualRoot, expectedRoot)
g.Expect(expectedonly).To(BeEmpty(), "Expect no files in %s but not in %s", expectedRoot, actualRoot)
Expect(actualonly).To(BeEmpty(), "Expect no files in %s but not in %s", actualRoot, expectedRoot)
Expect(expectedonly).To(BeEmpty(), "Expect no files in %s but not in %s", expectedRoot, actualRoot)
// these are enumerated, so that the output is the actual difference
for _, diff := range different {
diff.FailedExpectation(g)
diff.FailedExpectation()
}
}
type Diff interface {
Path() string
FailedExpectation(g *WithT)
FailedExpectation()
}
type contentDiff struct {
type contentdiff struct {
path, actual, expected string
}
func (d contentDiff) Path() string {
func (d contentdiff) Path() string {
return d.path
}
func (d contentDiff) FailedExpectation(g *WithT) {
g.Expect(d.actual).To(Equal(d.expected))
// Run an expectation that will fail, giving an appropriate error
func (d contentdiff) FailedExpectation() {
Expect(d.actual).To(Equal(d.expected))
}
type dirFile struct {
type dirfile struct {
abspath, path string
expectedRegularFile bool
}
func (d dirFile) Path() string {
func (d dirfile) Path() string {
return d.path
}
func (d dirFile) FailedExpectation(g *WithT) {
func (d dirfile) FailedExpectation() {
if d.expectedRegularFile {
g.Expect(d.path).To(BeARegularFile())
Expect(d.path).To(BeARegularFile())
} else {
g.Expect(d.path).To(BeADirectory())
Expect(d.path).To(BeADirectory())
}
}
// DiffDirectories walks the two given directories, recursively, and
// reports relative paths for any files that are:
//
// (in actual but not expected, in expected but not actual, in both but different)
// (in actual but not expected, in expected but not actual, in both but different)
//
// It ignores dot directories (e.g., `.git/`) and Emacs backups (e.g.,
// `foo.yaml~`). It panics if it encounters any error apart from a
// file not found.
func DiffDirectories(actual, expected string) (actualonly []string, expectedonly []string, different []Diff) {
seen := make(map[string]struct{})
filepath.Walk(expected, func(expectedPath string, expectedInfo os.FileInfo, err error) error {
if err != nil {
panic(err)
}
relPath := expectedPath[len(expected):]
seen[relPath] = struct{}{}
// ignore emacs backups
if strings.HasSuffix(expectedPath, "~") {
return nil
}
relPath := expectedPath[len(expected):]
actualPath := filepath.Join(actual, relPath)
// ignore dotfiles
if strings.HasPrefix(filepath.Base(expectedPath), ".") {
if expectedInfo.IsDir() {
@ -106,30 +100,24 @@ func DiffDirectories(actual, expected string) (actualonly []string, expectedonly
return nil
}
actualPath := filepath.Join(actual, relPath)
actualInfo, err := os.Stat(actualPath)
switch {
case err == nil:
break
case os.IsNotExist(err):
expectedonly = append(expectedonly, relPath)
if expectedInfo.IsDir() {
return filepath.SkipDir
}
return nil
default:
panic(err)
}
// file exists in both places
switch {
case actualInfo.IsDir() && expectedInfo.IsDir():
return nil // i.e., keep recursing
case actualInfo.IsDir() || expectedInfo.IsDir():
different = append(different, dirFile{path: relPath, abspath: actualPath, expectedRegularFile: actualInfo.IsDir()})
if expectedInfo.IsDir() {
return filepath.SkipDir
}
different = append(different, dirfile{path: relPath, abspath: actualPath, expectedRegularFile: actualInfo.IsDir()})
return nil
}
@ -144,7 +132,7 @@ func DiffDirectories(actual, expected string) (actualonly []string, expectedonly
panic(err)
}
if string(actualBytes) != string(expectedBytes) {
different = append(different, contentDiff{path: relPath, actual: string(actualBytes), expected: string(expectedBytes)})
different = append(different, contentdiff{path: relPath, actual: string(actualBytes), expected: string(expectedBytes)})
}
return nil
})
@ -163,12 +151,18 @@ func DiffDirectories(actual, expected string) (actualonly []string, expectedonly
if actualInfo.IsDir() && strings.HasPrefix(filepath.Base(actualPath), ".") {
return filepath.SkipDir
}
if _, ok := seen[relPath]; !ok {
// since I've already compared any file that exists in
// expected or both, I'm only concerned with files that appear
// in actual but not in expected.
expectedPath := filepath.Join(expected, relPath)
_, err = os.Stat(expectedPath)
switch {
case err == nil:
break
case os.IsNotExist(err):
actualonly = append(actualonly, relPath)
if actualInfo.IsDir() {
return filepath.SkipDir
}
default:
panic(err)
}
return nil
})

Some files were not shown because too many files have changed in this diff Show More