├── .dockerignore ├── .github ├── dependabot.yaml ├── labels.yaml └── workflows │ ├── backport.yaml │ ├── build.yaml │ ├── cifuzz.yaml │ ├── nightly.yaml │ ├── release.yml │ ├── scan.yml │ └── sync-labels.yaml ├── .gitignore ├── .goreleaser.yaml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── DCO ├── DEVELOPMENT.md ├── Dockerfile ├── LICENSE ├── MAINTAINERS ├── Makefile ├── PROJECT ├── README.md ├── api ├── go.mod ├── go.sum ├── v1beta1 │ ├── condition_types.go │ ├── doc.go │ ├── git.go │ ├── groupversion_info.go │ ├── imageupdateautomation_types.go │ ├── reference.go │ └── zz_generated.deepcopy.go └── v1beta2 │ ├── condition_types.go │ ├── doc.go │ ├── git.go │ ├── groupversion_info.go │ ├── imageupdateautomation_types.go │ ├── reference.go │ └── zz_generated.deepcopy.go ├── config ├── crd │ ├── bases │ │ └── image.toolkit.fluxcd.io_imageupdateautomations.yaml │ └── kustomization.yaml ├── default │ ├── kustomization.yaml │ └── namespace.yaml ├── manager │ ├── deployment.yaml │ └── kustomization.yaml ├── rbac │ ├── imageupdateautomation_editor_role.yaml │ ├── imageupdateautomation_viewer_role.yaml │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── role.yaml │ └── role_binding.yaml └── samples │ ├── image_v1beta1_imageupdateautomation.yaml │ └── image_v1beta2_imageupdateautomation.yaml ├── docs ├── api │ ├── v1beta1 │ │ └── image-automation.md │ └── v1beta2 │ │ └── image-automation.md └── spec │ ├── v1alpha1 │ └── imageupdateautomations.md │ ├── v1alpha2 │ └── imageupdateautomations.md │ ├── v1beta1 │ └── imageupdateautomations.md │ └── v1beta2 │ └── imageupdateautomations.md ├── go.mod ├── go.sum ├── hack ├── api-docs │ ├── config.json │ └── template │ │ ├── members.tpl │ │ ├── pkg.tpl │ │ └── type.tpl └── boilerplate.go.txt ├── internal ├── constants │ └── constants.go ├── controller │ ├── controllers_fuzzer_test.go │ ├── imageupdateautomation_controller.go │ ├── imageupdateautomation_controller_test.go │ ├── predicate.go │ ├── predicate_test.go │ ├── suite_test.go │ └── testdata │ │ ├── appconfig-setters-expected │ │ └── deploy.yaml │ │ ├── appconfig │ │ └── deploy.yaml │ │ └── pathconfig │ │ ├── no │ │ └── deploy.yaml │ │ └── yes │ │ └── deploy.yaml ├── features │ └── features.go ├── policy │ ├── applier.go │ ├── applier_test.go │ └── testdata │ │ ├── appconfig-setters-expected │ │ └── deploy.yaml │ │ ├── appconfig │ │ └── deploy.yaml │ │ ├── pathconfig-expected │ │ ├── no │ │ │ └── deploy.yaml │ │ └── yes │ │ │ └── deploy.yaml │ │ └── pathconfig │ │ ├── no │ │ └── deploy.yaml │ │ └── yes │ │ └── deploy.yaml ├── source │ ├── git.go │ ├── git_test.go │ ├── source.go │ ├── source_test.go │ └── testdata │ │ └── appconfig │ │ └── deploy.yaml └── testutil │ └── util.go ├── main.go ├── pkg ├── test │ ├── files.go │ ├── files_test.go │ └── testdata │ │ ├── base │ │ └── foo.yaml │ │ ├── diff │ │ ├── a │ │ │ ├── different │ │ │ │ └── content.yaml │ │ │ ├── dirfile │ │ │ ├── only │ │ │ │ └── here.yaml │ │ │ └── onlyhere.yaml │ │ └── b │ │ │ ├── different │ │ │ └── content.yaml │ │ │ └── dirfile │ │ │ └── .ignore │ │ └── equiv │ │ └── foo.yaml └── update │ ├── filereader.go │ ├── filereader_test.go │ ├── filter.go │ ├── filter_test.go │ ├── result.go │ ├── result_test.go │ ├── setters.go │ ├── testdata │ ├── leave │ │ ├── expected │ │ │ ├── cronjob.yaml │ │ │ └── deployment.yaml │ │ └── original │ │ │ ├── cronjob.yaml │ │ │ └── deployment.yaml │ ├── replace │ │ ├── commented-expected │ │ │ └── deployment.yaml │ │ ├── commented │ │ │ └── deployment.yaml │ │ ├── expected │ │ │ ├── cronjob.yaml │ │ │ └── deployment.yaml │ │ └── original │ │ │ ├── cronjob.yaml │ │ │ └── deployment.yaml │ └── setters │ │ ├── expected │ │ ├── Kustomization │ │ ├── kustomization.yml │ │ └── marked.yaml │ │ └── original │ │ ├── Kustomization │ │ ├── kustomization.yml │ │ ├── marked.yaml │ │ ├── otherns.yaml │ │ └── unmarked.yaml │ └── update_test.go └── tests └── fuzz ├── Dockerfile.builder ├── README.md ├── native_go_run.sh ├── oss_fuzz_prebuild.sh └── oss_fuzz_run.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "gomod" 5 | directory: "/" 6 | labels: ["dependencies"] 7 | schedule: 8 | interval: "monthly" 9 | groups: 10 | go-deps: 11 | patterns: 12 | - "*" 13 | allow: 14 | - dependency-type: "direct" 15 | ignore: 16 | # Kubernetes deps are updated by fluxcd/pkg 17 | - dependency-name: "k8s.io/*" 18 | - dependency-name: "sigs.k8s.io/*" 19 | - dependency-name: "github.com/go-logr/*" 20 | # Flux APIs are updated at release time 21 | - dependency-name: "github.com/fluxcd/image-automation-controller/api" 22 | - dependency-name: "github.com/fluxcd/image-reflector-controller/api" 23 | - dependency-name: "github.com/fluxcd/source-controller/api" 24 | - package-ecosystem: "github-actions" 25 | directory: "/" 26 | labels: ["area/ci", "dependencies"] 27 | groups: 28 | ci: 29 | patterns: 30 | - "*" 31 | schedule: 32 | interval: "monthly" 33 | -------------------------------------------------------------------------------- /.github/labels.yaml: -------------------------------------------------------------------------------- 1 | # Configuration file to declaratively configure labels 2 | # Ref: https://github.com/EndBug/label-sync#Config-files 3 | 4 | - name: area/git 5 | description: Git related issues and pull requests 6 | color: '#863faf' 7 | - name: area/kyaml 8 | description: YAML patching related issues and pull requests 9 | color: '#86dbf2' 10 | 11 | # TODO: enable this when we have a release/v1.0.x branch 12 | #- name: backport:release/v1.0.x 13 | # description: To be backported to release/v1.0.x 14 | # color: '#ffd700' 15 | -------------------------------------------------------------------------------- /.github/workflows/backport.yaml: -------------------------------------------------------------------------------- 1 | name: backport 2 | 3 | on: 4 | pull_request_target: 5 | types: [closed, labeled] 6 | 7 | jobs: 8 | pull-request: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | if: github.event.pull_request.state == 'closed' && github.event.pull_request.merged && (github.event_name != 'labeled' || startsWith('backport:', github.event.label.name)) 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 17 | with: 18 | ref: ${{ github.event.pull_request.head.sha }} 19 | - name: Create backport PRs 20 | uses: korthout/backport-action@436145e922f9561fc5ea157ff406f21af2d6b363 # v3.2.0 21 | # xref: https://github.com/korthout/backport-action#inputs 22 | with: 23 | # Use token to allow workflows to be triggered for the created PR 24 | github_token: ${{ secrets.BOT_GITHUB_TOKEN }} 25 | # Match labels with a pattern `backport:` 26 | label_pattern: '^backport:([^ ]+)$' 27 | # A bit shorter pull-request title than the default 28 | pull_title: '[${target_branch}] ${pull_title}' 29 | # Simpler PR description than default 30 | pull_description: |- 31 | Automated backport to `${target_branch}`, triggered by a label in #${pull_number}. 32 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | pull_request: 4 | push: 5 | branches: [ 'main', 'release/**' ] 6 | 7 | permissions: 8 | contents: read # for actions/checkout to fetch code 9 | 10 | jobs: 11 | test-linux-amd64: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: checkout 15 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 16 | - name: Setup Go 17 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 18 | with: 19 | go-version: 1.24.x 20 | cache-dependency-path: | 21 | **/go.sum 22 | **/go.mod 23 | - name: Run tests 24 | run: make test 25 | - name: Verify 26 | run: make verify 27 | 28 | kind-linux-arm64: 29 | runs-on: 30 | group: "ARM64" 31 | steps: 32 | - name: checkout 33 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 34 | - name: Setup Go 35 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 36 | with: 37 | go-version: 1.24.x 38 | cache-dependency-path: | 39 | **/go.sum 40 | **/go.mod 41 | - name: Run tests 42 | run: make test 43 | env: 44 | SKIP_COSIGN_VERIFICATION: true 45 | - name: Verify 46 | run: make verify 47 | -------------------------------------------------------------------------------- /.github/workflows/cifuzz.yaml: -------------------------------------------------------------------------------- 1 | name: fuzz 2 | on: 3 | pull_request: 4 | branches: [ 'main', 'release/**' ] 5 | 6 | permissions: 7 | contents: read # for actions/checkout to fetch code 8 | 9 | jobs: 10 | smoketest: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 15 | - name: Setup Go 16 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 17 | with: 18 | go-version: 1.24.x 19 | cache-dependency-path: | 20 | **/go.sum 21 | **/go.mod 22 | - name: Smoke test Fuzzers 23 | run: make fuzz-smoketest 24 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yaml: -------------------------------------------------------------------------------- 1 | name: nightly 2 | on: 3 | schedule: 4 | - cron: '0 0 * * *' 5 | workflow_dispatch: 6 | 7 | env: 8 | REPOSITORY: ${{ github.repository }} 9 | 10 | permissions: 11 | contents: read # for actions/checkout to fetch code 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 19 | - name: Setup QEMU 20 | uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 21 | - name: Setup Docker Buildx 22 | id: buildx 23 | uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 24 | - name: Build multi-arch container image 25 | uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0 26 | with: 27 | push: false 28 | builder: ${{ steps.buildx.outputs.name }} 29 | context: . 30 | file: ./Dockerfile 31 | platforms: linux/amd64,linux/arm/v7,linux/arm64 32 | tags: | 33 | ${{ env.REPOSITORY }}:nightly 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | workflow_dispatch: 7 | inputs: 8 | tag: 9 | description: 'image tag prefix' 10 | default: 'preview' 11 | required: true 12 | 13 | permissions: 14 | contents: read 15 | 16 | env: 17 | CONTROLLER: ${{ github.event.repository.name }} 18 | 19 | jobs: 20 | release: 21 | outputs: 22 | hashes: ${{ steps.slsa.outputs.hashes }} 23 | image_url: ${{ steps.slsa.outputs.image_url }} 24 | image_digest: ${{ steps.slsa.outputs.image_digest }} 25 | runs-on: ubuntu-latest 26 | permissions: 27 | contents: write # for creating the GitHub release. 28 | id-token: write # for creating OIDC tokens for signing. 29 | packages: write # for pushing and signing container images. 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 33 | - name: Setup Kustomize 34 | uses: fluxcd/pkg/actions/kustomize@main 35 | - name: Prepare 36 | id: prep 37 | run: | 38 | VERSION="${{ github.event.inputs.tag }}-${GITHUB_SHA::8}" 39 | if [[ $GITHUB_REF == refs/tags/* ]]; then 40 | VERSION=${GITHUB_REF/refs\/tags\//} 41 | fi 42 | echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT 43 | echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT 44 | - name: Setup QEMU 45 | uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 46 | - name: Setup Docker Buildx 47 | id: buildx 48 | uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 49 | - name: Login to GitHub Container Registry 50 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 51 | with: 52 | registry: ghcr.io 53 | username: fluxcdbot 54 | password: ${{ secrets.GHCR_TOKEN }} 55 | - name: Login to Docker Hub 56 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 57 | with: 58 | username: fluxcdbot 59 | password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }} 60 | - name: Generate images meta 61 | id: meta 62 | uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 63 | with: 64 | images: | 65 | fluxcd/${{ env.CONTROLLER }} 66 | ghcr.io/fluxcd/${{ env.CONTROLLER }} 67 | tags: | 68 | type=raw,value=${{ steps.prep.outputs.VERSION }} 69 | - name: Publish images 70 | id: build-push 71 | uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0 72 | with: 73 | sbom: true 74 | provenance: true 75 | push: true 76 | builder: ${{ steps.buildx.outputs.name }} 77 | context: . 78 | file: ./Dockerfile 79 | platforms: linux/amd64,linux/arm/v7,linux/arm64 80 | tags: ${{ steps.meta.outputs.tags }} 81 | labels: ${{ steps.meta.outputs.labels }} 82 | - uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2 83 | - name: Sign images 84 | env: 85 | COSIGN_EXPERIMENTAL: 1 86 | run: | 87 | cosign sign --yes fluxcd/${{ env.CONTROLLER }}@${{ steps.build-push.outputs.digest }} 88 | cosign sign --yes ghcr.io/fluxcd/${{ env.CONTROLLER }}@${{ steps.build-push.outputs.digest }} 89 | - name: Generate release artifacts 90 | if: startsWith(github.ref, 'refs/tags/v') 91 | run: | 92 | mkdir -p config/release 93 | kustomize build ./config/crd > ./config/release/${{ env.CONTROLLER }}.crds.yaml 94 | kustomize build ./config/manager > ./config/release/${{ env.CONTROLLER }}.deployment.yaml 95 | - uses: anchore/sbom-action/download-syft@e11c554f704a0b820cbf8c51673f6945e0731532 # v0.20.0 96 | - name: Create release and SBOM 97 | id: run-goreleaser 98 | if: startsWith(github.ref, 'refs/tags/v') 99 | uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 100 | with: 101 | version: latest 102 | args: release --clean --skip=validate 103 | env: 104 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 105 | - name: Generate SLSA metadata 106 | id: slsa 107 | env: 108 | ARTIFACTS: "${{ steps.run-goreleaser.outputs.artifacts }}" 109 | run: | 110 | hashes=$(echo -E $ARTIFACTS | jq --raw-output '.[] | {name, "digest": (.extra.Digest // .extra.Checksum)} | select(.digest) | {digest} + {name} | join(" ") | sub("^sha256:";"")' | base64 -w0) 111 | echo "hashes=$hashes" >> $GITHUB_OUTPUT 112 | 113 | image_url=fluxcd/${{ env.CONTROLLER }}:${{ steps.prep.outputs.version }} 114 | echo "image_url=$image_url" >> $GITHUB_OUTPUT 115 | 116 | image_digest=${{ steps.build-push.outputs.digest }} 117 | echo "image_digest=$image_digest" >> $GITHUB_OUTPUT 118 | 119 | release-provenance: 120 | needs: [release] 121 | permissions: 122 | actions: read # for detecting the Github Actions environment. 123 | id-token: write # for creating OIDC tokens for signing. 124 | contents: write # for uploading attestations to GitHub releases. 125 | if: startsWith(github.ref, 'refs/tags/v') 126 | uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 127 | with: 128 | provenance-name: "provenance.intoto.jsonl" 129 | base64-subjects: "${{ needs.release.outputs.hashes }}" 130 | upload-assets: true 131 | 132 | dockerhub-provenance: 133 | needs: [release] 134 | permissions: 135 | actions: read # for detecting the Github Actions environment. 136 | id-token: write # for creating OIDC tokens for signing. 137 | packages: write # for uploading attestations. 138 | if: startsWith(github.ref, 'refs/tags/v') 139 | uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0 140 | with: 141 | image: ${{ needs.release.outputs.image_url }} 142 | digest: ${{ needs.release.outputs.image_digest }} 143 | registry-username: fluxcdbot 144 | secrets: 145 | registry-password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }} 146 | 147 | ghcr-provenance: 148 | needs: [release] 149 | permissions: 150 | actions: read # for detecting the Github Actions environment. 151 | id-token: write # for creating OIDC tokens for signing. 152 | packages: write # for uploading attestations. 153 | if: startsWith(github.ref, 'refs/tags/v') 154 | uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0 155 | with: 156 | image: ghcr.io/${{ needs.release.outputs.image_url }} 157 | digest: ${{ needs.release.outputs.image_digest }} 158 | registry-username: fluxcdbot 159 | secrets: 160 | registry-password: ${{ secrets.GHCR_TOKEN }} 161 | -------------------------------------------------------------------------------- /.github/workflows/scan.yml: -------------------------------------------------------------------------------- 1 | name: scan 2 | 3 | on: 4 | push: 5 | branches: [ 'main', 'release/**' ] 6 | pull_request: 7 | branches: [ 'main', 'release/**' ] 8 | schedule: 9 | - cron: '18 10 * * 3' 10 | 11 | permissions: 12 | contents: read # for actions/checkout to fetch code 13 | security-events: write # for codeQL to write security events 14 | 15 | jobs: 16 | fossa: 17 | name: FOSSA 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | - name: Run FOSSA scan and upload build data 23 | uses: fossa-contrib/fossa-action@3d2ef181b1820d6dcd1972f86a767d18167fa19b # v3.0.1 24 | with: 25 | # FOSSA Push-Only API Token 26 | fossa-api-key: 5ee8bf422db1471e0bcf2bcb289185de 27 | github-token: ${{ github.token }} 28 | 29 | codeql: 30 | name: CodeQL 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 35 | - name: Setup Go 36 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 37 | with: 38 | go-version: 1.24.x 39 | cache-dependency-path: | 40 | **/go.sum 41 | **/go.mod 42 | - name: Initialize CodeQL 43 | uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 44 | with: 45 | languages: go 46 | # 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 47 | # xref: https://codeql.github.com/codeql-query-help/go/ 48 | queries: security-and-quality 49 | - name: Autobuild 50 | uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 51 | - name: Perform CodeQL Analysis 52 | uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 53 | -------------------------------------------------------------------------------- /.github/workflows/sync-labels.yaml: -------------------------------------------------------------------------------- 1 | name: sync-labels 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - .github/labels.yaml 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | labels: 15 | name: Run sync 16 | runs-on: ubuntu-latest 17 | permissions: 18 | issues: write 19 | steps: 20 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 21 | - uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2.3.3 22 | with: 23 | # Configuration file 24 | config-file: | 25 | https://raw.githubusercontent.com/fluxcd/community/main/.github/standard-labels.yaml 26 | .github/labels.yaml 27 | # Strictly declarative 28 | delete-other-labels: true 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | notes 2 | 3 | # Thes are downloaded in the Makefile 4 | cache/* 5 | internal/controller/testdata/crds/* 6 | 7 | # Binaries for programs and plugins 8 | *.exe 9 | *.exe~ 10 | *.dll 11 | *.so 12 | *.dylib 13 | bin 14 | testbin 15 | 16 | # Test binary, build with `go test -c` 17 | *.test 18 | 19 | # Output of the go coverage tool, specifically when used with LiteIDE 20 | *.out 21 | 22 | # Kubernetes Generated files - skip generated files, except for vendored files 23 | 24 | !vendor/**/zz_generated.* 25 | 26 | # editor and IDE paraphernalia 27 | .idea 28 | *.swp 29 | *.swo 30 | *~ 31 | 32 | # Exclude all build related files 33 | build/ 34 | 35 | # CRDs for fuzzing tests. 36 | internal/controller/testdata/crd 37 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: image-automation-controller 2 | 3 | builds: 4 | - skip: true 5 | 6 | release: 7 | extra_files: 8 | - glob: config/release/*.yaml 9 | prerelease: "true" 10 | header: | 11 | ## Changelog 12 | 13 | [{{.Tag}} changelog](https://github.com/fluxcd/{{.ProjectName}}/blob/{{.Tag}}/CHANGELOG.md) 14 | footer: | 15 | ## Container images 16 | 17 | - `docker.io/fluxcd/{{.ProjectName}}:{{.Tag}}` 18 | - `ghcr.io/fluxcd/{{.ProjectName}}:{{.Tag}}` 19 | 20 | Supported architectures: `linux/amd64`, `linux/arm64` and `linux/arm/v7`. 21 | 22 | The container images are built on GitHub hosted runners and are signed with cosign and GitHub OIDC. 23 | To verify the images and their provenance (SLSA level 3), please see the [security documentation](https://fluxcd.io/flux/security/). 24 | 25 | changelog: 26 | disable: true 27 | 28 | checksum: 29 | extra_files: 30 | - glob: config/release/*.yaml 31 | 32 | source: 33 | enabled: true 34 | name_template: "{{ .ProjectName }}_{{ .Version }}_source_code" 35 | 36 | sboms: 37 | - id: source 38 | artifacts: source 39 | documents: 40 | - "{{ .ProjectName }}_{{ .Version }}_sbom.spdx.json" 41 | 42 | # signs the checksum file 43 | # all files (including the sboms) are included in the checksum 44 | # https://goreleaser.com/customization/sign 45 | signs: 46 | - cmd: cosign 47 | env: 48 | - COSIGN_EXPERIMENTAL=1 49 | certificate: "${artifact}.pem" 50 | args: 51 | - sign-blob 52 | - "--yes" 53 | - "--output-certificate=${certificate}" 54 | - "--output-signature=${signature}" 55 | - "${artifact}" 56 | artifacts: checksum 57 | output: true 58 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | 3 | The image automation controller follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). 4 | -------------------------------------------------------------------------------- /DCO: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 660 York Street, Suite 102, 6 | San Francisco, CA 94110 USA 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | 12 | Developer's Certificate of Origin 1.1 13 | 14 | By making a contribution to this project, I certify that: 15 | 16 | (a) The contribution was created in whole or in part by me and I 17 | have the right to submit it under the open source license 18 | indicated in the file; or 19 | 20 | (b) The contribution is based upon previous work that, to the best 21 | of my knowledge, is covered under an appropriate open source 22 | license and I have the right under that license to submit that 23 | work with modifications, whether created in whole or in part 24 | by me, under the same open source license (unless I am 25 | permitted to submit under a different license), as indicated 26 | in the file; or 27 | 28 | (c) The contribution was provided directly to me by some other 29 | person who certified (a), (b) or (c) and I have not modified 30 | it. 31 | 32 | (d) I understand and agree that this project and the contribution 33 | are public and that a record of the contribution (including all 34 | personal information I submit with it, including my sign-off) is 35 | maintained indefinitely and may be redistributed consistent with 36 | this project or the open source license(s) involved. 37 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | > **Note:** Please take a look at 4 | > to find out about how to contribute to Flux and how to interact with the 5 | > Flux Development team. 6 | 7 | ## Installing required dependencies 8 | 9 | There are a number of dependencies required to be able to run the controller and its test suite locally: 10 | 11 | - [Install Go](https://golang.org/doc/install) 12 | - [Install Kustomize](https://kubernetes-sigs.github.io/kustomize/installation/) 13 | - [Install Docker](https://docs.docker.com/engine/install/) 14 | - (Optional) [Install Kubebuilder](https://book.kubebuilder.io/quick-start.html#installation) 15 | 16 | The following dependencies are also used by some of the `make` targets: 17 | 18 | - `controller-gen` (v0.7.0) 19 | - `gen-crd-api-reference-docs` (v0.3.0) 20 | - `setup-envtest` (latest) 21 | 22 | If any of the above dependencies are not present on your system, the first invocation of a `make` target that requires them will install them. 23 | 24 | ## How to run the test suite 25 | 26 | Prerequisites: 27 | * Go >= 1.24 28 | 29 | You can run the test suite by simply doing 30 | 31 | ```sh 32 | make test 33 | ``` 34 | ## How to run the controller locally 35 | 36 | Install the controller's CRDs on your test cluster: 37 | 38 | ```sh 39 | make install 40 | ``` 41 | 42 | Note that `image-automation-controller` depends on [source-controller](https://github.com/fluxcd/source-controller) to acquire its artifacts and [image-reflector-controller](https://github.com/fluxcd/image-reflector-controller) to access container image metadata. Ensure that they are both running on your test cluster prior to running the `image-automation-controller`. 43 | 44 | Run the controller locally: 45 | 46 | ```sh 47 | make run 48 | ``` 49 | 50 | ## How to install the controller 51 | 52 | ### Building the container image 53 | 54 | Set the name of the container image to be created from the source code. This will be used when building, pushing and referring to the image on YAML files: 55 | 56 | ```sh 57 | export IMG=registry-path/kustomize-controller 58 | export TAG=latest 59 | ``` 60 | Build and push the container image, tagging it as `$(IMG):$(TAG)`: 61 | 62 | ```sh 63 | BUILD_ARGS=--push make docker-build 64 | ``` 65 | **Note**: `make docker-build` will build images for the `amd64`,`arm64` and `arm/v7` architectures. 66 | 67 | If you get the following error when building the docker container: 68 | ``` 69 | Multiple platforms feature is currently not supported for docker driver. 70 | Please switch to a different driver (eg. "docker buildx create --use") 71 | ``` 72 | 73 | you may need to create and switch to a new builder that supports multiple platforms: 74 | 75 | ```sh 76 | docker buildx create --use 77 | ``` 78 | 79 | ### Deploying into a cluster 80 | 81 | Deploy `image-automation-controller` into the cluster that is configured in the local kubeconfig file (i.e. `~/.kube/config`): 82 | 83 | ```sh 84 | make deploy 85 | ``` 86 | 87 | ### Debugging controller with VSCode 88 | 89 | Create a `.vscode/launch.json` file: 90 | ```json 91 | { 92 | "version": "0.2.0", 93 | "configurations": [ 94 | { 95 | "name": "Launch Package", 96 | "type": "go", 97 | "request": "launch", 98 | "mode": "auto", 99 | "program": "${workspaceFolder}/main.go" 100 | } 101 | ] 102 | } 103 | ``` 104 | 105 | Start debugging by either clicking `Run` > `Start Debugging` or using 106 | the relevant shortcut. 107 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BASE_VARIANT=alpine 2 | ARG GO_VERSION=1.24 3 | ARG XX_VERSION=1.6.1 4 | 5 | FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx 6 | 7 | FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-${BASE_VARIANT} AS gostable 8 | 9 | FROM gostable AS go-linux 10 | 11 | # Build-base consists of build platform dependencies and xx. 12 | # These will be used at current arch to yield execute the cross compilations. 13 | FROM go-${TARGETOS} AS build-base 14 | 15 | RUN apk add clang lld 16 | 17 | COPY --from=xx / / 18 | 19 | # build can still be cached at build platform architecture. 20 | FROM build-base AS build 21 | 22 | ARG TARGETPLATFORM 23 | 24 | # Some dependencies have to installed for the target platform: 25 | # https://github.com/tonistiigi/xx#go--cgo 26 | RUN xx-apk add musl-dev gcc 27 | 28 | # Configure workspace 29 | WORKDIR /workspace 30 | 31 | # Copy api submodule 32 | COPY api/ api/ 33 | 34 | # Copy modules manifests 35 | COPY go.mod go.mod 36 | COPY go.sum go.sum 37 | 38 | # Cache modules 39 | RUN go mod download 40 | 41 | # Copy source code 42 | COPY main.go main.go 43 | COPY pkg/ pkg/ 44 | COPY internal/ internal/ 45 | 46 | ARG TARGETPLATFORM 47 | ARG TARGETARCH 48 | 49 | # Reasons why CGO is in use: 50 | # - The SHA1 implementation (sha1cd) used by go-git depends on CGO for 51 | # performance reasons. See: https://github.com/pjbgf/sha1cd/issues/15 52 | ENV CGO_ENABLED=1 53 | 54 | RUN export CGO_LDFLAGS="-static -fuse-ld=lld" && \ 55 | xx-go build \ 56 | -ldflags "-s -w" \ 57 | -tags 'netgo,osusergo,static_build' \ 58 | -o /image-automation-controller -trimpath main.go; 59 | 60 | # Ensure that the binary was cross-compiled correctly to the target platform. 61 | RUN xx-verify --static /image-automation-controller 62 | 63 | FROM alpine:3.21 64 | 65 | ARG TARGETPLATFORM 66 | RUN apk --no-cache add ca-certificates \ 67 | && update-ca-certificates 68 | 69 | # Copy over binary from build 70 | COPY --from=build /image-automation-controller /usr/local/bin/ 71 | 72 | USER 65534:65534 73 | ENTRYPOINT [ "image-automation-controller" ] 74 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | The maintainers are generally available in Slack at 2 | https://cloud-native.slack.com in #flux (https://cloud-native.slack.com/messages/CLAJ40HV3) 3 | (obtain an invitation at https://slack.cncf.io/). 4 | 5 | In additional to those listed below, this project shares maintainers 6 | from the main Flux v2 git repository, as listed in 7 | 8 | https://github.com/fluxcd/flux2/blob/main/MAINTAINERS 9 | 10 | In alphabetical order: 11 | 12 | Dipti Pai, Microsoft (github: @dipti-pai, slack: Dipti Pai) 13 | Paulo Gomes, SUSE (github: @pjbgf, slack: pjbgf) 14 | 15 | Retired maintainers: 16 | 17 | - Michael Bridgen 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Image URL to use all building/pushing image targets 2 | IMG ?= fluxcd/image-automation-controller 3 | # Image tag to use all building/push image targets 4 | TAG ?= latest 5 | 6 | # Produce CRDs that work back to Kubernetes 1.16 7 | CRD_OPTIONS ?= crd:crdVersions=v1 8 | 9 | # Allows for defining additional Docker buildx arguments, 10 | # e.g. '--push'. 11 | BUILD_ARGS ?= 12 | # Architectures to build images for 13 | BUILD_PLATFORMS ?= linux/amd64,linux/arm64,linux/arm/v7 14 | 15 | # Allows for defining additional Go test args, e.g. '-tags integration'. 16 | GO_TEST_ARGS ?= -race 17 | 18 | # Defines whether cosign verification should be skipped. 19 | SKIP_COSIGN_VERIFICATION ?= false 20 | 21 | # Directory with versioned, downloaded things 22 | CACHE := cache 23 | 24 | # Version of the source-controller from which to get the GitRepository CRD. 25 | # Pulls source-controller/api's version set in go.mod. 26 | SOURCE_VER ?= $(shell go list -m github.com/fluxcd/source-controller/api | awk '{print $$2}') 27 | 28 | # Version of the image-reflector-controller from which to get the ImagePolicy CRD. 29 | # Pulls image-reflector-controller/api's version set in go.mod. 30 | REFLECTOR_VER ?= $(shell go list -m github.com/fluxcd/image-reflector-controller/api | awk '{print $$2}') 31 | 32 | # Repository root based on Git metadata. 33 | REPOSITORY_ROOT := $(shell git rev-parse --show-toplevel) 34 | BUILD_DIR := $(REPOSITORY_ROOT)/build 35 | 36 | # FUZZ_TIME defines the max amount of time, in Go Duration, 37 | # each fuzzer should run for. 38 | FUZZ_TIME ?= 1m 39 | 40 | ifeq ($(shell uname -s),Darwin) 41 | GO_STATIC_FLAGS=-ldflags "-s -w" -tags 'netgo,osusergo,static_build' 42 | endif 43 | 44 | ifeq ($(shell uname -s),Linux) 45 | GO_STATIC_FLAGS=-ldflags "-s -w" -tags 'netgo,osusergo,static_build' 46 | endif 47 | 48 | # API (doc) generation utilities 49 | CONTROLLER_GEN_VERSION ?= v0.16.1 50 | GEN_API_REF_DOCS_VERSION ?= e327d0730470cbd61b06300f81c5fcf91c23c113 51 | 52 | # If gobin not set, create one on ./build and add to path. 53 | ifeq (,$(shell go env GOBIN)) 54 | export GOBIN=$(BUILD_DIR)/gobin 55 | else 56 | export GOBIN=$(shell go env GOBIN) 57 | endif 58 | export PATH:=${GOBIN}:${PATH} 59 | 60 | # Architecture to use envtest with 61 | ifeq ($(shell uname -m),x86_64) 62 | ENVTEST_ARCH ?= amd64 63 | else 64 | ENVTEST_ARCH ?= arm64 65 | endif 66 | 67 | ifeq ($(shell uname -s),Darwin) 68 | # Envtest only supports darwin-amd64 69 | ENVTEST_ARCH=amd64 70 | endif 71 | 72 | TEST_CRDS := internal/controller/testdata/crds 73 | 74 | # Log level for `make run` 75 | LOG_LEVEL ?= info 76 | 77 | # Architecture to use envtest with 78 | ENVTEST_ARCH ?= amd64 79 | 80 | all: manager 81 | 82 | # Running the tests requires the source.toolkit.fluxcd.io CRDs 83 | test_deps: ${TEST_CRDS}/imagepolicies.yaml ${TEST_CRDS}/gitrepositories.yaml 84 | 85 | clean_test_deps: 86 | rm -r ${TEST_CRDS} 87 | 88 | ${TEST_CRDS}/imagepolicies.yaml: ${CACHE}/imagepolicies_${REFLECTOR_VER}.yaml 89 | mkdir -p ${TEST_CRDS} 90 | cp $^ $@ 91 | 92 | ${TEST_CRDS}/gitrepositories.yaml: ${CACHE}/gitrepositories_${SOURCE_VER}.yaml 93 | mkdir -p ${TEST_CRDS} 94 | cp $^ $@ 95 | 96 | ${CACHE}/gitrepositories_${SOURCE_VER}.yaml: 97 | mkdir -p ${CACHE} 98 | curl -s --fail https://raw.githubusercontent.com/fluxcd/source-controller/${SOURCE_VER}/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml \ 99 | -o ${CACHE}/gitrepositories_${SOURCE_VER}.yaml 100 | 101 | ${CACHE}/imagepolicies_${REFLECTOR_VER}.yaml: 102 | mkdir -p ${CACHE} 103 | curl -s --fail https://raw.githubusercontent.com/fluxcd/image-reflector-controller/${REFLECTOR_VER}/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml \ 104 | -o ${CACHE}/imagepolicies_${REFLECTOR_VER}.yaml 105 | 106 | check-deps: 107 | ifeq ($(shell uname -s),Darwin) 108 | if ! command -v pkg-config &> /dev/null; then echo "pkg-config is required"; exit 1; fi 109 | endif 110 | 111 | KUBEBUILDER_ASSETS?="$(shell $(ENVTEST) --arch=$(ENVTEST_ARCH) use -i $(ENVTEST_KUBERNETES_VERSION) --bin-dir=$(ENVTEST_ASSETS_DIR) -p path)" 112 | test: tidy test-api test_deps generate fmt vet manifests api-docs install-envtest ## Run tests 113 | KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) \ 114 | go test $(GO_STATIC_FLAGS) $(GO_TEST_ARGS) ./... -coverprofile cover.out 115 | 116 | test-api: ## Run api tests 117 | cd api; go test $(GO_TEST_ARGS) ./... -coverprofile cover.out 118 | 119 | manager: generate fmt vet ## Build manager binary 120 | go build -o $(BUILD_DIR)/bin/manager ./main.go 121 | 122 | run: generate fmt vet manifests # Run against the configured Kubernetes cluster in ~/.kube/config 123 | go run $(GO_STATIC_FLAGS) ./main.go --log-level=${LOG_LEVEL} --log-encoding=console 124 | 125 | install: manifests ## Install CRDs into a cluster 126 | kustomize build config/crd | kubectl apply -f - 127 | 128 | uninstall: manifests ## Uninstall CRDs from a cluster 129 | kustomize build config/crd | kubectl delete -f - 130 | 131 | deploy: manifests ## Deploy controller in the configured Kubernetes cluster in ~/.kube/config 132 | cd config/manager && kustomize edit set image fluxcd/image-automation-controller=$(IMG):$(TAG) 133 | kustomize build config/default | kubectl apply -f - 134 | 135 | dev-deploy: manifests 136 | mkdir -p config/dev && cp config/default/* config/dev 137 | cd config/dev && kustomize edit set image fluxcd/image-automation-controller=$(IMG):$(TAG) 138 | kustomize build config/dev | kubectl apply -f - 139 | rm -rf config/dev 140 | 141 | manifests: controller-gen ## Generate manifests e.g. CRD, RBAC etc. 142 | $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role paths="./..." output:crd:artifacts:config="config/crd/bases" 143 | cd api; $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role paths="./..." output:crd:artifacts:config="../config/crd/bases" 144 | 145 | api-docs: gen-crd-api-reference-docs ## Generate API reference documentation 146 | $(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 147 | 148 | tidy: ## Run go mod tidy 149 | cd api; rm -f go.sum; go mod tidy -compat=1.24 150 | rm -f go.sum; go mod tidy -compat=1.24 151 | 152 | fmt: ## Run go fmt against code 153 | go fmt ./... 154 | cd api; go fmt ./... 155 | 156 | vet: ## Run go vet against code 157 | go vet ./... 158 | cd api; go vet ./... 159 | 160 | 161 | generate: controller-gen ## Generate code 162 | cd api; $(CONTROLLER_GEN) object:headerFile="../hack/boilerplate.go.txt" paths="./..." 163 | 164 | docker-build: ## Build the Docker image 165 | docker buildx build \ 166 | --platform=$(BUILD_PLATFORMS) \ 167 | -t $(IMG):$(TAG) \ 168 | $(BUILD_ARGS) . 169 | 170 | docker-push: ## Push the Docker image 171 | docker push $(IMG):$(TAG) 172 | 173 | docker-deploy: ## Set the Docker image in-cluster 174 | kubectl -n flux-system set image deployment/image-automation-controller manager=$(IMG):$(TAG) 175 | 176 | # Find or download controller-gen 177 | CONTROLLER_GEN = $(GOBIN)/controller-gen 178 | .PHONY: controller-gen 179 | controller-gen: ## Download controller-gen locally if necessary. 180 | $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_GEN_VERSION)) 181 | 182 | # Find or download gen-crd-api-reference-docs 183 | GEN_CRD_API_REFERENCE_DOCS = $(GOBIN)/gen-crd-api-reference-docs 184 | .PHONY: gen-crd-api-reference-docs 185 | gen-crd-api-reference-docs: 186 | $(call go-install-tool,$(GEN_CRD_API_REFERENCE_DOCS),github.com/ahmetb/gen-crd-api-reference-docs@$(GEN_API_REF_DOCS_VERSION)) 187 | 188 | ENVTEST_ASSETS_DIR=$(BUILD_DIR)/testbin 189 | ENVTEST_KUBERNETES_VERSION?=latest 190 | install-envtest: setup-envtest 191 | mkdir -p ${ENVTEST_ASSETS_DIR} 192 | $(ENVTEST) use $(ENVTEST_KUBERNETES_VERSION) --arch=$(ENVTEST_ARCH) --bin-dir=$(ENVTEST_ASSETS_DIR) 193 | chmod -R u+w $(BUILD_DIR)/testbin 194 | 195 | ENVTEST = $(GOBIN)/setup-envtest 196 | .PHONY: envtest 197 | setup-envtest: ## Download envtest-setup locally if necessary. 198 | $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest@latest) 199 | 200 | # Build fuzzers used by oss-fuzz. 201 | fuzz-build: 202 | rm -rf $(shell pwd)/build/fuzz/ 203 | mkdir -p $(shell pwd)/build/fuzz/out/ 204 | 205 | docker build . --tag local-fuzzing:latest -f tests/fuzz/Dockerfile.builder 206 | docker run --rm \ 207 | -e FUZZING_LANGUAGE=go -e SANITIZER=address \ 208 | -e CIFUZZ_DEBUG='True' -e OSS_FUZZ_PROJECT_NAME=fluxcd \ 209 | -v "$(shell pwd)/build/fuzz/out":/out \ 210 | local-fuzzing:latest 211 | 212 | # Run each fuzzer once to ensure they will work when executed by oss-fuzz. 213 | fuzz-smoketest: fuzz-build 214 | docker run --rm \ 215 | -v "$(shell pwd)/build/fuzz/out":/out \ 216 | -v "$(shell pwd)/tests/fuzz/oss_fuzz_run.sh":/runner.sh \ 217 | local-fuzzing:latest \ 218 | bash -c "/runner.sh" 219 | 220 | # Run fuzz tests for the duration set in FUZZ_TIME. 221 | fuzz-native: 222 | KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) \ 223 | FUZZ_TIME=$(FUZZ_TIME) \ 224 | ./tests/fuzz/native_go_run.sh 225 | 226 | # go-install-tool will 'go install' any package $2 and install it to $1. 227 | define go-install-tool 228 | @[ -f $(1) ] || { \ 229 | set -e ;\ 230 | TMP_DIR=$$(mktemp -d) ;\ 231 | cd $$TMP_DIR ;\ 232 | go mod init tmp ;\ 233 | echo "Downloading $(2)" ;\ 234 | env -i bash -c "GOBIN=$(GOBIN) PATH=$(PATH) GOPATH=$(shell go env GOPATH) GOCACHE=$(shell go env GOCACHE) go install $(2)" ;\ 235 | rm -rf $$TMP_DIR ;\ 236 | } 237 | endef 238 | 239 | update-attributions: 240 | ./hack/update-attributions.sh 241 | 242 | verify: 243 | ifneq (, $(shell git status --porcelain --untracked-files=no)) 244 | @{ \ 245 | echo "working directory is dirty:"; \ 246 | git --no-pager diff; \ 247 | exit 1; \ 248 | } 249 | endif 250 | 251 | .PHONY: help 252 | help: ## Display this help menu 253 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 254 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: toolkit.fluxcd.io 2 | repo: github.com/fluxcd/image-automation-controller 3 | resources: 4 | - group: image 5 | kind: ImageUpdateAutomation 6 | version: v1beta1 7 | - group: image 8 | kind: ImageUpdateAutomation 9 | version: v1beta2 10 | version: "2" 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Image automation controller 2 | 3 | [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/4789/badge)](https://bestpractices.coreinfrastructure.org/projects/4789) 4 | [![report](https://goreportcard.com/badge/github.com/fluxcd/image-automation-controller)](https://goreportcard.com/report/github.com/fluxcd/image-automation-controller) 5 | [![license](https://img.shields.io/github/license/fluxcd/image-automation-controller.svg)](https://github.com/fluxcd/image-automation-controller/blob/main/LICENSE) 6 | [![release](https://img.shields.io/github/release/fluxcd/image-automation-controller/all.svg)](https://github.com/fluxcd/image-automation-controller/releases) 7 | 8 | This controller automates updates to YAML when new container images 9 | are available. 10 | 11 | Its sibling, 12 | [image-reflector-controller](https://github.com/fluxcd/image-reflector-controller), 13 | scans container image repositories and reflects the metadata in 14 | Kubernetes resources. This controller reacts to that image metadata by 15 | updating YAML files in a git repository, and committing the changes. 16 | 17 | ## How to install it 18 | 19 | Please see the [installation and use 20 | guide](https://fluxcd.io/flux/guides/image-update/). 21 | 22 | ## How to work on it 23 | 24 | For additional information on dependecies and how to contribute 25 | please refer to [DEVELOPMENT.md](DEVELOPMENT.md). 26 | -------------------------------------------------------------------------------- /api/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fluxcd/image-automation-controller/api 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/fluxcd/pkg/apis/meta v1.12.0 7 | github.com/fluxcd/source-controller/api v1.6.0 8 | k8s.io/apimachinery v0.33.0 9 | sigs.k8s.io/controller-runtime v0.21.0 10 | ) 11 | 12 | require ( 13 | github.com/fluxcd/pkg/apis/acl v0.7.0 // indirect 14 | github.com/fxamacker/cbor/v2 v2.8.0 // indirect 15 | github.com/go-logr/logr v1.4.2 // indirect 16 | github.com/gogo/protobuf v1.3.2 // indirect 17 | github.com/json-iterator/go v1.1.12 // indirect 18 | github.com/kr/text v0.2.0 // indirect 19 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 20 | github.com/modern-go/reflect2 v1.0.2 // indirect 21 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 22 | github.com/x448/float16 v0.8.4 // indirect 23 | golang.org/x/net v0.40.0 // indirect 24 | golang.org/x/text v0.25.0 // indirect 25 | gopkg.in/inf.v0 v0.9.1 // indirect 26 | k8s.io/klog/v2 v2.130.1 // indirect 27 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect 28 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 29 | sigs.k8s.io/randfill v1.0.0 // indirect 30 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect 31 | sigs.k8s.io/yaml v1.4.0 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /api/v1beta1/condition_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta1 18 | 19 | const ( 20 | // ReconciliationSucceededReason represents the fact that 21 | // the reconciliation succeeded. 22 | ReconciliationSucceededReason string = "ReconciliationSucceeded" 23 | 24 | // ReconciliationFailedReason represents the fact that 25 | // the reconciliation failed. 26 | ReconciliationFailedReason string = "ReconciliationFailed" 27 | ) 28 | -------------------------------------------------------------------------------- /api/v1beta1/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1beta1 contains API types for the image API group, version 18 | // v1beta1. The types here are concerned with automated updates to 19 | // git, based on metadata from OCI image registries gathered by the 20 | // image-reflector-controller. v1alpha2 did some rearrangement from 21 | // v1alpha1 to make room for future enhancements; v1beta1 does not 22 | // change the schema from v1alpha2. 23 | // 24 | // +kubebuilder:object:generate=true 25 | // +groupName=image.toolkit.fluxcd.io 26 | package v1beta1 27 | -------------------------------------------------------------------------------- /api/v1beta1/git.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta1 18 | 19 | import ( 20 | "github.com/fluxcd/pkg/apis/meta" 21 | sourcev1 "github.com/fluxcd/source-controller/api/v1" 22 | ) 23 | 24 | type GitSpec struct { 25 | // Checkout gives the parameters for cloning the git repository, 26 | // ready to make changes. If not present, the `spec.ref` field from the 27 | // referenced `GitRepository` or its default will be used. 28 | // +optional 29 | Checkout *GitCheckoutSpec `json:"checkout,omitempty"` 30 | 31 | // Commit specifies how to commit to the git repository. 32 | // +required 33 | Commit CommitSpec `json:"commit"` 34 | 35 | // Push specifies how and where to push commits made by the 36 | // automation. If missing, commits are pushed (back) to 37 | // `.spec.checkout.branch` or its default. 38 | // +optional 39 | Push *PushSpec `json:"push,omitempty"` 40 | } 41 | 42 | type GitCheckoutSpec struct { 43 | // Reference gives a branch, tag or commit to clone from the Git 44 | // repository. 45 | // +required 46 | Reference sourcev1.GitRepositoryRef `json:"ref"` 47 | } 48 | 49 | // CommitSpec specifies how to commit changes to the git repository 50 | type CommitSpec struct { 51 | // Author gives the email and optionally the name to use as the 52 | // author of commits. 53 | // +required 54 | Author CommitUser `json:"author"` 55 | // SigningKey provides the option to sign commits with a GPG key 56 | // +optional 57 | SigningKey *SigningKey `json:"signingKey,omitempty"` 58 | // MessageTemplate provides a template for the commit message, 59 | // into which will be interpolated the details of the change made. 60 | // +optional 61 | MessageTemplate string `json:"messageTemplate,omitempty"` 62 | } 63 | 64 | type CommitUser struct { 65 | // Name gives the name to provide when making a commit. 66 | // +optional 67 | Name string `json:"name,omitempty"` 68 | // Email gives the email to provide when making a commit. 69 | // +required 70 | Email string `json:"email"` 71 | } 72 | 73 | // SigningKey references a Kubernetes secret that contains a GPG keypair 74 | type SigningKey struct { 75 | // SecretRef holds the name to a secret that contains a 'git.asc' key 76 | // corresponding to the ASCII Armored file containing the GPG signing 77 | // keypair as the value. It must be in the same namespace as the 78 | // ImageUpdateAutomation. 79 | // +required 80 | SecretRef meta.LocalObjectReference `json:"secretRef,omitempty"` 81 | } 82 | 83 | // PushSpec specifies how and where to push commits. 84 | type PushSpec struct { 85 | // Branch specifies that commits should be pushed to the branch 86 | // named. The branch is created using `.spec.checkout.branch` as the 87 | // starting point, if it doesn't already exist. 88 | // +optional 89 | Branch string `json:"branch,omitempty"` 90 | 91 | // Refspec specifies the Git Refspec to use for a push operation. 92 | // If both Branch and Refspec are provided, then the commit is pushed 93 | // to the branch and also using the specified refspec. 94 | // For more details about Git Refspecs, see: 95 | // https://git-scm.com/book/en/v2/Git-Internals-The-Refspec 96 | // +optional 97 | Refspec string `json:"refspec,omitempty"` 98 | 99 | // Options specifies the push options that are sent to the Git 100 | // server when performing a push operation. For details, see: 101 | // https://git-scm.com/docs/git-push#Documentation/git-push.txt---push-optionltoptiongt 102 | // +optional 103 | Options map[string]string `json:"options,omitempty"` 104 | } 105 | -------------------------------------------------------------------------------- /api/v1beta1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta1 18 | 19 | import ( 20 | "k8s.io/apimachinery/pkg/runtime/schema" 21 | "sigs.k8s.io/controller-runtime/pkg/scheme" 22 | ) 23 | 24 | var ( 25 | // GroupVersion is group version used to register these objects 26 | GroupVersion = schema.GroupVersion{Group: "image.toolkit.fluxcd.io", Version: "v1beta1"} 27 | 28 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 29 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 30 | 31 | // AddToScheme adds the types in this group-version to the given scheme. 32 | AddToScheme = SchemeBuilder.AddToScheme 33 | ) 34 | -------------------------------------------------------------------------------- /api/v1beta1/imageupdateautomation_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta1 18 | 19 | import ( 20 | "time" 21 | 22 | apimeta "k8s.io/apimachinery/pkg/api/meta" 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | 25 | "github.com/fluxcd/pkg/apis/meta" 26 | ) 27 | 28 | const ( 29 | ImageUpdateAutomationKind = "ImageUpdateAutomation" 30 | ImageUpdateAutomationFinalizer = "finalizers.fluxcd.io" 31 | ) 32 | 33 | // ImageUpdateAutomationSpec defines the desired state of ImageUpdateAutomation 34 | type ImageUpdateAutomationSpec struct { 35 | // SourceRef refers to the resource giving access details 36 | // to a git repository. 37 | // +required 38 | SourceRef CrossNamespaceSourceReference `json:"sourceRef"` 39 | // GitSpec contains all the git-specific definitions. This is 40 | // technically optional, but in practice mandatory until there are 41 | // other kinds of source allowed. 42 | // +optional 43 | GitSpec *GitSpec `json:"git,omitempty"` 44 | 45 | // Interval gives an lower bound for how often the automation 46 | // run should be attempted. 47 | // +kubebuilder:validation:Type=string 48 | // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$" 49 | // +required 50 | Interval metav1.Duration `json:"interval"` 51 | 52 | // Update gives the specification for how to update the files in 53 | // the repository. This can be left empty, to use the default 54 | // value. 55 | // +kubebuilder:default={"strategy":"Setters"} 56 | Update *UpdateStrategy `json:"update,omitempty"` 57 | 58 | // Suspend tells the controller to not run this automation, until 59 | // it is unset (or set to false). Defaults to false. 60 | // +optional 61 | Suspend bool `json:"suspend,omitempty"` 62 | } 63 | 64 | // UpdateStrategyName is the type for names that go in 65 | // .update.strategy. NB the value in the const immediately below. 66 | // +kubebuilder:validation:Enum=Setters 67 | type UpdateStrategyName string 68 | 69 | const ( 70 | // UpdateStrategySetters is the name of the update strategy that 71 | // uses kyaml setters. NB the value in the enum annotation for the 72 | // type, above. 73 | UpdateStrategySetters UpdateStrategyName = "Setters" 74 | ) 75 | 76 | // UpdateStrategy is a union of the various strategies for updating 77 | // the Git repository. Parameters for each strategy (if any) can be 78 | // inlined here. 79 | type UpdateStrategy struct { 80 | // Strategy names the strategy to be used. 81 | // +required 82 | // +kubebuilder:default=Setters 83 | Strategy UpdateStrategyName `json:"strategy"` 84 | 85 | // Path to the directory containing the manifests to be updated. 86 | // Defaults to 'None', which translates to the root path 87 | // of the GitRepositoryRef. 88 | // +optional 89 | Path string `json:"path,omitempty"` 90 | } 91 | 92 | // ImageUpdateAutomationStatus defines the observed state of ImageUpdateAutomation 93 | type ImageUpdateAutomationStatus struct { 94 | // LastAutomationRunTime records the last time the controller ran 95 | // this automation through to completion (even if no updates were 96 | // made). 97 | // +optional 98 | LastAutomationRunTime *metav1.Time `json:"lastAutomationRunTime,omitempty"` 99 | // LastPushCommit records the SHA1 of the last commit made by the 100 | // controller, for this automation object 101 | // +optional 102 | LastPushCommit string `json:"lastPushCommit,omitempty"` 103 | // LastPushTime records the time of the last pushed change. 104 | // +optional 105 | LastPushTime *metav1.Time `json:"lastPushTime,omitempty"` 106 | // +optional 107 | ObservedGeneration int64 `json:"observedGeneration,omitempty"` 108 | // +optional 109 | Conditions []metav1.Condition `json:"conditions,omitempty"` 110 | meta.ReconcileRequestStatus `json:",inline"` 111 | } 112 | 113 | const ( 114 | // GitNotAvailableReason is used for ConditionReady when the 115 | // automation run cannot proceed because the git repository is 116 | // missing or cannot be cloned. 117 | GitNotAvailableReason = "GitRepositoryNotAvailable" 118 | // NoStrategyReason is used for ConditionReady when the automation 119 | // run cannot proceed because there is no update strategy given in 120 | // the spec. 121 | NoStrategyReason = "MissingUpdateStrategy" 122 | ) 123 | 124 | // SetImageUpdateAutomationReadiness sets the ready condition with the given status, reason and message. 125 | func SetImageUpdateAutomationReadiness(auto *ImageUpdateAutomation, status metav1.ConditionStatus, reason, message string) { 126 | auto.Status.ObservedGeneration = auto.ObjectMeta.Generation 127 | newCondition := metav1.Condition{ 128 | Type: meta.ReadyCondition, 129 | Status: status, 130 | Reason: reason, 131 | Message: message, 132 | } 133 | apimeta.SetStatusCondition(auto.GetStatusConditions(), newCondition) 134 | } 135 | 136 | //+kubebuilder:object:root=true 137 | //+kubebuilder:subresource:status 138 | //+kubebuilder:deprecatedversion:warning="v1beta1 ImageUpdateAutomation is deprecated, upgrade to v1beta2" 139 | //+kubebuilder:printcolumn:name="Last run",type=string,JSONPath=`.status.lastAutomationRunTime` 140 | 141 | // ImageUpdateAutomation is the Schema for the imageupdateautomations API 142 | type ImageUpdateAutomation struct { 143 | metav1.TypeMeta `json:",inline"` 144 | metav1.ObjectMeta `json:"metadata,omitempty"` 145 | 146 | Spec ImageUpdateAutomationSpec `json:"spec,omitempty"` 147 | // +kubebuilder:default={"observedGeneration":-1} 148 | Status ImageUpdateAutomationStatus `json:"status,omitempty"` 149 | } 150 | 151 | // GetRequeueAfter returns the duration after which the ImageUpdateAutomation 152 | // must be reconciled again. 153 | func (auto ImageUpdateAutomation) GetRequeueAfter() time.Duration { 154 | return auto.Spec.Interval.Duration 155 | } 156 | 157 | // GetConditions returns the status conditions of the object. 158 | func (auto ImageUpdateAutomation) GetConditions() []metav1.Condition { 159 | return auto.Status.Conditions 160 | } 161 | 162 | // SetConditions sets the status conditions on the object. 163 | func (auto *ImageUpdateAutomation) SetConditions(conditions []metav1.Condition) { 164 | auto.Status.Conditions = conditions 165 | } 166 | 167 | // GetStatusConditions returns a pointer to the Status.Conditions slice. 168 | // Deprecated: use GetConditions instead. 169 | func (auto *ImageUpdateAutomation) GetStatusConditions() *[]metav1.Condition { 170 | return &auto.Status.Conditions 171 | } 172 | 173 | //+kubebuilder:object:root=true 174 | 175 | // ImageUpdateAutomationList contains a list of ImageUpdateAutomation 176 | type ImageUpdateAutomationList struct { 177 | metav1.TypeMeta `json:",inline"` 178 | metav1.ListMeta `json:"metadata,omitempty"` 179 | Items []ImageUpdateAutomation `json:"items"` 180 | } 181 | 182 | func init() { 183 | SchemeBuilder.Register(&ImageUpdateAutomation{}, &ImageUpdateAutomationList{}) 184 | } 185 | -------------------------------------------------------------------------------- /api/v1beta1/reference.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020, 2021 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta1 18 | 19 | import "fmt" 20 | 21 | // CrossNamespaceSourceReference contains enough information to let you locate the 22 | // typed Kubernetes resource object at cluster level. 23 | type CrossNamespaceSourceReference struct { 24 | // API version of the referent. 25 | // +optional 26 | APIVersion string `json:"apiVersion,omitempty"` 27 | 28 | // Kind of the referent. 29 | // +kubebuilder:validation:Enum=GitRepository 30 | // +kubebuilder:default=GitRepository 31 | // +required 32 | Kind string `json:"kind"` 33 | 34 | // Name of the referent. 35 | // +required 36 | Name string `json:"name"` 37 | 38 | // Namespace of the referent, defaults to the namespace of the Kubernetes resource object that contains the reference. 39 | // +optional 40 | Namespace string `json:"namespace,omitempty"` 41 | } 42 | 43 | func (s *CrossNamespaceSourceReference) String() string { 44 | if s.Namespace != "" { 45 | return fmt.Sprintf("%s/%s/%s", s.Kind, s.Namespace, s.Name) 46 | } 47 | return fmt.Sprintf("%s/%s", s.Kind, s.Name) 48 | } 49 | -------------------------------------------------------------------------------- /api/v1beta1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | /* 4 | Copyright 2024 The Flux authors 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package v1beta1 22 | 23 | import ( 24 | "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | runtime "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 29 | func (in *CommitSpec) DeepCopyInto(out *CommitSpec) { 30 | *out = *in 31 | out.Author = in.Author 32 | if in.SigningKey != nil { 33 | in, out := &in.SigningKey, &out.SigningKey 34 | *out = new(SigningKey) 35 | **out = **in 36 | } 37 | } 38 | 39 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommitSpec. 40 | func (in *CommitSpec) DeepCopy() *CommitSpec { 41 | if in == nil { 42 | return nil 43 | } 44 | out := new(CommitSpec) 45 | in.DeepCopyInto(out) 46 | return out 47 | } 48 | 49 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 50 | func (in *CommitUser) DeepCopyInto(out *CommitUser) { 51 | *out = *in 52 | } 53 | 54 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommitUser. 55 | func (in *CommitUser) DeepCopy() *CommitUser { 56 | if in == nil { 57 | return nil 58 | } 59 | out := new(CommitUser) 60 | in.DeepCopyInto(out) 61 | return out 62 | } 63 | 64 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 65 | func (in *CrossNamespaceSourceReference) DeepCopyInto(out *CrossNamespaceSourceReference) { 66 | *out = *in 67 | } 68 | 69 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CrossNamespaceSourceReference. 70 | func (in *CrossNamespaceSourceReference) DeepCopy() *CrossNamespaceSourceReference { 71 | if in == nil { 72 | return nil 73 | } 74 | out := new(CrossNamespaceSourceReference) 75 | in.DeepCopyInto(out) 76 | return out 77 | } 78 | 79 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 80 | func (in *GitCheckoutSpec) DeepCopyInto(out *GitCheckoutSpec) { 81 | *out = *in 82 | out.Reference = in.Reference 83 | } 84 | 85 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitCheckoutSpec. 86 | func (in *GitCheckoutSpec) DeepCopy() *GitCheckoutSpec { 87 | if in == nil { 88 | return nil 89 | } 90 | out := new(GitCheckoutSpec) 91 | in.DeepCopyInto(out) 92 | return out 93 | } 94 | 95 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 96 | func (in *GitSpec) DeepCopyInto(out *GitSpec) { 97 | *out = *in 98 | if in.Checkout != nil { 99 | in, out := &in.Checkout, &out.Checkout 100 | *out = new(GitCheckoutSpec) 101 | **out = **in 102 | } 103 | in.Commit.DeepCopyInto(&out.Commit) 104 | if in.Push != nil { 105 | in, out := &in.Push, &out.Push 106 | *out = new(PushSpec) 107 | (*in).DeepCopyInto(*out) 108 | } 109 | } 110 | 111 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitSpec. 112 | func (in *GitSpec) DeepCopy() *GitSpec { 113 | if in == nil { 114 | return nil 115 | } 116 | out := new(GitSpec) 117 | in.DeepCopyInto(out) 118 | return out 119 | } 120 | 121 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 122 | func (in *ImageUpdateAutomation) DeepCopyInto(out *ImageUpdateAutomation) { 123 | *out = *in 124 | out.TypeMeta = in.TypeMeta 125 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 126 | in.Spec.DeepCopyInto(&out.Spec) 127 | in.Status.DeepCopyInto(&out.Status) 128 | } 129 | 130 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageUpdateAutomation. 131 | func (in *ImageUpdateAutomation) DeepCopy() *ImageUpdateAutomation { 132 | if in == nil { 133 | return nil 134 | } 135 | out := new(ImageUpdateAutomation) 136 | in.DeepCopyInto(out) 137 | return out 138 | } 139 | 140 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 141 | func (in *ImageUpdateAutomation) DeepCopyObject() runtime.Object { 142 | if c := in.DeepCopy(); c != nil { 143 | return c 144 | } 145 | return nil 146 | } 147 | 148 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 149 | func (in *ImageUpdateAutomationList) DeepCopyInto(out *ImageUpdateAutomationList) { 150 | *out = *in 151 | out.TypeMeta = in.TypeMeta 152 | in.ListMeta.DeepCopyInto(&out.ListMeta) 153 | if in.Items != nil { 154 | in, out := &in.Items, &out.Items 155 | *out = make([]ImageUpdateAutomation, len(*in)) 156 | for i := range *in { 157 | (*in)[i].DeepCopyInto(&(*out)[i]) 158 | } 159 | } 160 | } 161 | 162 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageUpdateAutomationList. 163 | func (in *ImageUpdateAutomationList) DeepCopy() *ImageUpdateAutomationList { 164 | if in == nil { 165 | return nil 166 | } 167 | out := new(ImageUpdateAutomationList) 168 | in.DeepCopyInto(out) 169 | return out 170 | } 171 | 172 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 173 | func (in *ImageUpdateAutomationList) DeepCopyObject() runtime.Object { 174 | if c := in.DeepCopy(); c != nil { 175 | return c 176 | } 177 | return nil 178 | } 179 | 180 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 181 | func (in *ImageUpdateAutomationSpec) DeepCopyInto(out *ImageUpdateAutomationSpec) { 182 | *out = *in 183 | out.SourceRef = in.SourceRef 184 | if in.GitSpec != nil { 185 | in, out := &in.GitSpec, &out.GitSpec 186 | *out = new(GitSpec) 187 | (*in).DeepCopyInto(*out) 188 | } 189 | out.Interval = in.Interval 190 | if in.Update != nil { 191 | in, out := &in.Update, &out.Update 192 | *out = new(UpdateStrategy) 193 | **out = **in 194 | } 195 | } 196 | 197 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageUpdateAutomationSpec. 198 | func (in *ImageUpdateAutomationSpec) DeepCopy() *ImageUpdateAutomationSpec { 199 | if in == nil { 200 | return nil 201 | } 202 | out := new(ImageUpdateAutomationSpec) 203 | in.DeepCopyInto(out) 204 | return out 205 | } 206 | 207 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 208 | func (in *ImageUpdateAutomationStatus) DeepCopyInto(out *ImageUpdateAutomationStatus) { 209 | *out = *in 210 | if in.LastAutomationRunTime != nil { 211 | in, out := &in.LastAutomationRunTime, &out.LastAutomationRunTime 212 | *out = (*in).DeepCopy() 213 | } 214 | if in.LastPushTime != nil { 215 | in, out := &in.LastPushTime, &out.LastPushTime 216 | *out = (*in).DeepCopy() 217 | } 218 | if in.Conditions != nil { 219 | in, out := &in.Conditions, &out.Conditions 220 | *out = make([]v1.Condition, len(*in)) 221 | for i := range *in { 222 | (*in)[i].DeepCopyInto(&(*out)[i]) 223 | } 224 | } 225 | out.ReconcileRequestStatus = in.ReconcileRequestStatus 226 | } 227 | 228 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageUpdateAutomationStatus. 229 | func (in *ImageUpdateAutomationStatus) DeepCopy() *ImageUpdateAutomationStatus { 230 | if in == nil { 231 | return nil 232 | } 233 | out := new(ImageUpdateAutomationStatus) 234 | in.DeepCopyInto(out) 235 | return out 236 | } 237 | 238 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 239 | func (in *PushSpec) DeepCopyInto(out *PushSpec) { 240 | *out = *in 241 | if in.Options != nil { 242 | in, out := &in.Options, &out.Options 243 | *out = make(map[string]string, len(*in)) 244 | for key, val := range *in { 245 | (*out)[key] = val 246 | } 247 | } 248 | } 249 | 250 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSpec. 251 | func (in *PushSpec) DeepCopy() *PushSpec { 252 | if in == nil { 253 | return nil 254 | } 255 | out := new(PushSpec) 256 | in.DeepCopyInto(out) 257 | return out 258 | } 259 | 260 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 261 | func (in *SigningKey) DeepCopyInto(out *SigningKey) { 262 | *out = *in 263 | out.SecretRef = in.SecretRef 264 | } 265 | 266 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SigningKey. 267 | func (in *SigningKey) DeepCopy() *SigningKey { 268 | if in == nil { 269 | return nil 270 | } 271 | out := new(SigningKey) 272 | in.DeepCopyInto(out) 273 | return out 274 | } 275 | 276 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 277 | func (in *UpdateStrategy) DeepCopyInto(out *UpdateStrategy) { 278 | *out = *in 279 | } 280 | 281 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpdateStrategy. 282 | func (in *UpdateStrategy) DeepCopy() *UpdateStrategy { 283 | if in == nil { 284 | return nil 285 | } 286 | out := new(UpdateStrategy) 287 | in.DeepCopyInto(out) 288 | return out 289 | } 290 | -------------------------------------------------------------------------------- /api/v1beta2/condition_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta2 18 | 19 | const ( 20 | // InvalidUpdateStrategyReason represents an invalid image update strategy 21 | // configuration. 22 | InvalidUpdateStrategyReason string = "InvalidUpdateStrategy" 23 | 24 | // InvalidSourceConfigReason represents an invalid source configuration. 25 | InvalidSourceConfigReason string = "InvalidSourceConfiguration" 26 | 27 | // SourceManagerFailedReason represents a failure in the SourceManager which 28 | // manages the source. 29 | SourceManagerFailedReason string = "SourceManagerFailed" 30 | 31 | // GitOperationFailedReason represents a failure in Git source operation. 32 | GitOperationFailedReason string = "GitOperationFailed" 33 | 34 | // UpdateFailedReason represents a failure during source update. 35 | UpdateFailedReason string = "UpdateFailed" 36 | 37 | // InvalidPolicySelectorReason represents an invalid policy selector. 38 | InvalidPolicySelectorReason string = "InvalidPolicySelector" 39 | ) 40 | -------------------------------------------------------------------------------- /api/v1beta2/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1beta2 contains API types for the image API group, version 18 | // v1beta2. The types here are concerned with automated updates to 19 | // git, based on metadata from OCI image registries gathered by the 20 | // image-reflector-controller. 21 | // 22 | // +kubebuilder:object:generate=true 23 | // +groupName=image.toolkit.fluxcd.io 24 | package v1beta2 25 | -------------------------------------------------------------------------------- /api/v1beta2/git.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta2 18 | 19 | import ( 20 | "github.com/fluxcd/pkg/apis/meta" 21 | sourcev1 "github.com/fluxcd/source-controller/api/v1" 22 | ) 23 | 24 | type GitSpec struct { 25 | // Checkout gives the parameters for cloning the git repository, 26 | // ready to make changes. If not present, the `spec.ref` field from the 27 | // referenced `GitRepository` or its default will be used. 28 | // +optional 29 | Checkout *GitCheckoutSpec `json:"checkout,omitempty"` 30 | 31 | // Commit specifies how to commit to the git repository. 32 | // +required 33 | Commit CommitSpec `json:"commit"` 34 | 35 | // Push specifies how and where to push commits made by the 36 | // automation. If missing, commits are pushed (back) to 37 | // `.spec.checkout.branch` or its default. 38 | // +optional 39 | Push *PushSpec `json:"push,omitempty"` 40 | } 41 | 42 | // HasRefspec returns if the GitSpec has a Refspec. 43 | func (gs GitSpec) HasRefspec() bool { 44 | if gs.Push == nil { 45 | return false 46 | } 47 | return gs.Push.Refspec != "" 48 | } 49 | 50 | type GitCheckoutSpec struct { 51 | // Reference gives a branch, tag or commit to clone from the Git 52 | // repository. 53 | // +required 54 | Reference sourcev1.GitRepositoryRef `json:"ref"` 55 | } 56 | 57 | // CommitSpec specifies how to commit changes to the git repository 58 | type CommitSpec struct { 59 | // Author gives the email and optionally the name to use as the 60 | // author of commits. 61 | // +required 62 | Author CommitUser `json:"author"` 63 | // SigningKey provides the option to sign commits with a GPG key 64 | // +optional 65 | SigningKey *SigningKey `json:"signingKey,omitempty"` 66 | // MessageTemplate provides a template for the commit message, 67 | // into which will be interpolated the details of the change made. 68 | // +optional 69 | MessageTemplate string `json:"messageTemplate,omitempty"` 70 | 71 | // MessageTemplateValues provides additional values to be available to the 72 | // templating rendering. 73 | // +optional 74 | MessageTemplateValues map[string]string `json:"messageTemplateValues,omitempty"` 75 | } 76 | 77 | type CommitUser struct { 78 | // Name gives the name to provide when making a commit. 79 | // +optional 80 | Name string `json:"name,omitempty"` 81 | // Email gives the email to provide when making a commit. 82 | // +required 83 | Email string `json:"email"` 84 | } 85 | 86 | // SigningKey references a Kubernetes secret that contains a GPG keypair 87 | type SigningKey struct { 88 | // SecretRef holds the name to a secret that contains a 'git.asc' key 89 | // corresponding to the ASCII Armored file containing the GPG signing 90 | // keypair as the value. It must be in the same namespace as the 91 | // ImageUpdateAutomation. 92 | // +required 93 | SecretRef meta.LocalObjectReference `json:"secretRef,omitempty"` 94 | } 95 | 96 | // PushSpec specifies how and where to push commits. 97 | type PushSpec struct { 98 | // Branch specifies that commits should be pushed to the branch 99 | // named. The branch is created using `.spec.checkout.branch` as the 100 | // starting point, if it doesn't already exist. 101 | // +optional 102 | Branch string `json:"branch,omitempty"` 103 | 104 | // Refspec specifies the Git Refspec to use for a push operation. 105 | // If both Branch and Refspec are provided, then the commit is pushed 106 | // to the branch and also using the specified refspec. 107 | // For more details about Git Refspecs, see: 108 | // https://git-scm.com/book/en/v2/Git-Internals-The-Refspec 109 | // +optional 110 | Refspec string `json:"refspec,omitempty"` 111 | 112 | // Options specifies the push options that are sent to the Git 113 | // server when performing a push operation. For details, see: 114 | // https://git-scm.com/docs/git-push#Documentation/git-push.txt---push-optionltoptiongt 115 | // +optional 116 | Options map[string]string `json:"options,omitempty"` 117 | } 118 | -------------------------------------------------------------------------------- /api/v1beta2/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1beta2 contains API Schema definitions for the image v1beta2 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=image.toolkit.fluxcd.io 20 | package v1beta2 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "image.toolkit.fluxcd.io", Version: "v1beta2"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /api/v1beta2/imageupdateautomation_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta2 18 | 19 | import ( 20 | "time" 21 | 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | 24 | "github.com/fluxcd/pkg/apis/meta" 25 | ) 26 | 27 | const ( 28 | ImageUpdateAutomationKind = "ImageUpdateAutomation" 29 | ImageUpdateAutomationFinalizer = "finalizers.fluxcd.io" 30 | ) 31 | 32 | // ImageUpdateAutomationSpec defines the desired state of ImageUpdateAutomation 33 | type ImageUpdateAutomationSpec struct { 34 | // SourceRef refers to the resource giving access details 35 | // to a git repository. 36 | // +required 37 | SourceRef CrossNamespaceSourceReference `json:"sourceRef"` 38 | 39 | // GitSpec contains all the git-specific definitions. This is 40 | // technically optional, but in practice mandatory until there are 41 | // other kinds of source allowed. 42 | // +optional 43 | GitSpec *GitSpec `json:"git,omitempty"` 44 | 45 | // Interval gives an lower bound for how often the automation 46 | // run should be attempted. 47 | // +kubebuilder:validation:Type=string 48 | // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$" 49 | // +required 50 | Interval metav1.Duration `json:"interval"` 51 | 52 | // PolicySelector allows to filter applied policies based on labels. 53 | // By default includes all policies in namespace. 54 | // +optional 55 | PolicySelector *metav1.LabelSelector `json:"policySelector,omitempty"` 56 | 57 | // Update gives the specification for how to update the files in 58 | // the repository. This can be left empty, to use the default 59 | // value. 60 | // +kubebuilder:default={"strategy":"Setters"} 61 | Update *UpdateStrategy `json:"update,omitempty"` 62 | 63 | // Suspend tells the controller to not run this automation, until 64 | // it is unset (or set to false). Defaults to false. 65 | // +optional 66 | Suspend bool `json:"suspend,omitempty"` 67 | } 68 | 69 | // UpdateStrategyName is the type for names that go in 70 | // .update.strategy. NB the value in the const immediately below. 71 | // +kubebuilder:validation:Enum=Setters 72 | type UpdateStrategyName string 73 | 74 | const ( 75 | // UpdateStrategySetters is the name of the update strategy that 76 | // uses kyaml setters. NB the value in the enum annotation for the 77 | // type, above. 78 | UpdateStrategySetters UpdateStrategyName = "Setters" 79 | ) 80 | 81 | // UpdateStrategy is a union of the various strategies for updating 82 | // the Git repository. Parameters for each strategy (if any) can be 83 | // inlined here. 84 | type UpdateStrategy struct { 85 | // Strategy names the strategy to be used. 86 | // +required 87 | // +kubebuilder:default=Setters 88 | Strategy UpdateStrategyName `json:"strategy"` 89 | 90 | // Path to the directory containing the manifests to be updated. 91 | // Defaults to 'None', which translates to the root path 92 | // of the GitRepositoryRef. 93 | // +optional 94 | Path string `json:"path,omitempty"` 95 | } 96 | 97 | // ImageUpdateAutomationStatus defines the observed state of ImageUpdateAutomation 98 | type ImageUpdateAutomationStatus struct { 99 | // LastAutomationRunTime records the last time the controller ran 100 | // this automation through to completion (even if no updates were 101 | // made). 102 | // +optional 103 | LastAutomationRunTime *metav1.Time `json:"lastAutomationRunTime,omitempty"` 104 | // LastPushCommit records the SHA1 of the last commit made by the 105 | // controller, for this automation object 106 | // +optional 107 | LastPushCommit string `json:"lastPushCommit,omitempty"` 108 | // LastPushTime records the time of the last pushed change. 109 | // +optional 110 | LastPushTime *metav1.Time `json:"lastPushTime,omitempty"` 111 | // +optional 112 | ObservedGeneration int64 `json:"observedGeneration,omitempty"` 113 | // +optional 114 | Conditions []metav1.Condition `json:"conditions,omitempty"` 115 | // ObservedPolicies is the list of observed ImagePolicies that were 116 | // considered by the ImageUpdateAutomation update process. 117 | // +optional 118 | ObservedPolicies ObservedPolicies `json:"observedPolicies,omitempty"` 119 | // ObservedPolicies []ObservedPolicy `json:"observedPolicies,omitempty"` 120 | // ObservedSourceRevision is the last observed source revision. This can be 121 | // used to determine if the source has been updated since last observation. 122 | // +optional 123 | ObservedSourceRevision string `json:"observedSourceRevision,omitempty"` 124 | 125 | meta.ReconcileRequestStatus `json:",inline"` 126 | } 127 | 128 | // ObservedPolicies is a map of policy name and ImageRef of their latest 129 | // ImageRef. 130 | type ObservedPolicies map[string]ImageRef 131 | 132 | //+kubebuilder:storageversion 133 | //+kubebuilder:object:root=true 134 | //+kubebuilder:subresource:status 135 | //+kubebuilder:printcolumn:name="Last run",type=string,JSONPath=`.status.lastAutomationRunTime` 136 | 137 | // ImageUpdateAutomation is the Schema for the imageupdateautomations API 138 | type ImageUpdateAutomation struct { 139 | metav1.TypeMeta `json:",inline"` 140 | metav1.ObjectMeta `json:"metadata,omitempty"` 141 | 142 | Spec ImageUpdateAutomationSpec `json:"spec,omitempty"` 143 | // +kubebuilder:default={"observedGeneration":-1} 144 | Status ImageUpdateAutomationStatus `json:"status,omitempty"` 145 | } 146 | 147 | // GetRequeueAfter returns the duration after which the ImageUpdateAutomation 148 | // must be reconciled again. 149 | func (auto ImageUpdateAutomation) GetRequeueAfter() time.Duration { 150 | return auto.Spec.Interval.Duration 151 | } 152 | 153 | // GetConditions returns the status conditions of the object. 154 | func (auto ImageUpdateAutomation) GetConditions() []metav1.Condition { 155 | return auto.Status.Conditions 156 | } 157 | 158 | // SetConditions sets the status conditions on the object. 159 | func (auto *ImageUpdateAutomation) SetConditions(conditions []metav1.Condition) { 160 | auto.Status.Conditions = conditions 161 | } 162 | 163 | //+kubebuilder:object:root=true 164 | 165 | // ImageUpdateAutomationList contains a list of ImageUpdateAutomation 166 | type ImageUpdateAutomationList struct { 167 | metav1.TypeMeta `json:",inline"` 168 | metav1.ListMeta `json:"metadata,omitempty"` 169 | Items []ImageUpdateAutomation `json:"items"` 170 | } 171 | 172 | func init() { 173 | SchemeBuilder.Register(&ImageUpdateAutomation{}, &ImageUpdateAutomationList{}) 174 | } 175 | -------------------------------------------------------------------------------- /api/v1beta2/reference.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta2 18 | 19 | import ( 20 | "fmt" 21 | ) 22 | 23 | // CrossNamespaceSourceReference contains enough information to let you locate the 24 | // typed Kubernetes resource object at cluster level. 25 | type CrossNamespaceSourceReference struct { 26 | // API version of the referent. 27 | // +optional 28 | APIVersion string `json:"apiVersion,omitempty"` 29 | 30 | // Kind of the referent. 31 | // +kubebuilder:validation:Enum=GitRepository 32 | // +kubebuilder:default=GitRepository 33 | // +required 34 | Kind string `json:"kind"` 35 | 36 | // Name of the referent. 37 | // +required 38 | Name string `json:"name"` 39 | 40 | // Namespace of the referent, defaults to the namespace of the Kubernetes resource object that contains the reference. 41 | // +optional 42 | Namespace string `json:"namespace,omitempty"` 43 | } 44 | 45 | func (s *CrossNamespaceSourceReference) String() string { 46 | if s.Namespace != "" { 47 | return fmt.Sprintf("%s/%s/%s", s.Kind, s.Namespace, s.Name) 48 | } 49 | return fmt.Sprintf("%s/%s", s.Kind, s.Name) 50 | } 51 | 52 | // ImageRef represents an image reference. 53 | type ImageRef struct { 54 | // Name is the bare image's name. 55 | // +required 56 | Name string `json:"name"` 57 | // Tag is the image's tag. 58 | // +required 59 | Tag string `json:"tag"` 60 | // Digest is the image's digest. 61 | // +optional 62 | Digest string `json:"digest,omitempty"` 63 | } 64 | 65 | func (in *ImageRef) String() string { 66 | res := in.Name + ":" + in.Tag 67 | if in.Digest != "" { 68 | res += "@" + in.Digest 69 | } 70 | return res 71 | } 72 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - bases/image.toolkit.fluxcd.io_imageupdateautomations.yaml 6 | # +kubebuilder:scaffold:crdkustomizeresource 7 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | # Adds namespace to all resources. 4 | namespace: image-automation-system 5 | 6 | # Labels to add to all resources and selectors. 7 | #commonLabels: 8 | # someName: someValue 9 | 10 | resources: 11 | - ../crd 12 | - ../rbac 13 | - ../manager 14 | - namespace.yaml 15 | -------------------------------------------------------------------------------- /config/default/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: image-automation-system 5 | labels: 6 | control-plane: controller-manager 7 | -------------------------------------------------------------------------------- /config/manager/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: image-automation-controller 5 | labels: 6 | control-plane: controller 7 | spec: 8 | selector: 9 | matchLabels: 10 | app: image-automation-controller 11 | replicas: 1 12 | template: 13 | metadata: 14 | labels: 15 | app: image-automation-controller 16 | annotations: 17 | prometheus.io/scrape: "true" 18 | prometheus.io/port: "8080" 19 | spec: 20 | terminationGracePeriodSeconds: 10 21 | # Required for AWS IAM Role bindings 22 | # https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts-technical-overview.html 23 | securityContext: 24 | fsGroup: 1337 25 | containers: 26 | - name: manager 27 | image: fluxcd/image-automation-controller 28 | imagePullPolicy: IfNotPresent 29 | securityContext: 30 | allowPrivilegeEscalation: false 31 | readOnlyRootFilesystem: true 32 | runAsNonRoot: true 33 | capabilities: 34 | drop: [ "ALL" ] 35 | seccompProfile: 36 | type: RuntimeDefault 37 | ports: 38 | - containerPort: 8080 39 | name: http-prom 40 | protocol: TCP 41 | - containerPort: 9440 42 | name: healthz 43 | protocol: TCP 44 | env: 45 | - name: RUNTIME_NAMESPACE 46 | valueFrom: 47 | fieldRef: 48 | fieldPath: metadata.namespace 49 | args: 50 | - --watch-all-namespaces 51 | - --log-level=info 52 | - --log-encoding=json 53 | - --enable-leader-election 54 | readinessProbe: 55 | httpGet: 56 | path: /readyz 57 | port: healthz 58 | livenessProbe: 59 | httpGet: 60 | path: /healthz 61 | port: healthz 62 | resources: 63 | limits: 64 | cpu: 1000m 65 | memory: 1Gi 66 | requests: 67 | cpu: 100m 68 | memory: 64Mi 69 | volumeMounts: 70 | - name: temp 71 | mountPath: /tmp 72 | volumes: 73 | - name: temp 74 | emptyDir: {} 75 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - deployment.yaml 5 | images: 6 | - name: fluxcd/image-automation-controller 7 | newName: fluxcd/image-automation-controller 8 | newTag: v0.41.0 9 | -------------------------------------------------------------------------------- /config/rbac/imageupdateautomation_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit imageupdateautomations. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: imageupdateautomation-editor-role 6 | rules: 7 | - apiGroups: 8 | - image.toolkit.fluxcd.io 9 | resources: 10 | - imageupdateautomations 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - image.toolkit.fluxcd.io 21 | resources: 22 | - imageupdateautomations/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/imageupdateautomation_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view imageupdateautomations. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: imageupdateautomation-viewer-role 6 | rules: 7 | - apiGroups: 8 | - image.toolkit.fluxcd.io 9 | resources: 10 | - imageupdateautomations 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - image.toolkit.fluxcd.io 17 | resources: 18 | - imageupdateautomations/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - role.yaml 5 | - role_binding.yaml 6 | - leader_election_role.yaml 7 | - leader_election_role_binding.yaml 8 | namePrefix: image-automation- 9 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - configmaps/status 23 | verbs: 24 | - get 25 | - update 26 | - patch 27 | - apiGroups: 28 | - "" 29 | resources: 30 | - events 31 | verbs: 32 | - create 33 | - apiGroups: 34 | - "coordination.k8s.io" 35 | resources: 36 | - leases 37 | verbs: 38 | - get 39 | - list 40 | - watch 41 | - create 42 | - update 43 | - patch 44 | - delete 45 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - events 11 | verbs: 12 | - create 13 | - patch 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - secrets 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - image.toolkit.fluxcd.io 24 | resources: 25 | - imagepolicies 26 | verbs: 27 | - get 28 | - list 29 | - watch 30 | - apiGroups: 31 | - image.toolkit.fluxcd.io 32 | resources: 33 | - imagepolicies/status 34 | verbs: 35 | - get 36 | - apiGroups: 37 | - image.toolkit.fluxcd.io 38 | resources: 39 | - imageupdateautomations 40 | verbs: 41 | - create 42 | - delete 43 | - get 44 | - list 45 | - patch 46 | - update 47 | - watch 48 | - apiGroups: 49 | - image.toolkit.fluxcd.io 50 | resources: 51 | - imageupdateautomations/status 52 | verbs: 53 | - get 54 | - patch 55 | - update 56 | - apiGroups: 57 | - source.toolkit.fluxcd.io 58 | resources: 59 | - gitrepositories 60 | verbs: 61 | - get 62 | - list 63 | - watch 64 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: manager-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: manager-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/samples/image_v1beta1_imageupdateautomation.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: image.toolkit.fluxcd.io/v1beta1 2 | kind: ImageUpdateAutomation 3 | metadata: 4 | name: imageupdateautomation-sample 5 | spec: 6 | interval: 5m 7 | sourceRef: 8 | kind: GitRepository # the only valid value, but good practice to be explicit here 9 | name: sample-repo 10 | git: 11 | checkout: 12 | ref: 13 | branch: main 14 | commit: 15 | author: 16 | name: fluxbot 17 | email: fluxbot@example.com 18 | messageTemplate: | 19 | An automated update from FluxBot 20 | 21 | [ci skip] 22 | signingKey: 23 | secretRef: 24 | name: git-pgp 25 | push: 26 | branch: auto 27 | update: 28 | strategy: Setters 29 | path: ./cluster/sample 30 | -------------------------------------------------------------------------------- /config/samples/image_v1beta2_imageupdateautomation.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: image.toolkit.fluxcd.io/v1beta2 2 | kind: ImageUpdateAutomation 3 | metadata: 4 | name: imageupdateautomation-sample 5 | spec: 6 | interval: 5m 7 | sourceRef: 8 | kind: GitRepository # the only valid value, but good practice to be explicit here 9 | name: sample-repo 10 | git: 11 | checkout: 12 | ref: 13 | branch: main 14 | commit: 15 | author: 16 | name: fluxbot 17 | email: fluxbot@example.com 18 | messageTemplate: | 19 | An automated update from FluxBot 20 | 21 | [ci skip] 22 | signingKey: 23 | secretRef: 24 | name: git-pgp 25 | push: 26 | branch: auto 27 | update: 28 | strategy: Setters 29 | path: ./cluster/sample 30 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fluxcd/image-automation-controller 2 | 3 | go 1.24.0 4 | 5 | replace github.com/fluxcd/image-automation-controller/api => ./api 6 | 7 | // Pin kustomize to v5.6.0 8 | replace ( 9 | sigs.k8s.io/kustomize/api => sigs.k8s.io/kustomize/api v0.19.0 10 | sigs.k8s.io/kustomize/kyaml => sigs.k8s.io/kustomize/kyaml v0.19.0 11 | ) 12 | 13 | require ( 14 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 15 | github.com/Masterminds/sprig/v3 v3.3.0 16 | github.com/ProtonMail/go-crypto v1.2.0 17 | github.com/cyphar/filepath-securejoin v0.4.1 18 | github.com/fluxcd/image-automation-controller/api v0.41.0 19 | github.com/fluxcd/image-reflector-controller/api v0.35.0 20 | github.com/fluxcd/pkg/apis/acl v0.7.0 21 | github.com/fluxcd/pkg/apis/event v0.17.0 22 | github.com/fluxcd/pkg/apis/meta v1.12.0 23 | github.com/fluxcd/pkg/auth v0.16.0 24 | github.com/fluxcd/pkg/cache v0.9.0 25 | github.com/fluxcd/pkg/git v0.31.0 26 | github.com/fluxcd/pkg/git/gogit v0.33.0 27 | github.com/fluxcd/pkg/gittestserver v0.17.0 28 | github.com/fluxcd/pkg/runtime v0.60.0 29 | github.com/fluxcd/pkg/ssh v0.18.0 30 | github.com/fluxcd/source-controller/api v1.6.0 31 | github.com/go-git/go-billy/v5 v5.6.2 32 | github.com/go-git/go-git/v5 v5.16.0 33 | github.com/go-logr/logr v1.4.2 34 | github.com/google/go-containerregistry v0.20.3 35 | github.com/onsi/gomega v1.37.0 36 | github.com/otiai10/copy v1.14.1 37 | github.com/spf13/pflag v1.0.6 38 | k8s.io/api v0.33.0 39 | k8s.io/apimachinery v0.33.0 40 | k8s.io/client-go v0.33.0 41 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff 42 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e 43 | sigs.k8s.io/controller-runtime v0.21.0 44 | sigs.k8s.io/kustomize/kyaml v0.19.0 45 | ) 46 | 47 | require ( 48 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 49 | dario.cat/mergo v1.0.1 // indirect 50 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect 51 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 // indirect 52 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect 53 | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect 54 | github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect 55 | github.com/MakeNowJust/heredoc v1.0.0 // indirect 56 | github.com/Masterminds/goutils v1.1.1 // indirect 57 | github.com/Masterminds/semver/v3 v3.3.0 // indirect 58 | github.com/Microsoft/go-winio v0.6.2 // indirect 59 | github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect 60 | github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect 61 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect 62 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect 63 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect 64 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect 65 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect 66 | github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3 // indirect 67 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect 68 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect 69 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect 70 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect 71 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect 72 | github.com/aws/smithy-go v1.22.2 // indirect 73 | github.com/beorn7/perks v1.0.1 // indirect 74 | github.com/blang/semver/v4 v4.0.0 // indirect 75 | github.com/bradleyfalzon/ghinstallation/v2 v2.15.0 // indirect 76 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 77 | github.com/chai2010/gettext-go v1.0.3 // indirect 78 | github.com/cloudflare/circl v1.6.1 // indirect 79 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 80 | github.com/docker/cli v27.5.0+incompatible // indirect 81 | github.com/docker/docker-credential-helpers v0.8.2 // indirect 82 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 83 | github.com/emirpasic/gods v1.18.1 // indirect 84 | github.com/evanphx/json-patch/v5 v5.9.11 // indirect 85 | github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect 86 | github.com/fluxcd/cli-utils v0.36.0-flux.13 // indirect 87 | github.com/fluxcd/gitkit v0.6.0 // indirect 88 | github.com/fluxcd/pkg/version v0.7.0 // indirect 89 | github.com/fsnotify/fsnotify v1.9.0 // indirect 90 | github.com/fxamacker/cbor/v2 v2.8.0 // indirect 91 | github.com/go-errors/errors v1.5.1 // indirect 92 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 93 | github.com/go-logr/zapr v1.3.0 // indirect 94 | github.com/go-openapi/jsonpointer v0.21.1 // indirect 95 | github.com/go-openapi/jsonreference v0.21.0 // indirect 96 | github.com/go-openapi/swag v0.23.1 // indirect 97 | github.com/gofrs/uuid v4.4.0+incompatible // indirect 98 | github.com/gogo/protobuf v1.3.2 // indirect 99 | github.com/golang-jwt/jwt/v4 v4.5.2 // indirect 100 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 101 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 102 | github.com/google/btree v1.1.3 // indirect 103 | github.com/google/gnostic-models v0.6.9 // indirect 104 | github.com/google/go-cmp v0.7.0 // indirect 105 | github.com/google/go-github/v71 v71.0.0 // indirect 106 | github.com/google/go-querystring v1.1.0 // indirect 107 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 108 | github.com/google/uuid v1.6.0 // indirect 109 | github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect 110 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect 111 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 112 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 113 | github.com/huandu/xstrings v1.5.0 // indirect 114 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 115 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 116 | github.com/josharian/intern v1.0.0 // indirect 117 | github.com/json-iterator/go v1.1.12 // indirect 118 | github.com/kevinburke/ssh_config v1.2.0 // indirect 119 | github.com/kylelemons/godebug v1.1.0 // indirect 120 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect 121 | github.com/mailru/easyjson v0.9.0 // indirect 122 | github.com/mitchellh/copystructure v1.2.0 // indirect 123 | github.com/mitchellh/go-homedir v1.1.0 // indirect 124 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 125 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 126 | github.com/moby/spdystream v0.5.0 // indirect 127 | github.com/moby/term v0.5.2 // indirect 128 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 129 | github.com/modern-go/reflect2 v1.0.2 // indirect 130 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect 131 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 132 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect 133 | github.com/opencontainers/go-digest v1.0.0 // indirect 134 | github.com/otiai10/mint v1.6.3 // indirect 135 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 136 | github.com/pjbgf/sha1cd v0.3.2 // indirect 137 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect 138 | github.com/pkg/errors v0.9.1 // indirect 139 | github.com/prometheus/client_golang v1.22.0 // indirect 140 | github.com/prometheus/client_model v0.6.2 // indirect 141 | github.com/prometheus/common v0.63.0 // indirect 142 | github.com/prometheus/procfs v0.16.1 // indirect 143 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 144 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 145 | github.com/shopspring/decimal v1.4.0 // indirect 146 | github.com/sirupsen/logrus v1.9.3 // indirect 147 | github.com/skeema/knownhosts v1.3.1 // indirect 148 | github.com/spf13/cast v1.8.0 // indirect 149 | github.com/spf13/cobra v1.9.1 // indirect 150 | github.com/x448/float16 v0.8.4 // indirect 151 | github.com/xanzy/ssh-agent v0.3.3 // indirect 152 | github.com/xlab/treeprint v1.2.0 // indirect 153 | go.uber.org/multierr v1.11.0 // indirect 154 | go.uber.org/zap v1.27.0 // indirect 155 | golang.org/x/crypto v0.38.0 // indirect 156 | golang.org/x/net v0.40.0 // indirect 157 | golang.org/x/oauth2 v0.29.0 // indirect 158 | golang.org/x/sync v0.14.0 // indirect 159 | golang.org/x/sys v0.33.0 // indirect 160 | golang.org/x/term v0.32.0 // indirect 161 | golang.org/x/text v0.25.0 // indirect 162 | golang.org/x/time v0.11.0 // indirect 163 | gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect 164 | google.golang.org/protobuf v1.36.6 // indirect 165 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 166 | gopkg.in/inf.v0 v0.9.1 // indirect 167 | gopkg.in/warnings.v0 v0.1.2 // indirect 168 | gopkg.in/yaml.v3 v3.0.1 // indirect 169 | k8s.io/apiextensions-apiserver v0.33.0 // indirect 170 | k8s.io/cli-runtime v0.33.0 // indirect 171 | k8s.io/component-base v0.33.0 // indirect 172 | k8s.io/klog/v2 v2.130.1 // indirect 173 | k8s.io/kubectl v0.33.0 // indirect 174 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 175 | sigs.k8s.io/kustomize/api v0.19.0 // indirect 176 | sigs.k8s.io/randfill v1.0.0 // indirect 177 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect 178 | sigs.k8s.io/yaml v1.4.0 // indirect 179 | ) 180 | -------------------------------------------------------------------------------- /hack/api-docs/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "hideMemberFields": [ 3 | "TypeMeta" 4 | ], 5 | "hideTypePatterns": [ 6 | "ParseError$", 7 | "List$" 8 | ], 9 | "externalPackages": [ 10 | { 11 | "typeMatchPrefix": "^k8s\\.io/apimachinery/pkg/apis/meta/v1\\.Duration$", 12 | "docsURLTemplate": "https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration" 13 | }, 14 | { 15 | "typeMatchPrefix": "^k8s\\.io/(api|apimachinery/pkg/apis)/", 16 | "docsURLTemplate": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#{{lower .TypeIdentifier}}-{{arrIndex .PackageSegments -1}}-{{arrIndex .PackageSegments -2}}" 17 | }, 18 | { 19 | "typeMatchPrefix": "^github.com/fluxcd/pkg/runtime/dependency\\.CrossNamespaceDependencyReference$", 20 | "docsURLTemplate": "https://pkg.go.dev/github.com/fluxcd/pkg/runtime/dependency#CrossNamespaceDependencyReference" 21 | }, 22 | { 23 | "typeMatchPrefix": "^github.com/fluxcd/pkg/apis/meta", 24 | "docsURLTemplate": "https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#{{ .TypeIdentifier }}" 25 | }, 26 | { 27 | "typeMatchPrefix": "^github.com/fluxcd/source-controller/api/v1", 28 | "docsURLTemplate": "https://pkg.go.dev/github.com/fluxcd/source-controller/api/v1#{{ .TypeIdentifier }}" 29 | } 30 | ], 31 | "typeDisplayNamePrefixOverrides": { 32 | "k8s.io/api/": "Kubernetes ", 33 | "k8s.io/apimachinery/pkg/apis/": "Kubernetes ", 34 | "github.com/fluxcd/pkg/runtime/": "Runtime ", 35 | "github.com/fluxcd/pkg/apis/meta/": "Meta ", 36 | "github.com/fluxcd/source-controller/api": "Source " 37 | }, 38 | "markdownDisabled": false 39 | } 40 | -------------------------------------------------------------------------------- /hack/api-docs/template/members.tpl: -------------------------------------------------------------------------------- 1 | {{ define "members" }} 2 | {{ range .Members }} 3 | {{ if not (hiddenMember .)}} 4 | 5 | 6 | {{ fieldName . }}
7 | 8 | {{ if linkForType .Type }} 9 | 10 | {{ typeDisplayName .Type }} 11 | 12 | {{ else }} 13 | {{ typeDisplayName .Type }} 14 | {{ end }} 15 | 16 | 17 | 18 | {{ if fieldEmbedded . }} 19 |

20 | (Members of {{ fieldName . }} are embedded into this type.) 21 |

22 | {{ end}} 23 | 24 | {{ if isOptionalMember .}} 25 | (Optional) 26 | {{ end }} 27 | 28 | {{ safe (renderComments .CommentLines) }} 29 | 30 | {{ if and (eq (.Type.Name.Name) "ObjectMeta") }} 31 | Refer to the Kubernetes API documentation for the fields of the 32 | metadata field. 33 | {{ end }} 34 | 35 | {{ if or (eq (fieldName .) "spec") }} 36 |
37 |
38 | 39 | {{ template "members" .Type }} 40 |
41 | {{ end }} 42 | 43 | 44 | {{ end }} 45 | {{ end }} 46 | {{ end }} 47 | -------------------------------------------------------------------------------- /hack/api-docs/template/pkg.tpl: -------------------------------------------------------------------------------- 1 | {{ define "packages" }} 2 |

Image update automation API reference 3 | {{- with (index .packages 0) -}} 4 | {{ with (index .GoPackages 0 ) -}} 5 | {{ printf " %s" .Name -}} 6 | {{ end -}} 7 | {{ end }}

8 | 9 | {{ with .packages}} 10 |

Packages:

11 | 18 | {{ end}} 19 | 20 | {{ range .packages }} 21 |

22 | {{- packageDisplayName . -}} 23 |

24 | 25 | {{ with (index .GoPackages 0 )}} 26 | {{ with .DocComments }} 27 | {{ safe (renderComments .) }} 28 | {{ end }} 29 | {{ end }} 30 | 31 | Resource Types: 32 | 33 |
    34 | {{- range (visibleTypes (sortedTypes .Types)) -}} 35 | {{ if isExportedType . -}} 36 |
  • 37 | {{ typeDisplayName . }} 38 |
  • 39 | {{- end }} 40 | {{- end -}} 41 |
42 | 43 | {{ range (visibleTypes (sortedTypes .Types))}} 44 | {{ template "type" . }} 45 | {{ end }} 46 | {{ end }} 47 | 48 |
49 |

This page was automatically generated with gen-crd-api-reference-docs

50 |
51 | {{ end }} 52 | -------------------------------------------------------------------------------- /hack/api-docs/template/type.tpl: -------------------------------------------------------------------------------- 1 | {{ define "type" }} 2 |

3 | {{- .Name.Name }} 4 | {{ if eq .Kind "Alias" }}({{.Underlying}} alias){{ end -}} 5 |

6 | 7 | {{ with (typeReferences .) }} 8 |

9 | (Appears on: 10 | {{- $prev := "" -}} 11 | {{- range . -}} 12 | {{- if $prev -}}, {{ end -}} 13 | {{ $prev = . }} 14 | {{ typeDisplayName . }} 15 | {{- end -}} 16 | ) 17 |

18 | {{ end }} 19 | 20 | {{ with .CommentLines }} 21 | {{ safe (renderComments .) }} 22 | {{ end }} 23 | 24 | {{ if .Members }} 25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {{ if isExportedType . }} 36 | 37 | 40 | 43 | 44 | 45 | 49 | 52 | 53 | {{ end }} 54 | {{ template "members" . }} 55 | 56 |
FieldDescription
38 | apiVersion
39 | string
41 | {{ apiGroup . }} 42 |
46 | kind
47 | string 48 |
50 | {{ .Name.Name }} 51 |
57 |
58 |
59 | {{ end }} 60 | {{ end }} 61 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | -------------------------------------------------------------------------------- /internal/constants/constants.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package constants 18 | 19 | const ( 20 | // SetterShortHand is a shorthand that can be used to mark 21 | // setters; instead of 22 | // # { "$ref": "#/definitions/ 23 | SetterShortHand = "$imagepolicy" 24 | ) 25 | -------------------------------------------------------------------------------- /internal/controller/predicate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controller 18 | 19 | import ( 20 | "sigs.k8s.io/controller-runtime/pkg/event" 21 | "sigs.k8s.io/controller-runtime/pkg/predicate" 22 | 23 | imagev1_reflect "github.com/fluxcd/image-reflector-controller/api/v1beta2" 24 | ) 25 | 26 | // latestImageChangePredicate implements a predicate for latest image change. 27 | // This can be used to filter events from ImagePolicies for change in the latest 28 | // image. 29 | type latestImageChangePredicate struct { 30 | predicate.Funcs 31 | } 32 | 33 | func (latestImageChangePredicate) Create(e event.CreateEvent) bool { 34 | return false 35 | } 36 | 37 | func (latestImageChangePredicate) Delete(e event.DeleteEvent) bool { 38 | return false 39 | } 40 | 41 | func (latestImageChangePredicate) Update(e event.UpdateEvent) bool { 42 | if e.ObjectOld == nil || e.ObjectNew == nil { 43 | return false 44 | } 45 | 46 | oldSource, ok := e.ObjectOld.(*imagev1_reflect.ImagePolicy) 47 | if !ok { 48 | return false 49 | } 50 | 51 | newSource, ok := e.ObjectNew.(*imagev1_reflect.ImagePolicy) 52 | if !ok { 53 | return false 54 | } 55 | 56 | if newSource.Status.LatestRef == nil { 57 | return false 58 | } 59 | 60 | if oldSource.Status.LatestRef == nil || *oldSource.Status.LatestRef != *newSource.Status.LatestRef { 61 | return true 62 | } 63 | 64 | return false 65 | } 66 | 67 | // sourceConfigChangePredicate implements a predicate for source configuration 68 | // change. This can be used to filter events from source objects for change in 69 | // source configuration. 70 | type sourceConfigChangePredicate struct { 71 | predicate.Funcs 72 | } 73 | 74 | func (sourceConfigChangePredicate) Create(e event.CreateEvent) bool { 75 | return false 76 | } 77 | 78 | func (sourceConfigChangePredicate) Delete(e event.DeleteEvent) bool { 79 | return false 80 | } 81 | 82 | func (sourceConfigChangePredicate) Update(e event.UpdateEvent) bool { 83 | if e.ObjectOld == nil || e.ObjectNew == nil { 84 | return false 85 | } 86 | 87 | return e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration() 88 | } 89 | -------------------------------------------------------------------------------- /internal/controller/predicate_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controller 18 | 19 | import ( 20 | "testing" 21 | 22 | . "github.com/onsi/gomega" 23 | "sigs.k8s.io/controller-runtime/pkg/event" 24 | 25 | imagev1_reflect "github.com/fluxcd/image-reflector-controller/api/v1beta2" 26 | sourcev1 "github.com/fluxcd/source-controller/api/v1" 27 | ) 28 | 29 | func Test_latestImageChangePredicate_Update(t *testing.T) { 30 | tests := []struct { 31 | name string 32 | beforeFunc func(oldObj, newObj *imagev1_reflect.ImagePolicy) 33 | want bool 34 | }{ 35 | { 36 | name: "no latest image", 37 | beforeFunc: func(oldObj, newObj *imagev1_reflect.ImagePolicy) { 38 | oldObj.Status.LatestRef = nil 39 | newObj.Status.LatestRef = nil 40 | }, 41 | want: false, 42 | }, 43 | { 44 | name: "new image, no old image", 45 | beforeFunc: func(oldObj, newObj *imagev1_reflect.ImagePolicy) { 46 | oldObj.Status.LatestRef = nil 47 | newObj.Status.LatestRef = &imagev1_reflect.ImageRef{Name: "foo"} 48 | }, 49 | want: true, 50 | }, 51 | { 52 | name: "different old and new image", 53 | beforeFunc: func(oldObj, newObj *imagev1_reflect.ImagePolicy) { 54 | oldObj.Status.LatestRef = &imagev1_reflect.ImageRef{Name: "bar"} 55 | newObj.Status.LatestRef = &imagev1_reflect.ImageRef{Name: "foo"} 56 | }, 57 | want: true, 58 | }, 59 | } 60 | for _, tt := range tests { 61 | t.Run(tt.name, func(t *testing.T) { 62 | g := NewWithT(t) 63 | 64 | oldObj := &imagev1_reflect.ImagePolicy{} 65 | newObj := oldObj.DeepCopy() 66 | if tt.beforeFunc != nil { 67 | tt.beforeFunc(oldObj, newObj) 68 | } 69 | e := event.UpdateEvent{ 70 | ObjectOld: oldObj, 71 | ObjectNew: newObj, 72 | } 73 | p := latestImageChangePredicate{} 74 | g.Expect(p.Update(e)).To(Equal(tt.want)) 75 | }) 76 | } 77 | } 78 | 79 | func Test_sourceConfigChangePredicate_Update(t *testing.T) { 80 | tests := []struct { 81 | name string 82 | beforeFunc func(oldObj, newObj *sourcev1.GitRepository) 83 | want bool 84 | }{ 85 | { 86 | name: "no generation change, same config", 87 | beforeFunc: func(oldObj, newObj *sourcev1.GitRepository) { 88 | oldObj.Generation = 0 89 | newObj.Generation = 0 90 | }, 91 | want: false, 92 | }, 93 | { 94 | name: "new generation, config change", 95 | beforeFunc: func(oldObj, newObj *sourcev1.GitRepository) { 96 | oldObj.Generation = 1 97 | newObj.Generation = 2 98 | }, 99 | want: true, 100 | }, 101 | } 102 | for _, tt := range tests { 103 | t.Run(tt.name, func(t *testing.T) { 104 | g := NewWithT(t) 105 | 106 | oldObj := &sourcev1.GitRepository{} 107 | newObj := oldObj.DeepCopy() 108 | if tt.beforeFunc != nil { 109 | tt.beforeFunc(oldObj, newObj) 110 | } 111 | e := event.UpdateEvent{ 112 | ObjectOld: oldObj, 113 | ObjectNew: newObj, 114 | } 115 | p := sourceConfigChangePredicate{} 116 | g.Expect(p.Update(e)).To(Equal(tt.want)) 117 | }) 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /internal/controller/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controller 18 | 19 | import ( 20 | "fmt" 21 | "math/rand" 22 | "os" 23 | "path/filepath" 24 | "testing" 25 | "time" 26 | 27 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 28 | "k8s.io/client-go/kubernetes/scheme" 29 | "k8s.io/client-go/tools/record" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | "sigs.k8s.io/controller-runtime/pkg/client" 32 | 33 | imagev1_reflect "github.com/fluxcd/image-reflector-controller/api/v1beta2" 34 | "github.com/fluxcd/pkg/runtime/controller" 35 | "github.com/fluxcd/pkg/runtime/testenv" 36 | sourcev1 "github.com/fluxcd/source-controller/api/v1" 37 | 38 | imagev1 "github.com/fluxcd/image-automation-controller/api/v1beta2" 39 | // +kubebuilder:scaffold:imports 40 | ) 41 | 42 | // These tests make use of plain Go using Gomega for assertions. 43 | // At the beginning of every (sub)test Gomega can be initialized 44 | // using gomega.NewWithT. 45 | // Refer to http://onsi.github.io/gomega/ to learn more about 46 | // Gomega. 47 | 48 | var ( 49 | k8sClient client.Client 50 | testEnv *testenv.Environment 51 | ctx = ctrl.SetupSignalHandler() 52 | ) 53 | 54 | func init() { 55 | rand.Seed(time.Now().UnixNano()) 56 | } 57 | 58 | func TestMain(m *testing.M) { 59 | utilruntime.Must(imagev1_reflect.AddToScheme(scheme.Scheme)) 60 | utilruntime.Must(sourcev1.AddToScheme(scheme.Scheme)) 61 | utilruntime.Must(imagev1.AddToScheme(scheme.Scheme)) 62 | 63 | code := runTestsWithFeatures(m, nil) 64 | if code != 0 { 65 | fmt.Println("failed with default feature values") 66 | } 67 | 68 | os.Exit(code) 69 | } 70 | 71 | func runTestsWithFeatures(m *testing.M, feats map[string]bool) int { 72 | testEnv = testenv.New( 73 | testenv.WithCRDPath( 74 | filepath.Join("..", "..", "config", "crd", "bases"), 75 | filepath.Join("testdata", "crds"), 76 | ), 77 | testenv.WithMaxConcurrentReconciles(2), 78 | ) 79 | 80 | var err error 81 | // Initialize a cacheless client for tests that need the latest objects. 82 | k8sClient, err = client.New(testEnv.Config, client.Options{Scheme: scheme.Scheme}) 83 | if err != nil { 84 | panic(fmt.Sprintf("failed to create k8s client: %v", err)) 85 | } 86 | 87 | controllerName := "image-automation-controller" 88 | if err := (&ImageUpdateAutomationReconciler{ 89 | Client: testEnv, 90 | EventRecorder: record.NewFakeRecorder(32), 91 | features: feats, 92 | ControllerName: controllerName, 93 | }).SetupWithManager(ctx, testEnv, ImageUpdateAutomationReconcilerOptions{ 94 | RateLimiter: controller.GetDefaultRateLimiter(), 95 | }); err != nil { 96 | panic(fmt.Sprintf("failed to start ImageUpdateAutomationReconciler: %v", err)) 97 | } 98 | 99 | go func() { 100 | fmt.Println("Starting the test environment") 101 | if err := testEnv.Start(ctx); err != nil { 102 | panic(fmt.Sprintf("failed to start the test environment manager: %v", err)) 103 | } 104 | }() 105 | <-testEnv.Manager.Elected() 106 | 107 | code := m.Run() 108 | 109 | fmt.Println("Stopping the test environment") 110 | if err := testEnv.Stop(); err != nil { 111 | panic(fmt.Sprintf("failed to stop the test environment: %v", err)) 112 | } 113 | 114 | return code 115 | } 116 | -------------------------------------------------------------------------------- /internal/controller/testdata/appconfig-setters-expected/deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: test 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: hello 10 | image: helloworld:1.0.1 # SETTER_SITE 11 | -------------------------------------------------------------------------------- /internal/controller/testdata/appconfig/deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: test 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: hello 10 | image: helloworld:1.0.0 # SETTER_SITE 11 | -------------------------------------------------------------------------------- /internal/controller/testdata/pathconfig/no/deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: update-no 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: hello 10 | image: helloworld:1.0.0 # SETTER_SITE 11 | -------------------------------------------------------------------------------- /internal/controller/testdata/pathconfig/yes/deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: update-yes 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: hello 10 | image: helloworld:1.0.0 # SETTER_SITE 11 | -------------------------------------------------------------------------------- /internal/features/features.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package features sets the feature gates that 18 | // image-automation-controller supports, and their default 19 | // states. 20 | package features 21 | 22 | import ( 23 | "github.com/fluxcd/pkg/auth" 24 | feathelper "github.com/fluxcd/pkg/runtime/features" 25 | ) 26 | 27 | const ( 28 | // GitForcePushBranch enables the use of "force push" when push branches 29 | // are configured. 30 | GitForcePushBranch = "GitForcePushBranch" 31 | // GitShallowClone enables the use of shallow clones when pulling source from 32 | // Git repositories. 33 | GitShallowClone = "GitShallowClone" 34 | // GitAllBranchReferences enables the download of all branch head references 35 | // when push branches are configured. When enabled fixes fluxcd/flux2#3384. 36 | GitAllBranchReferences = "GitAllBranchReferences" 37 | // CacheSecretsAndConfigMaps controls whether Secrets and ConfigMaps should 38 | // be cached. 39 | // 40 | // When enabled, it will cache both object types, resulting in increased 41 | // memory usage and cluster-wide RBAC permissions (list and watch). 42 | CacheSecretsAndConfigMaps = "CacheSecretsAndConfigMaps" 43 | ) 44 | 45 | var features = map[string]bool{ 46 | // GitForcePushBranch 47 | // opt-out from v0.27 48 | GitForcePushBranch: true, 49 | 50 | // GitShallowClone 51 | // opt-out from v0.28 52 | GitShallowClone: true, 53 | 54 | // GitAllBranchReferences 55 | // opt-out from v0.28 56 | GitAllBranchReferences: true, 57 | 58 | // CacheSecretsAndConfigMaps 59 | // opt-in from v0.29 60 | CacheSecretsAndConfigMaps: false, 61 | } 62 | 63 | func init() { 64 | auth.SetFeatureGates(features) 65 | } 66 | 67 | // FeatureGates contains a list of all supported feature gates and 68 | // their default values. 69 | func FeatureGates() map[string]bool { 70 | return features 71 | } 72 | 73 | // Enabled verifies whether the feature is enabled or not. 74 | // 75 | // This is only a wrapper around the Enabled func in 76 | // pkg/runtime/features, so callers won't need to import 77 | // both packages for checking whether a feature is enabled. 78 | func Enabled(feature string) (bool, error) { 79 | return feathelper.Enabled(feature) 80 | } 81 | -------------------------------------------------------------------------------- /internal/policy/applier.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package policy 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "fmt" 23 | 24 | securejoin "github.com/cyphar/filepath-securejoin" 25 | "github.com/fluxcd/pkg/runtime/logger" 26 | "sigs.k8s.io/controller-runtime/pkg/log" 27 | 28 | imagev1_reflect "github.com/fluxcd/image-reflector-controller/api/v1beta2" 29 | 30 | imagev1 "github.com/fluxcd/image-automation-controller/api/v1beta2" 31 | "github.com/fluxcd/image-automation-controller/pkg/update" 32 | ) 33 | 34 | var ( 35 | // ErrNoUpdateStrategy is an update error when the update strategy is not 36 | // specified. 37 | ErrNoUpdateStrategy = errors.New("no update strategy") 38 | // ErrUnsupportedUpdateStrategy is an update error when the provided update 39 | // strategy is not supported. 40 | ErrUnsupportedUpdateStrategy = errors.New("unsupported update strategy") 41 | ) 42 | 43 | // ApplyPolicies applies the given set of policies on the source present in the 44 | // workDir based on the provided ImageUpdateAutomation configuration. 45 | func ApplyPolicies(ctx context.Context, workDir string, obj *imagev1.ImageUpdateAutomation, policies []imagev1_reflect.ImagePolicy) (update.ResultV2, error) { 46 | var result update.ResultV2 47 | if obj.Spec.Update == nil { 48 | return result, ErrNoUpdateStrategy 49 | } 50 | if obj.Spec.Update.Strategy != imagev1.UpdateStrategySetters { 51 | return result, fmt.Errorf("%w: %s", ErrUnsupportedUpdateStrategy, obj.Spec.Update.Strategy) 52 | } 53 | 54 | // Resolve the path to the manifests to apply policies on. 55 | manifestPath := workDir 56 | if obj.Spec.Update.Path != "" { 57 | p, err := securejoin.SecureJoin(workDir, obj.Spec.Update.Path) 58 | if err != nil { 59 | return result, fmt.Errorf("failed to secure join manifest path: %w", err) 60 | } 61 | manifestPath = p 62 | } 63 | 64 | tracelog := log.FromContext(ctx).V(logger.TraceLevel) 65 | return update.UpdateV2WithSetters(tracelog, manifestPath, manifestPath, policies) 66 | } 67 | -------------------------------------------------------------------------------- /internal/policy/applier_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package policy 18 | 19 | import ( 20 | "context" 21 | "path/filepath" 22 | "testing" 23 | 24 | . "github.com/onsi/gomega" 25 | "github.com/otiai10/copy" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | "k8s.io/apimachinery/pkg/types" 28 | 29 | imagev1_reflect "github.com/fluxcd/image-reflector-controller/api/v1beta2" 30 | 31 | imagev1 "github.com/fluxcd/image-automation-controller/api/v1beta2" 32 | "github.com/fluxcd/image-automation-controller/internal/testutil" 33 | "github.com/fluxcd/image-automation-controller/pkg/test" 34 | "github.com/fluxcd/image-automation-controller/pkg/update" 35 | ) 36 | 37 | func testdataPath(path string) string { 38 | return filepath.Join("testdata", path) 39 | } 40 | 41 | func Test_applyPolicies(t *testing.T) { 42 | tests := []struct { 43 | name string 44 | updateStrategy *imagev1.UpdateStrategy 45 | policyLatestImages map[string]string 46 | targetPolicyName string 47 | replaceMarkerFunc func(g *WithT, path string, policyKey types.NamespacedName) 48 | inputPath string 49 | expectedPath string 50 | wantErr bool 51 | wantResult update.Result 52 | }{ 53 | { 54 | name: "valid update strategy and one policy", 55 | updateStrategy: &imagev1.UpdateStrategy{ 56 | Strategy: imagev1.UpdateStrategySetters, 57 | }, 58 | policyLatestImages: map[string]string{ 59 | "policy1": "helloworld:1.0.1", 60 | }, 61 | targetPolicyName: "policy1", 62 | inputPath: testdataPath("appconfig"), 63 | expectedPath: testdataPath("appconfig-setters-expected"), 64 | wantErr: false, 65 | }, 66 | { 67 | name: "no update strategy", 68 | updateStrategy: nil, 69 | wantErr: true, 70 | }, 71 | { 72 | name: "unknown update strategy", 73 | updateStrategy: &imagev1.UpdateStrategy{ 74 | Strategy: "foo", 75 | }, 76 | wantErr: true, 77 | }, 78 | { 79 | name: "valid update strategy and multiple policies", 80 | updateStrategy: &imagev1.UpdateStrategy{ 81 | Strategy: imagev1.UpdateStrategySetters, 82 | }, 83 | policyLatestImages: map[string]string{ 84 | "policy1": "foo:1.1.1", 85 | "policy2": "helloworld:1.0.1", 86 | "policy3": "bar:2.2.2", 87 | }, 88 | targetPolicyName: "policy2", 89 | inputPath: testdataPath("appconfig"), 90 | expectedPath: testdataPath("appconfig-setters-expected"), 91 | wantErr: false, 92 | }, 93 | { 94 | name: "valid update strategy with update path", 95 | updateStrategy: &imagev1.UpdateStrategy{ 96 | Strategy: imagev1.UpdateStrategySetters, 97 | Path: "./yes", 98 | }, 99 | policyLatestImages: map[string]string{ 100 | "policy1": "helloworld:1.0.1", 101 | }, 102 | targetPolicyName: "policy1", 103 | replaceMarkerFunc: func(g *WithT, path string, policyKey types.NamespacedName) { 104 | g.Expect(testutil.ReplaceMarker(filepath.Join(path, "yes", "deploy.yaml"), policyKey)).ToNot(HaveOccurred()) 105 | g.Expect(testutil.ReplaceMarker(filepath.Join(path, "no", "deploy.yaml"), policyKey)).ToNot(HaveOccurred()) 106 | }, 107 | inputPath: testdataPath("pathconfig"), 108 | expectedPath: testdataPath("pathconfig-expected"), 109 | wantErr: false, 110 | }, 111 | } 112 | 113 | for _, tt := range tests { 114 | t.Run(tt.name, func(t *testing.T) { 115 | g := NewWithT(t) 116 | 117 | testNS := "test-ns" 118 | workDir := t.TempDir() 119 | 120 | // Create all the policy objects. 121 | policyList := []imagev1_reflect.ImagePolicy{} 122 | for name, image := range tt.policyLatestImages { 123 | policy := &imagev1_reflect.ImagePolicy{} 124 | policy.Name = name 125 | policy.Namespace = testNS 126 | policy.Status = imagev1_reflect.ImagePolicyStatus{ 127 | LatestRef: testutil.ImageToRef(image), 128 | } 129 | policyList = append(policyList, *policy) 130 | } 131 | targetPolicyKey := types.NamespacedName{ 132 | Name: tt.targetPolicyName, Namespace: testNS, 133 | } 134 | 135 | if tt.inputPath != "" { 136 | g.Expect(copy.Copy(tt.inputPath, workDir)).ToNot(HaveOccurred()) 137 | // Update the test files with the target policy. 138 | if tt.replaceMarkerFunc != nil { 139 | tt.replaceMarkerFunc(g, workDir, targetPolicyKey) 140 | } else { 141 | g.Expect(testutil.ReplaceMarker(filepath.Join(workDir, "deploy.yaml"), targetPolicyKey)).ToNot(HaveOccurred()) 142 | } 143 | } 144 | 145 | updateAuto := &imagev1.ImageUpdateAutomation{} 146 | updateAuto.Name = "test-update" 147 | updateAuto.Namespace = testNS 148 | updateAuto.Spec = imagev1.ImageUpdateAutomationSpec{ 149 | Update: tt.updateStrategy, 150 | } 151 | 152 | scheme := runtime.NewScheme() 153 | imagev1_reflect.AddToScheme(scheme) 154 | imagev1.AddToScheme(scheme) 155 | 156 | _, err := ApplyPolicies(context.TODO(), workDir, updateAuto, policyList) 157 | g.Expect(err != nil).To(Equal(tt.wantErr)) 158 | 159 | // Check the results if there wasn't any error. 160 | if !tt.wantErr { 161 | expected := t.TempDir() 162 | copy.Copy(tt.expectedPath, expected) 163 | // Update the markers in the expected test data. 164 | if tt.replaceMarkerFunc != nil { 165 | tt.replaceMarkerFunc(g, expected, targetPolicyKey) 166 | } else { 167 | g.Expect(testutil.ReplaceMarker(filepath.Join(expected, "deploy.yaml"), targetPolicyKey)).ToNot(HaveOccurred()) 168 | } 169 | test.ExpectMatchingDirectories(g, workDir, expected) 170 | } 171 | }) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /internal/policy/testdata/appconfig-setters-expected/deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: test 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: hello 10 | image: helloworld:1.0.1 # SETTER_SITE 11 | -------------------------------------------------------------------------------- /internal/policy/testdata/appconfig/deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: test 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: hello 10 | image: helloworld:1.0.0 # SETTER_SITE 11 | -------------------------------------------------------------------------------- /internal/policy/testdata/pathconfig-expected/no/deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: update-no 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: hello 10 | image: helloworld:1.0.0 # SETTER_SITE 11 | -------------------------------------------------------------------------------- /internal/policy/testdata/pathconfig-expected/yes/deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: update-yes 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: hello 10 | image: helloworld:1.0.1 # SETTER_SITE 11 | -------------------------------------------------------------------------------- /internal/policy/testdata/pathconfig/no/deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: update-no 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: hello 10 | image: helloworld:1.0.0 # SETTER_SITE 11 | -------------------------------------------------------------------------------- /internal/policy/testdata/pathconfig/yes/deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: update-yes 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: hello 10 | image: helloworld:1.0.0 # SETTER_SITE 11 | -------------------------------------------------------------------------------- /internal/source/testdata/appconfig/deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: test 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: hello 10 | image: helloworld:1.0.0 # SETTER_SITE 11 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | 23 | flag "github.com/spf13/pflag" 24 | corev1 "k8s.io/api/core/v1" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 27 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 28 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 29 | "k8s.io/utils/pointer" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache" 32 | ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" 33 | ctrlcfg "sigs.k8s.io/controller-runtime/pkg/config" 34 | ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" 35 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 36 | 37 | imagev1_reflect "github.com/fluxcd/image-reflector-controller/api/v1beta2" 38 | "github.com/fluxcd/pkg/auth" 39 | cache "github.com/fluxcd/pkg/cache" 40 | "github.com/fluxcd/pkg/runtime/acl" 41 | "github.com/fluxcd/pkg/runtime/client" 42 | helper "github.com/fluxcd/pkg/runtime/controller" 43 | "github.com/fluxcd/pkg/runtime/events" 44 | feathelper "github.com/fluxcd/pkg/runtime/features" 45 | "github.com/fluxcd/pkg/runtime/leaderelection" 46 | "github.com/fluxcd/pkg/runtime/logger" 47 | "github.com/fluxcd/pkg/runtime/metrics" 48 | "github.com/fluxcd/pkg/runtime/pprof" 49 | "github.com/fluxcd/pkg/runtime/probes" 50 | sourcev1 "github.com/fluxcd/source-controller/api/v1" 51 | 52 | "github.com/fluxcd/pkg/git" 53 | 54 | imagev1 "github.com/fluxcd/image-automation-controller/api/v1beta2" 55 | "github.com/fluxcd/image-automation-controller/internal/features" 56 | 57 | // +kubebuilder:scaffold:imports 58 | "github.com/fluxcd/image-automation-controller/internal/controller" 59 | ) 60 | 61 | const ( 62 | controllerName = "image-automation-controller" 63 | ) 64 | 65 | var ( 66 | scheme = runtime.NewScheme() 67 | setupLog = ctrl.Log.WithName("setup") 68 | ) 69 | 70 | func init() { 71 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 72 | utilruntime.Must(imagev1_reflect.AddToScheme(scheme)) 73 | utilruntime.Must(sourcev1.AddToScheme(scheme)) 74 | utilruntime.Must(imagev1.AddToScheme(scheme)) 75 | // +kubebuilder:scaffold:scheme 76 | } 77 | 78 | func main() { 79 | const ( 80 | tokenCacheDefaultMaxSize = 100 81 | ) 82 | 83 | var ( 84 | metricsAddr string 85 | eventsAddr string 86 | healthAddr string 87 | clientOptions client.Options 88 | aclOptions acl.Options 89 | logOptions logger.Options 90 | leaderElectionOptions leaderelection.Options 91 | rateLimiterOptions helper.RateLimiterOptions 92 | featureGates feathelper.FeatureGates 93 | watchOptions helper.WatchOptions 94 | concurrent int 95 | tokenCacheOptions cache.TokenFlags 96 | ) 97 | 98 | flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") 99 | flag.StringVar(&eventsAddr, "events-addr", "", "The address of the events receiver.") 100 | flag.StringVar(&healthAddr, "health-addr", ":9440", "The address the health endpoint binds to.") 101 | flag.IntVar(&concurrent, "concurrent", 4, "The number of concurrent resource reconciles.") 102 | flag.StringSliceVar(&git.KexAlgos, "ssh-kex-algos", []string{}, 103 | "The list of key exchange algorithms to use for ssh connections, arranged from most preferred to the least.") 104 | flag.StringSliceVar(&git.HostKeyAlgos, "ssh-hostkey-algos", []string{}, 105 | "The list of hostkey algorithms to use for ssh connections, arranged from most preferred to the least.") 106 | 107 | clientOptions.BindFlags(flag.CommandLine) 108 | logOptions.BindFlags(flag.CommandLine) 109 | leaderElectionOptions.BindFlags(flag.CommandLine) 110 | aclOptions.BindFlags(flag.CommandLine) 111 | rateLimiterOptions.BindFlags(flag.CommandLine) 112 | featureGates.BindFlags(flag.CommandLine) 113 | watchOptions.BindFlags(flag.CommandLine) 114 | tokenCacheOptions.BindFlags(flag.CommandLine, tokenCacheDefaultMaxSize) 115 | 116 | flag.Parse() 117 | 118 | logger.SetLogger(logger.NewLogger(logOptions)) 119 | 120 | err := featureGates.WithLogger(setupLog). 121 | SupportedFeatures(features.FeatureGates()) 122 | if err != nil { 123 | setupLog.Error(err, "unable to load feature gates") 124 | os.Exit(1) 125 | } 126 | 127 | switch enabled, err := features.Enabled(auth.FeatureGateObjectLevelWorkloadIdentity); { 128 | case err != nil: 129 | setupLog.Error(err, "unable to check feature gate "+auth.FeatureGateObjectLevelWorkloadIdentity) 130 | os.Exit(1) 131 | case enabled: 132 | auth.EnableObjectLevelWorkloadIdentity() 133 | } 134 | 135 | watchNamespace := "" 136 | if !watchOptions.AllNamespaces { 137 | watchNamespace = os.Getenv("RUNTIME_NAMESPACE") 138 | } 139 | 140 | var disableCacheFor []ctrlclient.Object 141 | shouldCache, err := features.Enabled(features.CacheSecretsAndConfigMaps) 142 | if err != nil { 143 | setupLog.Error(err, "unable to check feature gate "+features.CacheSecretsAndConfigMaps) 144 | os.Exit(1) 145 | } 146 | if !shouldCache { 147 | disableCacheFor = append(disableCacheFor, &corev1.Secret{}, &corev1.ConfigMap{}) 148 | } 149 | 150 | restConfig := client.GetConfigOrDie(clientOptions) 151 | 152 | watchSelector, err := helper.GetWatchSelector(watchOptions) 153 | if err != nil { 154 | setupLog.Error(err, "unable to configure watch label selector for manager") 155 | os.Exit(1) 156 | } 157 | 158 | leaderElectionID := fmt.Sprintf("%s-leader-election", controllerName) 159 | if watchOptions.LabelSelector != "" { 160 | leaderElectionID = leaderelection.GenerateID(leaderElectionID, watchOptions.LabelSelector) 161 | } 162 | 163 | mgrConfig := ctrl.Options{ 164 | Scheme: scheme, 165 | HealthProbeBindAddress: healthAddr, 166 | LeaderElection: leaderElectionOptions.Enable, 167 | LeaderElectionReleaseOnCancel: leaderElectionOptions.ReleaseOnCancel, 168 | LeaseDuration: &leaderElectionOptions.LeaseDuration, 169 | RenewDeadline: &leaderElectionOptions.RenewDeadline, 170 | RetryPeriod: &leaderElectionOptions.RetryPeriod, 171 | LeaderElectionID: leaderElectionID, 172 | Client: ctrlclient.Options{ 173 | Cache: &ctrlclient.CacheOptions{ 174 | DisableFor: disableCacheFor, 175 | }, 176 | }, 177 | Cache: ctrlcache.Options{ 178 | ByObject: map[ctrlclient.Object]ctrlcache.ByObject{ 179 | &imagev1.ImageUpdateAutomation{}: {Label: watchSelector}, 180 | }, 181 | }, 182 | Metrics: metricsserver.Options{ 183 | BindAddress: metricsAddr, 184 | ExtraHandlers: pprof.GetHandlers(), 185 | }, 186 | Controller: ctrlcfg.Controller{ 187 | RecoverPanic: pointer.Bool(true), 188 | MaxConcurrentReconciles: concurrent, 189 | }, 190 | } 191 | 192 | if watchNamespace != "" { 193 | mgrConfig.Cache.DefaultNamespaces = map[string]ctrlcache.Config{ 194 | watchNamespace: ctrlcache.Config{}, 195 | } 196 | } 197 | 198 | mgr, err := ctrl.NewManager(restConfig, mgrConfig) 199 | if err != nil { 200 | setupLog.Error(err, "unable to start manager") 201 | os.Exit(1) 202 | } 203 | 204 | probes.SetupChecks(mgr, setupLog) 205 | 206 | var eventRecorder *events.Recorder 207 | if eventRecorder, err = events.NewRecorder(mgr, ctrl.Log, eventsAddr, controllerName); err != nil { 208 | setupLog.Error(err, "unable to create event recorder") 209 | os.Exit(1) 210 | } 211 | 212 | metricsH := helper.NewMetrics(mgr, metrics.MustMakeRecorder(), imagev1.ImageUpdateAutomationFinalizer) 213 | 214 | var tokenCache *cache.TokenCache 215 | if tokenCacheOptions.MaxSize > 0 { 216 | var err error 217 | tokenCache, err = cache.NewTokenCache(tokenCacheOptions.MaxSize, 218 | cache.WithMaxDuration(tokenCacheOptions.MaxDuration), 219 | cache.WithMetricsRegisterer(ctrlmetrics.Registry), 220 | cache.WithMetricsPrefix("gotk_token_")) 221 | if err != nil { 222 | setupLog.Error(err, "unable to create token cache") 223 | os.Exit(1) 224 | } 225 | } 226 | 227 | ctx := ctrl.SetupSignalHandler() 228 | 229 | if err := (&controller.ImageUpdateAutomationReconciler{ 230 | Client: mgr.GetClient(), 231 | EventRecorder: eventRecorder, 232 | Metrics: metricsH, 233 | NoCrossNamespaceRef: aclOptions.NoCrossNamespaceRefs, 234 | ControllerName: controllerName, 235 | }).SetupWithManager(ctx, mgr, controller.ImageUpdateAutomationReconcilerOptions{ 236 | RateLimiter: helper.GetRateLimiter(rateLimiterOptions), 237 | TokenCache: tokenCache, 238 | }); err != nil { 239 | setupLog.Error(err, "unable to create controller", "controller", "ImageUpdateAutomation") 240 | os.Exit(1) 241 | } 242 | // +kubebuilder:scaffold:builder 243 | 244 | setupLog.Info("starting manager") 245 | if err := mgr.Start(ctx); err != nil { 246 | setupLog.Error(err, "problem running manager") 247 | os.Exit(1) 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /pkg/test/files.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package test 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | "strings" 23 | 24 | . "github.com/onsi/gomega" 25 | ) 26 | 27 | // TODO rewrite this as just doing the diff, so I can test that it 28 | // fails at the right times too. 29 | func ExpectMatchingDirectories(g *WithT, actualRoot, expectedRoot string) { 30 | g.Expect(actualRoot).To(BeADirectory()) 31 | g.Expect(expectedRoot).To(BeADirectory()) 32 | actualonly, expectedonly, different := DiffDirectories(actualRoot, expectedRoot) 33 | g.Expect(actualonly).To(BeEmpty(), "Expect no files in %s but not in %s", actualRoot, expectedRoot) 34 | g.Expect(expectedonly).To(BeEmpty(), "Expect no files in %s but not in %s", expectedRoot, actualRoot) 35 | // these are enumerated, so that the output is the actual difference 36 | for _, diff := range different { 37 | diff.FailedExpectation(g) 38 | } 39 | } 40 | 41 | type Diff interface { 42 | Path() string 43 | FailedExpectation(g *WithT) 44 | } 45 | 46 | type contentdiff struct { 47 | path, actual, expected string 48 | } 49 | 50 | func (d contentdiff) Path() string { 51 | return d.path 52 | } 53 | 54 | // Run an expectation that will fail, giving an appropriate error 55 | func (d contentdiff) FailedExpectation(g *WithT) { 56 | g.Expect(d.actual).To(Equal(d.expected)) 57 | } 58 | 59 | type dirfile struct { 60 | abspath, path string 61 | expectedRegularFile bool 62 | } 63 | 64 | func (d dirfile) Path() string { 65 | return d.path 66 | } 67 | 68 | func (d dirfile) FailedExpectation(g *WithT) { 69 | if d.expectedRegularFile { 70 | g.Expect(d.path).To(BeARegularFile()) 71 | } else { 72 | g.Expect(d.path).To(BeADirectory()) 73 | } 74 | } 75 | 76 | // DiffDirectories walks the two given directories, recursively, and 77 | // reports relative paths for any files that are: 78 | // 79 | // (in actual but not expected, in expected but not actual, in both but different) 80 | // 81 | // It ignores dot directories (e.g., `.git/`) and Emacs backups (e.g., 82 | // `foo.yaml~`). It panics if it encounters any error apart from a 83 | // file not found. 84 | func DiffDirectories(actual, expected string) (actualonly []string, expectedonly []string, different []Diff) { 85 | seen := make(map[string]struct{}) 86 | 87 | filepath.Walk(expected, func(expectedPath string, expectedInfo os.FileInfo, err error) error { 88 | if err != nil { 89 | panic(err) 90 | } 91 | 92 | relPath := expectedPath[len(expected):] 93 | seen[relPath] = struct{}{} 94 | 95 | // ignore emacs backups 96 | if strings.HasSuffix(expectedPath, "~") { 97 | return nil 98 | } 99 | 100 | // ignore dotfiles 101 | if strings.HasPrefix(filepath.Base(expectedPath), ".") { 102 | if expectedInfo.IsDir() { 103 | return filepath.SkipDir 104 | } 105 | return nil 106 | } 107 | 108 | actualPath := filepath.Join(actual, relPath) 109 | actualInfo, err := os.Stat(actualPath) 110 | switch { 111 | case err == nil: 112 | break 113 | case os.IsNotExist(err): 114 | expectedonly = append(expectedonly, relPath) 115 | if expectedInfo.IsDir() { 116 | return filepath.SkipDir 117 | } 118 | return nil 119 | default: 120 | panic(err) 121 | } 122 | 123 | // file exists in both places 124 | switch { 125 | case actualInfo.IsDir() && expectedInfo.IsDir(): 126 | return nil // i.e., keep recursing 127 | case actualInfo.IsDir() || expectedInfo.IsDir(): 128 | different = append(different, dirfile{path: relPath, abspath: actualPath, expectedRegularFile: actualInfo.IsDir()}) 129 | if expectedInfo.IsDir() { 130 | return filepath.SkipDir 131 | } 132 | return nil 133 | } 134 | 135 | // both regular files 136 | 137 | actualBytes, err := os.ReadFile(actualPath) 138 | if err != nil { 139 | panic(err) 140 | } 141 | expectedBytes, err := os.ReadFile(expectedPath) 142 | if err != nil { 143 | panic(err) 144 | } 145 | if string(actualBytes) != string(expectedBytes) { 146 | different = append(different, contentdiff{path: relPath, actual: string(actualBytes), expected: string(expectedBytes)}) 147 | } 148 | return nil 149 | }) 150 | 151 | // every file and directory in the actual result should be expected 152 | filepath.Walk(actual, func(actualPath string, actualInfo os.FileInfo, err error) error { 153 | if err != nil { 154 | panic(err) 155 | } 156 | relPath := actualPath[len(actual):] 157 | // ignore emacs backups 158 | if strings.HasSuffix(actualPath, "~") { 159 | return nil 160 | } 161 | // skip dotdirs 162 | if actualInfo.IsDir() && strings.HasPrefix(filepath.Base(actualPath), ".") { 163 | return filepath.SkipDir 164 | } 165 | 166 | if _, ok := seen[relPath]; !ok { 167 | actualonly = append(actualonly, relPath) 168 | if actualInfo.IsDir() { 169 | return filepath.SkipDir 170 | } 171 | } 172 | return nil 173 | }) 174 | return 175 | } 176 | -------------------------------------------------------------------------------- /pkg/test/files_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package test 18 | 19 | import ( 20 | "testing" 21 | 22 | . "github.com/onsi/gomega" 23 | ) 24 | 25 | func TestExpectMatchingDirectories(t *testing.T) { 26 | tests := []struct { 27 | name string 28 | actualRoot string 29 | expectedRoot string 30 | }{ 31 | { 32 | name: "same directory", 33 | actualRoot: "testdata/base", 34 | expectedRoot: "testdata/base", 35 | }, 36 | { 37 | name: "different equivalent directories", 38 | actualRoot: "testdata/base", 39 | expectedRoot: "testdata/equiv", 40 | }, 41 | } 42 | 43 | for _, tt := range tests { 44 | t.Run(tt.name, func(t *testing.T) { 45 | g := NewWithT(t) 46 | ExpectMatchingDirectories(g, tt.actualRoot, tt.expectedRoot) 47 | }) 48 | } 49 | } 50 | 51 | func TestDiffDirectories(t *testing.T) { 52 | g := NewWithT(t) 53 | 54 | // Finds files in actual a/ that weren't expected from b/. 55 | actualonly, _, _ := DiffDirectories("testdata/diff/a", "testdata/diff/b") 56 | g.Expect(actualonly).To(Equal([]string{"/only", "/onlyhere.yaml"})) 57 | 58 | // Finds files in expected from a/ but not in actual b/. 59 | _, expectedonly, _ := DiffDirectories("testdata/diff/b", "testdata/diff/a") // NB change in order 60 | g.Expect(expectedonly).To(Equal([]string{"/only", "/onlyhere.yaml"})) 61 | 62 | // Finds files that are different in a and b. 63 | _, _, diffs := DiffDirectories("testdata/diff/a", "testdata/diff/b") 64 | var diffpaths []string 65 | for _, d := range diffs { 66 | diffpaths = append(diffpaths, d.Path()) 67 | } 68 | g.Expect(diffpaths).To(Equal([]string{"/different/content.yaml", "/dirfile"})) 69 | } 70 | -------------------------------------------------------------------------------- /pkg/test/testdata/base/foo.yaml: -------------------------------------------------------------------------------- 1 | foo: 1 2 | -------------------------------------------------------------------------------- /pkg/test/testdata/diff/a/different/content.yaml: -------------------------------------------------------------------------------- 1 | # This file has different content here than in the corresponding file 2 | # in b/. 3 | -------------------------------------------------------------------------------- /pkg/test/testdata/diff/a/dirfile: -------------------------------------------------------------------------------- 1 | # This is a file here, and a directory in the compared root 2 | -------------------------------------------------------------------------------- /pkg/test/testdata/diff/a/only/here.yaml: -------------------------------------------------------------------------------- 1 | # this file exists for the purpose of diffing directories 2 | -------------------------------------------------------------------------------- /pkg/test/testdata/diff/a/onlyhere.yaml: -------------------------------------------------------------------------------- 1 | # this file exists for the purpose of diffing directories 2 | -------------------------------------------------------------------------------- /pkg/test/testdata/diff/b/different/content.yaml: -------------------------------------------------------------------------------- 1 | # This file has different content here to the corresponding file in 2 | # a/. 3 | -------------------------------------------------------------------------------- /pkg/test/testdata/diff/b/dirfile/.ignore: -------------------------------------------------------------------------------- 1 | Just here to preserve the directory. 2 | -------------------------------------------------------------------------------- /pkg/test/testdata/equiv/foo.yaml: -------------------------------------------------------------------------------- 1 | foo: 1 2 | -------------------------------------------------------------------------------- /pkg/update/filereader.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020, 2021 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package update 18 | 19 | import ( 20 | "bytes" 21 | "fmt" 22 | "os" 23 | "path/filepath" 24 | 25 | "github.com/go-logr/logr" 26 | "sigs.k8s.io/kustomize/kyaml/kio" 27 | "sigs.k8s.io/kustomize/kyaml/kio/kioutil" 28 | "sigs.k8s.io/kustomize/kyaml/yaml" 29 | ) 30 | 31 | // ScreeningReader is a kio.Reader that includes only files that are 32 | // pertinent to automation. In practice this means looking for a 33 | // particular token in each file, and ignoring those files without the 34 | // token. This avoids most problematic cases -- e.g., templates in a 35 | // Helm chart, which won't parse as YAML -- and cheaply filters for 36 | // only those files that need processing. 37 | type ScreeningLocalReader struct { 38 | Token string 39 | Path string 40 | 41 | Trace logr.Logger 42 | 43 | // This records the relative path of each file that passed 44 | // screening (i.e., contained the token), but couldn't be parsed. 45 | ProblemFiles []string 46 | } 47 | 48 | // Read scans the .Path recursively for files that contain .Token, and 49 | // parses any that do. It applies the filename annotation used by 50 | // [`kio.LocalPackageWriter`](https://godoc.org/sigs.k8s.io/kustomize/kyaml/kio#LocalPackageWriter) 51 | // so that the same will write files back to their original 52 | // location. The implementation follows that of 53 | // [LocalPackageReader.Read](https://godoc.org/sigs.k8s.io/kustomize/kyaml/kio#LocalPackageReader.Read), 54 | // adapting lightly (mainly to leave features out). 55 | func (r *ScreeningLocalReader) Read() ([]*yaml.RNode, error) { 56 | tracelog := r.Trace 57 | if (logr.Logger{} == tracelog) { 58 | tracelog = logr.Discard() 59 | } 60 | tracelog.Info("scanning files", "path", r.Path, "token", r.Token) 61 | 62 | if r.Path == "" { 63 | return nil, fmt.Errorf("must supply path to scan for files") 64 | } 65 | 66 | root, err := filepath.Abs(r.Path) 67 | if err != nil { 68 | return nil, fmt.Errorf("path field cannot be made absolute: %w", err) 69 | } 70 | 71 | // For the filename annotation, I want a directory for filenames 72 | // to be relative to; but I don't know whether path is a directory 73 | // or file yet so this must wait until the body of the filepath.Walk. 74 | var relativePath string 75 | 76 | tokenbytes := []byte(r.Token) 77 | 78 | var result []*yaml.RNode 79 | err = filepath.Walk(root, func(p string, info os.FileInfo, err error) error { 80 | if err != nil { 81 | return fmt.Errorf("walking path for files: %w", err) 82 | } 83 | 84 | if p == root { 85 | if info.IsDir() { 86 | relativePath = p 87 | return nil // keep walking 88 | } 89 | relativePath = filepath.Dir(p) 90 | } 91 | 92 | if info.IsDir() { 93 | return nil 94 | } 95 | 96 | if ext := filepath.Ext(p); ext != ".yaml" && ext != ".yml" && filepath.Base(p) != "Kustomization" { 97 | return nil 98 | } 99 | 100 | // To check for the token, I need the file contents. This 101 | // assumes the file is encoded as UTF8. 102 | filebytes, err := os.ReadFile(p) 103 | if err != nil { 104 | return fmt.Errorf("reading YAML file: %w", err) 105 | } 106 | 107 | if !bytes.Contains(filebytes, tokenbytes) { 108 | return nil 109 | } 110 | 111 | path, err := filepath.Rel(relativePath, p) 112 | if err != nil { 113 | return fmt.Errorf("relativising path: %w", err) 114 | } 115 | annotations := map[string]string{ 116 | kioutil.PathAnnotation: path, 117 | } 118 | 119 | tracelog.Info("reading file", "path", path) 120 | rdr := &kio.ByteReader{ 121 | Reader: bytes.NewBuffer(filebytes), 122 | SetAnnotations: annotations, 123 | PreserveSeqIndent: true, 124 | } 125 | 126 | nodes, err := rdr.Read() 127 | // Having screened the file and decided it's worth examining, 128 | // an error at this point is most unfortunate. However, it 129 | // doesn't need to be the end of the matter; we can record 130 | // this file as problematic, and continue. 131 | if err != nil { 132 | tracelog.Info("problem file", "path", path) 133 | r.ProblemFiles = append(r.ProblemFiles, path) 134 | return nil 135 | } 136 | result = append(result, nodes...) 137 | return nil 138 | }) 139 | 140 | return result, err 141 | } 142 | -------------------------------------------------------------------------------- /pkg/update/filereader_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020, 2021 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package update 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/go-logr/logr" 23 | . "github.com/onsi/gomega" 24 | "sigs.k8s.io/kustomize/kyaml/kio/kioutil" 25 | ) 26 | 27 | func TestScreeningLocalReader(t *testing.T) { 28 | g := NewWithT(t) 29 | r := ScreeningLocalReader{ 30 | Path: "testdata/setters/original", 31 | Token: "$imagepolicy", 32 | Trace: logr.Discard(), 33 | } 34 | nodes, err := r.Read() 35 | g.Expect(err).ToNot(HaveOccurred()) 36 | // the test fixture has four files that contain the marker: 37 | // - marked.yaml 38 | // - otherns.yaml 39 | // - kustomization.yml 40 | // - Kustomization 41 | g.Expect(len(nodes)).To(Equal(4)) 42 | filesSeen := map[string]struct{}{} 43 | for i := range nodes { 44 | path, _, err := kioutil.GetFileAnnotations(nodes[i]) 45 | g.Expect(err).ToNot(HaveOccurred()) 46 | filesSeen[path] = struct{}{} 47 | } 48 | g.Expect(filesSeen).To(Equal(map[string]struct{}{ 49 | "marked.yaml": {}, 50 | "otherns.yaml": {}, 51 | "kustomization.yml": {}, 52 | "Kustomization": {}, 53 | })) 54 | 55 | } 56 | -------------------------------------------------------------------------------- /pkg/update/filter.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020, 2021 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package update 18 | 19 | import ( 20 | "encoding/json" 21 | 22 | "github.com/go-logr/logr" 23 | "k8s.io/kube-openapi/pkg/validation/spec" 24 | "sigs.k8s.io/kustomize/kyaml/fieldmeta" 25 | "sigs.k8s.io/kustomize/kyaml/openapi" 26 | "sigs.k8s.io/kustomize/kyaml/yaml" 27 | ) 28 | 29 | // The implementation of this filter is adapted from 30 | // [kyaml](https://github.com/kubernetes-sigs/kustomize/blob/kyaml/v0.10.16/kyaml/setters2/set.go), 31 | // with the following changes: 32 | // 33 | // - it calls its callback for each field it sets 34 | // 35 | // - it will set all fields referring to a setter present in the 36 | // schema -- this is behind a flag in the kyaml implementation, but 37 | // the only desired mode of operation here 38 | // 39 | // - substitutions are not supported -- they are not used for image 40 | // updates 41 | // 42 | // - no validation is done on the value being set -- since the schema 43 | // is constructed here, it's assumed the values will be appropriate 44 | // 45 | // - only scalar nodes are considered (i.e., no sequence replacements) 46 | // 47 | // - only per-field schema references (those in a comment in the YAML) 48 | // are considered -- these are the only ones relevant to image updates 49 | 50 | type SetAllCallback struct { 51 | SettersSchema *spec.Schema 52 | Callback func(setter, oldValue, newValue string) 53 | Trace logr.Logger 54 | } 55 | 56 | func (s *SetAllCallback) TraceOrDiscard() logr.Logger { 57 | if (logr.Logger{} == s.Trace) { 58 | return logr.Discard() 59 | } 60 | return s.Trace 61 | } 62 | 63 | func (s *SetAllCallback) Filter(object *yaml.RNode) (*yaml.RNode, error) { 64 | return object, accept(s, object, "", s.SettersSchema) 65 | } 66 | 67 | // visitor is provided to accept to walk the AST. 68 | type visitor interface { 69 | // visitScalar is called for each scalar field value on a resource 70 | // node is the scalar field value 71 | // path is the path to the field; path elements are separated by '.' 72 | visitScalar(node *yaml.RNode, path string, schema *openapi.ResourceSchema) error 73 | } 74 | 75 | // getSchema returns per-field OpenAPI schema for a particular node. 76 | func getSchema(r *yaml.RNode, settersSchema *spec.Schema) *openapi.ResourceSchema { 77 | // get the override schema if it exists on the field 78 | fm := fieldmeta.FieldMeta{SettersSchema: settersSchema} 79 | if err := fm.Read(r); err == nil && !fm.IsEmpty() { 80 | // per-field schema, this is fine 81 | if fm.Schema.Ref.String() != "" { 82 | // resolve the reference 83 | s, err := openapi.Resolve(&fm.Schema.Ref, settersSchema) 84 | if err == nil && s != nil { 85 | fm.Schema = *s 86 | } 87 | } 88 | return &openapi.ResourceSchema{Schema: &fm.Schema} 89 | } 90 | return nil 91 | } 92 | 93 | // accept walks the AST and calls the visitor at each scalar node. 94 | func accept(v visitor, object *yaml.RNode, p string, settersSchema *spec.Schema) error { 95 | switch object.YNode().Kind { 96 | case yaml.DocumentNode: 97 | // Traverse the child of the document 98 | return accept(v, yaml.NewRNode(object.YNode()), p, settersSchema) 99 | case yaml.MappingNode: 100 | return object.VisitFields(func(node *yaml.MapNode) error { 101 | // Traverse each field value 102 | return accept(v, node.Value, p+"."+node.Key.YNode().Value, settersSchema) 103 | }) 104 | case yaml.SequenceNode: 105 | return object.VisitElements(func(node *yaml.RNode) error { 106 | // Traverse each list element 107 | return accept(v, node, p, settersSchema) 108 | }) 109 | case yaml.ScalarNode: 110 | fieldSchema := getSchema(object, settersSchema) 111 | return v.visitScalar(object, p, fieldSchema) 112 | } 113 | return nil 114 | } 115 | 116 | type setter struct { 117 | Name string `json:"name"` 118 | Value string `json:"value"` 119 | } 120 | 121 | type extension struct { 122 | Setter *setter `json:"setter,omitempty"` 123 | } 124 | 125 | // set applies the value from ext to field 126 | func (s *SetAllCallback) set(field *yaml.RNode, ext *extension, sch *spec.Schema) (bool, error) { 127 | // check full setter 128 | if ext.Setter == nil { 129 | return false, nil 130 | } 131 | 132 | // this has a full setter, set its value 133 | old := field.YNode().Value 134 | field.YNode().Value = ext.Setter.Value 135 | s.TraceOrDiscard().Info("applying setter", "setter", ext.Setter.Name, "old", old, "new", ext.Setter.Value) 136 | s.Callback(ext.Setter.Name, old, ext.Setter.Value) 137 | 138 | // format the node so it is quoted if it is a string. If there is 139 | // type information on the setter schema, we use it. 140 | if len(sch.Type) > 0 { 141 | yaml.FormatNonStringStyle(field.YNode(), *sch) 142 | } 143 | return true, nil 144 | } 145 | 146 | // visitScalar 147 | func (s *SetAllCallback) visitScalar(object *yaml.RNode, p string, fieldSchema *openapi.ResourceSchema) error { 148 | if fieldSchema == nil { 149 | return nil 150 | } 151 | // get the openAPI for this field describing how to apply the setter 152 | ext, err := getExtFromSchema(fieldSchema.Schema) 153 | if err != nil { 154 | return err 155 | } 156 | if ext == nil { 157 | return nil 158 | } 159 | 160 | s.TraceOrDiscard().Info("found schema extension", "path", p) 161 | // perform a direct set of the field if it matches 162 | _, err = s.set(object, ext, fieldSchema.Schema) 163 | return err 164 | } 165 | 166 | func getExtFromSchema(schema *spec.Schema) (*extension, error) { 167 | cep := schema.VendorExtensible.Extensions[K8sCliExtensionKey] 168 | if cep == nil { 169 | return nil, nil 170 | } 171 | b, err := json.Marshal(cep) 172 | if err != nil { 173 | return nil, err 174 | } 175 | val := &extension{} 176 | if err := json.Unmarshal(b, val); err != nil { 177 | return nil, err 178 | } 179 | return val, nil 180 | } 181 | -------------------------------------------------------------------------------- /pkg/update/filter_test.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-logr/logr" 7 | . "github.com/onsi/gomega" 8 | "k8s.io/kube-openapi/pkg/validation/spec" 9 | "sigs.k8s.io/kustomize/kyaml/yaml" 10 | ) 11 | 12 | func TestSetAllCallbackAccept(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | object *yaml.RNode 16 | settersSchema *spec.Schema 17 | expectedError bool 18 | }{ 19 | { 20 | name: "Accept - Scalar Node", 21 | object: yaml.NewRNode(&yaml.Node{ 22 | Kind: yaml.ScalarNode, 23 | Value: "test", 24 | }), 25 | settersSchema: &spec.Schema{}, 26 | expectedError: false, 27 | }, 28 | { 29 | name: "Accept - Scalar Node - Error", 30 | object: yaml.NewRNode(&yaml.Node{ 31 | Kind: yaml.ScalarNode, 32 | Value: "test", 33 | }), 34 | settersSchema: nil, 35 | expectedError: false, 36 | }, 37 | } 38 | 39 | for _, test := range tests { 40 | t.Run(test.name, func(t *testing.T) { 41 | callbackInstance := SetAllCallback{ 42 | SettersSchema: test.settersSchema, 43 | Trace: logr.Discard(), 44 | } 45 | 46 | err := accept(&callbackInstance, test.object, "", test.settersSchema) 47 | g := NewWithT(t) 48 | if test.expectedError { 49 | g.Expect(err).To(HaveOccurred()) 50 | } else { 51 | g.Expect(err).ToNot(HaveOccurred()) 52 | } 53 | }) 54 | } 55 | } 56 | 57 | func TestGetExtFromSchema(t *testing.T) { 58 | tests := []struct { 59 | name string 60 | schema *spec.Schema 61 | expectedExtension *extension 62 | expectedError bool 63 | }{ 64 | { 65 | name: "Extension Present", 66 | schema: &spec.Schema{ 67 | VendorExtensible: spec.VendorExtensible{ 68 | Extensions: map[string]interface{}{ 69 | K8sCliExtensionKey: &extension{ 70 | Setter: &setter{ 71 | Name: "testSetter", 72 | Value: "testValue", 73 | }, 74 | }, 75 | }, 76 | }, 77 | }, 78 | expectedExtension: &extension{ 79 | Setter: &setter{ 80 | Name: "testSetter", 81 | Value: "testValue", 82 | }, 83 | }, 84 | expectedError: false, 85 | }, 86 | { 87 | name: "Extension Not Present", 88 | schema: &spec.Schema{}, 89 | expectedError: false, 90 | }, 91 | } 92 | 93 | for _, test := range tests { 94 | t.Run(test.name, func(t *testing.T) { 95 | g := NewWithT(t) 96 | ext, err := getExtFromSchema(test.schema) 97 | 98 | if test.expectedError { 99 | g.Expect(err).To(HaveOccurred()) 100 | } else { 101 | g.Expect(err).ToNot(HaveOccurred()) 102 | g.Expect(ext).To(Equal(test.expectedExtension)) 103 | } 104 | }) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /pkg/update/result.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import ( 4 | "github.com/google/go-containerregistry/pkg/name" 5 | "k8s.io/apimachinery/pkg/types" 6 | "sigs.k8s.io/kustomize/kyaml/yaml" 7 | ) 8 | 9 | // ImageRef represents the image reference used to replace a field 10 | // value in an update. 11 | type ImageRef interface { 12 | // String returns a string representation of the image ref as it 13 | // is used in the update; e.g., "helloworld:v1.0.1" 14 | String() string 15 | // Identifier returns the tag or digest; e.g., "v1.0.1" 16 | Identifier() string 17 | // Repository returns the repository component of the ImageRef, 18 | // with an implied defaults, e.g., "library/helloworld" 19 | Repository() string 20 | // Registry returns the registry component of the ImageRef, e.g., 21 | // "index.docker.io" 22 | Registry() string 23 | // Name gives the fully-qualified reference name, e.g., 24 | // "index.docker.io/library/helloworld:v1.0.1" 25 | Name() string 26 | // Policy gives the namespaced name of the image policy that led 27 | // to the update. 28 | Policy() types.NamespacedName 29 | } 30 | 31 | type imageRef struct { 32 | name.Reference 33 | policy types.NamespacedName 34 | } 35 | 36 | // Policy gives the namespaced name of the policy that led to the 37 | // update. 38 | func (i imageRef) Policy() types.NamespacedName { 39 | return i.policy 40 | } 41 | 42 | // Repository gives the repository component of the image ref. 43 | func (i imageRef) Repository() string { 44 | return i.Context().RepositoryStr() 45 | } 46 | 47 | // Registry gives the registry component of the image ref. 48 | func (i imageRef) Registry() string { 49 | return i.Context().Registry.String() 50 | } 51 | 52 | // ObjectIdentifier holds the identifying data for a particular 53 | // object. This won't always have a name (e.g., a kustomization.yaml). 54 | type ObjectIdentifier struct { 55 | yaml.ResourceIdentifier 56 | } 57 | 58 | // Result reports the outcome of an automated update. It has a nested 59 | // structure file->objects->images. Different projections (e.g., all 60 | // the images, regardless of object) are available via methods. 61 | type Result struct { 62 | Files map[string]FileResult 63 | } 64 | 65 | // FileResult gives the updates in a particular file. 66 | type FileResult struct { 67 | Objects map[ObjectIdentifier][]ImageRef 68 | } 69 | 70 | // Images returns all the images that were involved in at least one 71 | // update. 72 | func (r Result) Images() []ImageRef { 73 | seen := make(map[ImageRef]struct{}) 74 | var result []ImageRef 75 | for _, file := range r.Files { 76 | for _, images := range file.Objects { 77 | for _, ref := range images { 78 | if _, ok := seen[ref]; !ok { 79 | seen[ref] = struct{}{} 80 | result = append(result, ref) 81 | } 82 | } 83 | } 84 | } 85 | return result 86 | } 87 | 88 | // Objects returns a map of all the objects against the images updated 89 | // within, regardless of which file they appear in. 90 | func (r Result) Objects() map[ObjectIdentifier][]ImageRef { 91 | result := make(map[ObjectIdentifier][]ImageRef) 92 | for _, file := range r.Files { 93 | for res, refs := range file.Objects { 94 | result[res] = append(result[res], refs...) 95 | } 96 | } 97 | return result 98 | } 99 | 100 | // ResultV2 contains Result of update and also the file changes made during the 101 | // update. This extends the Result to include details about the exact changes 102 | // made to the files and the objects in them. It has a nested structure 103 | // file->objects->changes. 104 | type ResultV2 struct { 105 | ImageResult Result 106 | FileChanges map[string]ObjectChanges 107 | } 108 | 109 | // ObjectChanges contains all the changes made to objects. 110 | type ObjectChanges map[ObjectIdentifier][]Change 111 | 112 | // Change contains the setter that resulted in a Change, the old and the new 113 | // value after the Change. 114 | type Change struct { 115 | OldValue string 116 | NewValue string 117 | Setter string 118 | } 119 | 120 | // AddChange adds changes to Resultv2 for a given file, object and changes 121 | // associated with it. 122 | func (r *ResultV2) AddChange(file string, objectID ObjectIdentifier, changes ...Change) { 123 | if r.FileChanges == nil { 124 | r.FileChanges = map[string]ObjectChanges{} 125 | } 126 | // Create an entry for the file if not present. 127 | _, ok := r.FileChanges[file] 128 | if !ok { 129 | r.FileChanges[file] = ObjectChanges{} 130 | } 131 | // Append to the changes for the object. 132 | r.FileChanges[file][objectID] = append(r.FileChanges[file][objectID], changes...) 133 | } 134 | 135 | // Changes returns all the changes that were made in at least one update. 136 | func (r ResultV2) Changes() []Change { 137 | seen := make(map[Change]struct{}) 138 | var result []Change 139 | for _, objChanges := range r.FileChanges { 140 | for _, changes := range objChanges { 141 | for _, change := range changes { 142 | if _, ok := seen[change]; !ok { 143 | seen[change] = struct{}{} 144 | result = append(result, change) 145 | } 146 | } 147 | } 148 | } 149 | return result 150 | } 151 | 152 | // Objects returns ObjectChanges, regardless of which file they appear in. 153 | func (r ResultV2) Objects() ObjectChanges { 154 | result := make(ObjectChanges) 155 | for _, objChanges := range r.FileChanges { 156 | for obj, change := range objChanges { 157 | result[obj] = change 158 | } 159 | } 160 | return result 161 | } 162 | -------------------------------------------------------------------------------- /pkg/update/result_test.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-containerregistry/pkg/name" 7 | . "github.com/onsi/gomega" 8 | "k8s.io/apimachinery/pkg/types" 9 | "sigs.k8s.io/kustomize/kyaml/yaml" 10 | ) 11 | 12 | // mustRef creates an imageRef for use in tests. It panics if the ref 13 | // given is invalid. 14 | func mustRef(ref string) imageRef { 15 | r, err := name.ParseReference(ref) 16 | if err != nil { 17 | panic(err) 18 | } 19 | return imageRef{r, types.NamespacedName{}} 20 | } 21 | 22 | func TestMustRef(t *testing.T) { 23 | g := NewWithT(t) 24 | 25 | t.Run("gives each component of an image ref", func(t *testing.T) { 26 | ref := mustRef("helloworld:v1.0.1") 27 | g.Expect(ref.String()).To(Equal("helloworld:v1.0.1")) 28 | g.Expect(ref.Identifier()).To(Equal("v1.0.1")) 29 | g.Expect(ref.Repository()).To(Equal("library/helloworld")) 30 | g.Expect(ref.Registry()).To(Equal("index.docker.io")) 31 | g.Expect(ref.Name()).To(Equal("index.docker.io/library/helloworld:v1.0.1")) 32 | }) 33 | 34 | t.Run("deals with hostnames and digests", func(t *testing.T) { 35 | image := "localhost:5000/org/helloworld@sha256:6745aaad46d795c9836632e1fb62f24b7e7f4c843144da8e47a5465c411a14be" 36 | ref := mustRef(image) 37 | g.Expect(ref.String()).To(Equal(image)) 38 | g.Expect(ref.Identifier()).To(Equal("sha256:6745aaad46d795c9836632e1fb62f24b7e7f4c843144da8e47a5465c411a14be")) 39 | g.Expect(ref.Repository()).To(Equal("org/helloworld")) 40 | g.Expect(ref.Registry()).To(Equal("localhost:5000")) 41 | g.Expect(ref.Name()).To(Equal(image)) 42 | }) 43 | } 44 | 45 | func TestUpdateResults(t *testing.T) { 46 | g := NewWithT(t) 47 | 48 | var result Result 49 | objectNames := []ObjectIdentifier{ 50 | {yaml.ResourceIdentifier{ 51 | NameMeta: yaml.NameMeta{Namespace: "ns", Name: "foo"}, 52 | }}, 53 | {yaml.ResourceIdentifier{ 54 | NameMeta: yaml.NameMeta{Namespace: "ns", Name: "bar"}, 55 | }}, 56 | } 57 | 58 | result = Result{ 59 | Files: map[string]FileResult{ 60 | "foo.yaml": { 61 | Objects: map[ObjectIdentifier][]ImageRef{ 62 | objectNames[0]: { 63 | mustRef("image:v1.0"), 64 | mustRef("other:v2.0"), 65 | }, 66 | }, 67 | }, 68 | "bar.yaml": { 69 | Objects: map[ObjectIdentifier][]ImageRef{ 70 | objectNames[1]: { 71 | mustRef("image:v1.0"), 72 | mustRef("other:v2.0"), 73 | }, 74 | }, 75 | }, 76 | }, 77 | } 78 | 79 | g.Expect(result.Images()).To(Equal([]ImageRef{ 80 | mustRef("image:v1.0"), 81 | mustRef("other:v2.0"), 82 | })) 83 | 84 | g.Expect(result.Objects()).To(Equal(map[ObjectIdentifier][]ImageRef{ 85 | objectNames[0]: { 86 | mustRef("image:v1.0"), 87 | mustRef("other:v2.0"), 88 | }, 89 | objectNames[1]: { 90 | mustRef("image:v1.0"), 91 | mustRef("other:v2.0"), 92 | }, 93 | })) 94 | } 95 | 96 | func TestResultV2(t *testing.T) { 97 | g := NewWithT(t) 98 | 99 | var result ResultV2 100 | objectNames := []ObjectIdentifier{ 101 | {yaml.ResourceIdentifier{ 102 | NameMeta: yaml.NameMeta{Namespace: "ns", Name: "foo"}, 103 | }}, 104 | {yaml.ResourceIdentifier{ 105 | NameMeta: yaml.NameMeta{Namespace: "ns", Name: "bar"}, 106 | }}, 107 | } 108 | 109 | result.AddChange("foo.yaml", objectNames[0], Change{ 110 | OldValue: "aaa", 111 | NewValue: "bbb", 112 | Setter: "foo-ns:policy:name", 113 | }) 114 | result.AddChange("bar.yaml", objectNames[1], Change{ 115 | OldValue: "cccc:v1.0", 116 | NewValue: "cccc:v1.2", 117 | Setter: "foo-ns:policy", 118 | }) 119 | 120 | result = ResultV2{ 121 | FileChanges: map[string]ObjectChanges{ 122 | "foo.yaml": { 123 | objectNames[0]: []Change{ 124 | { 125 | OldValue: "aaa", 126 | NewValue: "bbb", 127 | Setter: "foo-ns:policy:name", 128 | }, 129 | }, 130 | }, 131 | "bar.yaml": { 132 | objectNames[1]: []Change{ 133 | { 134 | OldValue: "cccc:v1.0", 135 | NewValue: "cccc:v1.2", 136 | Setter: "foo-ns:policy", 137 | }, 138 | }, 139 | }, 140 | }, 141 | } 142 | 143 | g.Expect(result.Changes()).To(ContainElements([]Change{ 144 | { 145 | OldValue: "aaa", 146 | NewValue: "bbb", 147 | Setter: "foo-ns:policy:name", 148 | }, 149 | { 150 | OldValue: "cccc:v1.0", 151 | NewValue: "cccc:v1.2", 152 | Setter: "foo-ns:policy", 153 | }, 154 | })) 155 | g.Expect(result.Objects()).To(Equal(ObjectChanges{ 156 | objectNames[0]: []Change{ 157 | { 158 | OldValue: "aaa", 159 | NewValue: "bbb", 160 | Setter: "foo-ns:policy:name", 161 | }, 162 | }, 163 | objectNames[1]: []Change{ 164 | { 165 | OldValue: "cccc:v1.0", 166 | NewValue: "cccc:v1.2", 167 | Setter: "foo-ns:policy", 168 | }, 169 | }, 170 | })) 171 | } 172 | -------------------------------------------------------------------------------- /pkg/update/setters.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020, 2021 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package update 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/go-logr/logr" 23 | "github.com/google/go-containerregistry/pkg/name" 24 | "k8s.io/apimachinery/pkg/types" 25 | "k8s.io/kube-openapi/pkg/validation/spec" 26 | "sigs.k8s.io/kustomize/kyaml/fieldmeta" 27 | "sigs.k8s.io/kustomize/kyaml/kio" 28 | "sigs.k8s.io/kustomize/kyaml/kio/kioutil" 29 | "sigs.k8s.io/kustomize/kyaml/openapi" 30 | "sigs.k8s.io/kustomize/kyaml/sets" 31 | "sigs.k8s.io/kustomize/kyaml/yaml" 32 | 33 | "github.com/fluxcd/image-automation-controller/internal/constants" 34 | imagev1_reflect "github.com/fluxcd/image-reflector-controller/api/v1beta2" 35 | ) 36 | 37 | const ( 38 | // This is preserved from setters2 39 | K8sCliExtensionKey = "x-k8s-cli" 40 | ) 41 | 42 | func init() { 43 | fieldmeta.SetShortHandRef(constants.SetterShortHand) 44 | // this prevents the global schema, should it be initialised, from 45 | // parsing all the Kubernetes openAPI definitions, which is not 46 | // necessary. 47 | openapi.SuppressBuiltInSchemaUse() 48 | } 49 | 50 | // UpdateWithSetters takes all YAML files from `inpath`, updates any 51 | // that contain an "in scope" image policy marker, and writes files it 52 | // updated (and only those files) back to `outpath`. 53 | func UpdateWithSetters(tracelog logr.Logger, inpath, outpath string, policies []imagev1_reflect.ImagePolicy) (Result, error) { 54 | result, err := UpdateV2WithSetters(tracelog, inpath, outpath, policies) 55 | return result.ImageResult, err 56 | } 57 | 58 | // UpdateV2WithSetters takes all YAML files from `inpath`, updates any 59 | // that contain an "in scope" image policy marker, and writes files it 60 | // updated (and only those files) back to `outpath`. It also returns the result 61 | // of the changes it made as ResultV2. 62 | func UpdateV2WithSetters(tracelog logr.Logger, inpath, outpath string, policies []imagev1_reflect.ImagePolicy) (ResultV2, error) { 63 | // the OpenAPI schema is a package variable in kyaml/openapi. In 64 | // lieu of being able to isolate invocations (per 65 | // https://github.com/kubernetes-sigs/kustomize/issues/3058), I 66 | // serialise access to it and reset it each time. 67 | 68 | // construct definitions 69 | 70 | // the format of the definitions expected is given here: 71 | // https://github.com/kubernetes-sigs/kustomize/blob/master/kyaml/setters2/doc.go 72 | // 73 | // { 74 | // "definitions": { 75 | // "io.k8s.cli.setters.replicas": { 76 | // "x-k8s-cli": { 77 | // "setter": { 78 | // "name": "replicas", 79 | // "value": "4" 80 | // } 81 | // } 82 | // } 83 | // } 84 | // } 85 | // 86 | // (there are consts in kyaml/fieldmeta with the 87 | // prefixes). 88 | // 89 | // `fieldmeta.SetShortHandRef("$imagepolicy")` makes it possible 90 | // to just use (e.g.,) 91 | // 92 | // image: foo:v1 # {"$imagepolicy": "automation-ns:foo"} 93 | // 94 | // to mark the fields at which to make replacements. A colon is 95 | // used to separate namespace and name in the key, because a slash 96 | // would be interpreted as part of the $ref path. 97 | 98 | var settersSchema spec.Schema 99 | 100 | // collect setter defs and setters by going through all the image 101 | // policies available. 102 | result := Result{ 103 | Files: make(map[string]FileResult), 104 | } 105 | 106 | var resultV2 ResultV2 107 | 108 | // Compilng the result needs the file, the image ref used, and the 109 | // object. Each setter will supply its own name to its callback, 110 | // which can be used to look up the image ref; the file and object 111 | // we will get from `setAll` which keeps track of those as it 112 | // iterates. 113 | imageRefs := make(map[string]imageRef) 114 | setAllCallback := func(file, setterName string, node *yaml.RNode, old, new string) { 115 | ref, ok := imageRefs[setterName] 116 | if !ok { 117 | return 118 | } 119 | 120 | meta, err := node.GetMeta() 121 | if err != nil { 122 | return 123 | } 124 | oid := ObjectIdentifier{meta.GetIdentifier()} 125 | 126 | // Record the change. 127 | ch := Change{ 128 | OldValue: old, 129 | NewValue: new, 130 | Setter: setterName, 131 | } 132 | // Append the change for the file and identifier. 133 | resultV2.AddChange(file, oid, ch) 134 | 135 | fileres, ok := result.Files[file] 136 | if !ok { 137 | fileres = FileResult{ 138 | Objects: make(map[ObjectIdentifier][]ImageRef), 139 | } 140 | result.Files[file] = fileres 141 | } 142 | objres := fileres.Objects[oid] 143 | for _, n := range objres { 144 | if n == ref { 145 | return 146 | } 147 | } 148 | objres = append(objres, ref) 149 | fileres.Objects[oid] = objres 150 | } 151 | 152 | defs := map[string]spec.Schema{} 153 | for _, policy := range policies { 154 | if policy.Status.LatestRef == nil { 155 | continue 156 | } 157 | // Using strict validation would mean any image that omits the 158 | // registry would be rejected, so that can't be used 159 | // here. Using _weak_ validation means that defaults will be 160 | // filled in. Usually this would mean the tag would end up 161 | // being `latest` if empty in the input; but I'm assuming here 162 | // that the policy won't have a tagless ref. 163 | image := policy.Status.LatestRef.String() 164 | r, err := name.ParseReference(image, name.WeakValidation) 165 | if err != nil { 166 | return ResultV2{}, fmt.Errorf("encountered invalid image ref %q: %w", image, err) 167 | } 168 | ref := imageRef{ 169 | Reference: r, 170 | policy: types.NamespacedName{ 171 | Name: policy.Name, 172 | Namespace: policy.Namespace, 173 | }, 174 | } 175 | 176 | tag := policy.Status.LatestRef.Tag 177 | name := policy.Status.LatestRef.Name 178 | digest := policy.Status.LatestRef.Digest 179 | 180 | imageSetter := fmt.Sprintf("%s:%s", policy.GetNamespace(), policy.GetName()) 181 | tracelog.Info("adding setter", "name", imageSetter) 182 | defs[fieldmeta.SetterDefinitionPrefix+imageSetter] = setterSchema(imageSetter, image) 183 | imageRefs[imageSetter] = ref 184 | 185 | tagSetter := imageSetter + ":tag" 186 | tracelog.Info("adding setter", "name", tagSetter) 187 | defs[fieldmeta.SetterDefinitionPrefix+tagSetter] = setterSchema(tagSetter, tag) 188 | imageRefs[tagSetter] = ref 189 | 190 | nameSetter := imageSetter + ":name" 191 | tracelog.Info("adding setter", "name", nameSetter) 192 | defs[fieldmeta.SetterDefinitionPrefix+nameSetter] = setterSchema(nameSetter, name) 193 | imageRefs[nameSetter] = ref 194 | 195 | digestSetter := imageSetter + ":digest" 196 | tracelog.Info("adding setter", "name", digestSetter) 197 | defs[fieldmeta.SetterDefinitionPrefix+digestSetter] = setterSchema(digestSetter, digest) 198 | imageRefs[digestSetter] = ref 199 | } 200 | 201 | settersSchema.Definitions = defs 202 | 203 | // get ready with the reader and writer 204 | reader := &ScreeningLocalReader{ 205 | Path: inpath, 206 | Token: fmt.Sprintf("%q", constants.SetterShortHand), 207 | Trace: tracelog, 208 | } 209 | writer := &kio.LocalPackageWriter{ 210 | PackagePath: outpath, 211 | } 212 | 213 | pipeline := kio.Pipeline{ 214 | Inputs: []kio.Reader{reader}, 215 | Outputs: []kio.Writer{writer}, 216 | Filters: []kio.Filter{ 217 | setAll(&settersSchema, tracelog, setAllCallback), 218 | }, 219 | } 220 | 221 | // go! 222 | err := pipeline.Execute() 223 | if err != nil { 224 | return ResultV2{}, err 225 | } 226 | 227 | // Combine the results. 228 | resultV2.ImageResult = result 229 | return resultV2, nil 230 | } 231 | 232 | // setAll returns a kio.Filter using the supplied SetAllCallback 233 | // (dealing with individual nodes), amd calling the given callback 234 | // whenever a field value is changed, and returning only nodes from 235 | // files with changed nodes. This is based on 236 | // [`SetAll`](https://github.com/kubernetes-sigs/kustomize/blob/kyaml/v0.10.16/kyaml/setters2/set.go#L503 237 | // from kyaml/kio. 238 | func setAll(schema *spec.Schema, tracelog logr.Logger, callback func(file, setterName string, node *yaml.RNode, old, new string)) kio.Filter { 239 | filter := &SetAllCallback{ 240 | SettersSchema: schema, 241 | Trace: tracelog, 242 | } 243 | return kio.FilterFunc( 244 | func(nodes []*yaml.RNode) ([]*yaml.RNode, error) { 245 | filesToUpdate := sets.String{} 246 | for i := range nodes { 247 | path, _, err := kioutil.GetFileAnnotations(nodes[i]) 248 | if err != nil { 249 | return nil, err 250 | } 251 | 252 | filter.Callback = func(setter, oldValue, newValue string) { 253 | if newValue != oldValue { 254 | callback(path, setter, nodes[i], oldValue, newValue) 255 | filesToUpdate.Insert(path) 256 | } 257 | } 258 | _, err = filter.Filter(nodes[i]) 259 | if err != nil { 260 | return nil, err 261 | } 262 | } 263 | 264 | var nodesInUpdatedFiles []*yaml.RNode 265 | for i := range nodes { 266 | path, _, err := kioutil.GetFileAnnotations(nodes[i]) 267 | if err != nil { 268 | return nil, err 269 | } 270 | if filesToUpdate.Has(path) { 271 | nodesInUpdatedFiles = append(nodesInUpdatedFiles, nodes[i]) 272 | } 273 | } 274 | return nodesInUpdatedFiles, nil 275 | }) 276 | } 277 | 278 | func setterSchema(name, value string) spec.Schema { 279 | schema := spec.StringProperty() 280 | schema.Extensions = map[string]interface{}{} 281 | schema.Extensions.Add(K8sCliExtensionKey, map[string]interface{}{ 282 | "setter": map[string]string{ 283 | "name": name, 284 | "value": value, 285 | }, 286 | }) 287 | return *schema 288 | } 289 | -------------------------------------------------------------------------------- /pkg/update/testdata/leave/expected/cronjob.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1beta1 2 | kind: CronJob 3 | metadata: 4 | name: foo 5 | namespace: bar 6 | spec: 7 | schedule: "*/1 * * * *" 8 | jobTemplate: 9 | spec: 10 | template: 11 | spec: 12 | containers: 13 | - name: c 14 | image: helloworld:v1.0.0 15 | -------------------------------------------------------------------------------- /pkg/update/testdata/leave/expected/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: foo 5 | namespace: bar 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: c 11 | image: helloworld:v1.0.0 12 | args: 13 | # prove that go-yaml/yaml v3.0.0-20200615113413-eeeca48fe776 fixes https://github.com/kubernetes-sigs/kustomize/issues/3969 14 | - mv /etc/resolv.conf /etc/resolv.conf.OLD && ln -s /run/systemd/resolve/resolv.conf /etc/resolv.conf 15 | -------------------------------------------------------------------------------- /pkg/update/testdata/leave/original/cronjob.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1beta1 2 | kind: CronJob 3 | metadata: 4 | name: foo 5 | namespace: bar 6 | spec: 7 | schedule: "*/1 * * * *" 8 | jobTemplate: 9 | spec: 10 | template: 11 | spec: 12 | containers: 13 | - name: c 14 | image: helloworld:v1.0.0 15 | -------------------------------------------------------------------------------- /pkg/update/testdata/leave/original/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: foo 5 | namespace: bar 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: c 11 | image: helloworld:v1.0.0 12 | args: 13 | # prove that go-yaml/yaml v3.0.0-20200615113413-eeeca48fe776 fixes https://github.com/kubernetes-sigs/kustomize/issues/3969 14 | - mv /etc/resolv.conf /etc/resolv.conf.OLD && ln -s /run/systemd/resolve/resolv.conf /etc/resolv.conf 15 | -------------------------------------------------------------------------------- /pkg/update/testdata/replace/commented-expected/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: foo 5 | namespace: bar 6 | spec: 7 | template: 8 | # prove that go-yaml/yaml v3.0.0-20200615113413-eeeca48fe776 fixes https://github.com/kubernetes-sigs/kustomize/issues/3605 9 | # 컨테이너 10 | spec: # 컨테이너 11 | containers: 12 | - name: c 13 | image: used:v1.1.0 # the comment must stay! 14 | -------------------------------------------------------------------------------- /pkg/update/testdata/replace/commented/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: foo 5 | namespace: bar 6 | spec: 7 | template: 8 | # prove that go-yaml/yaml v3.0.0-20200615113413-eeeca48fe776 fixes https://github.com/kubernetes-sigs/kustomize/issues/3605 9 | # 컨테이너 10 | spec: # 컨테이너 11 | containers: 12 | - name: c 13 | image: used:v1.0.0 # the comment must stay! 14 | -------------------------------------------------------------------------------- /pkg/update/testdata/replace/expected/cronjob.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1beta1 2 | kind: CronJob 3 | metadata: 4 | name: foo 5 | namespace: bar 6 | spec: 7 | schedule: "*/1 * * * *" 8 | jobTemplate: 9 | spec: 10 | template: 11 | spec: 12 | containers: 13 | - name: c 14 | image: used:v1.1.0 15 | -------------------------------------------------------------------------------- /pkg/update/testdata/replace/expected/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: foo 5 | namespace: bar 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: c 11 | image: used:v1.1.0 12 | -------------------------------------------------------------------------------- /pkg/update/testdata/replace/original/cronjob.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1beta1 2 | kind: CronJob 3 | metadata: 4 | name: foo 5 | namespace: bar 6 | spec: 7 | schedule: "*/1 * * * *" 8 | jobTemplate: 9 | spec: 10 | template: 11 | spec: 12 | containers: 13 | - name: c 14 | image: used:v1.0.0 15 | -------------------------------------------------------------------------------- /pkg/update/testdata/replace/original/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: foo 5 | namespace: bar 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: c 11 | image: used:v1.0.0 12 | -------------------------------------------------------------------------------- /pkg/update/testdata/setters/expected/Kustomization: -------------------------------------------------------------------------------- 1 | # This is not intended to be a working kustomization 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | resources: 5 | - irrelevant.yaml 6 | images: 7 | - name: container 8 | newName: index.repo.fake/updated # {"$imagepolicy": "automation-ns:policy:name"} 9 | newTag: v1.0.1 # {"$imagepolicy": "automation-ns:policy:tag"} 10 | -------------------------------------------------------------------------------- /pkg/update/testdata/setters/expected/kustomization.yml: -------------------------------------------------------------------------------- 1 | # This is not intended to be a working kustomization 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | resources: 5 | - unimportant.yaml 6 | images: 7 | - name: container 8 | newName: index.repo.fake/updated # {"$imagepolicy": "automation-ns:policy:name"} 9 | newTag: v1.0.1 # {"$imagepolicy": "automation-ns:policy:tag"} 10 | newDigest: sha256:6745aaad46d795c9836632e1fb62f24b7e7f4c843144da8e47a5465c411a14be # {"$imagepolicy": "automation-ns:policy-with-digest:digest"} 11 | # Prove fix for https://github.com/fluxcd/flux2/issues/3284 12 | patches: 13 | - patch: | 14 | - op: replace 15 | path: /spec/template/spec/containers/0/volumeMounts 16 | value: 17 | - mountPath: /usr/share/wildfly/wildfly/standalone/configuration/appConfigMaps/WF 18 | name: wf-config 19 | - mountPath: /usr/share/wildfly/wildfly/standalone/configuration/appConfigMaps/xxx_config 20 | name: xxx-config 21 | - mountPath: /usr/share/wildfly/wildfly/standalone/log 22 | name: wildfly-standalone-log 23 | target: 24 | group: apps 25 | version: v1 26 | kind: Deployment 27 | name: sxxxxdadminservice 28 | image: image:v1.0.0@sha256:6745aaad46d795c9836632e1fb62f24b7e7f4c843144da8e47a5465c411a14be # {"$imagepolicy": "automation-ns:policy-with-digest"} 29 | -------------------------------------------------------------------------------- /pkg/update/testdata/setters/expected/marked.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1beta1 2 | kind: CronJob 3 | metadata: 4 | name: foo 5 | namespace: bar 6 | spec: 7 | schedule: "*/1 * * * *" 8 | jobTemplate: 9 | spec: 10 | template: 11 | spec: 12 | containers: 13 | - name: c 14 | image: index.repo.fake/updated:v1.0.1 # {"$imagepolicy": "automation-ns:policy"} 15 | - name: d 16 | image: image:v1.0.0 # {"$imagepolicy": "automation-ns:unchanged"} 17 | -------------------------------------------------------------------------------- /pkg/update/testdata/setters/original/Kustomization: -------------------------------------------------------------------------------- 1 | # This is not intended to be a working kustomization 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | resources: 5 | - irrelevant.yaml 6 | images: 7 | - name: container 8 | newName: replaced # {"$imagepolicy": "automation-ns:policy:name"} 9 | newTag: v1 # {"$imagepolicy": "automation-ns:policy:tag"} 10 | -------------------------------------------------------------------------------- /pkg/update/testdata/setters/original/kustomization.yml: -------------------------------------------------------------------------------- 1 | # This is not intended to be a working kustomization 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | resources: 5 | - unimportant.yaml 6 | images: 7 | - name: container 8 | newName: replaced # {"$imagepolicy": "automation-ns:policy:name"} 9 | newTag: v1 # {"$imagepolicy": "automation-ns:policy:tag"} 10 | newDigest: sha256:1234567890abcdef # {"$imagepolicy": "automation-ns:policy-with-digest:digest"} 11 | # Prove fix for https://github.com/fluxcd/flux2/issues/3284 12 | patches: 13 | - patch: | 14 | - op: replace 15 | path: /spec/template/spec/containers/0/volumeMounts 16 | value: 17 | - mountPath: /usr/share/wildfly/wildfly/standalone/configuration/appConfigMaps/WF 18 | name: wf-config 19 | - mountPath: /usr/share/wildfly/wildfly/standalone/configuration/appConfigMaps/xxx_config 20 | name: xxx-config 21 | - mountPath: /usr/share/wildfly/wildfly/standalone/log 22 | name: wildfly-standalone-log 23 | target: 24 | group: apps 25 | version: v1 26 | kind: Deployment 27 | name: sxxxxdadminservice 28 | image: image # {"$imagepolicy": "automation-ns:policy-with-digest"} 29 | -------------------------------------------------------------------------------- /pkg/update/testdata/setters/original/marked.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1beta1 2 | kind: CronJob 3 | metadata: 4 | name: foo 5 | namespace: bar 6 | spec: 7 | schedule: "*/1 * * * *" 8 | jobTemplate: 9 | spec: 10 | template: 11 | spec: 12 | containers: 13 | - name: c 14 | image: image:v1.0.0 # {"$imagepolicy": "automation-ns:policy"} 15 | - name: d 16 | image: image:v1.0.0 # {"$imagepolicy": "automation-ns:unchanged"} 17 | -------------------------------------------------------------------------------- /pkg/update/testdata/setters/original/otherns.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: foo 5 | namespace: bar 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: c 11 | image: user:v1.0.0 # {"$imagepolicy": "other-namespace:policy"} 12 | -------------------------------------------------------------------------------- /pkg/update/testdata/setters/original/unmarked.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: foo 5 | namespace: bar 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: c 11 | image: image:v1.0.0 # uses the same image. but should be left alone 12 | -------------------------------------------------------------------------------- /pkg/update/update_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020, 2021 The Flux authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package update 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/go-logr/logr" 23 | "github.com/google/go-containerregistry/pkg/name" 24 | . "github.com/onsi/gomega" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/types" 27 | "sigs.k8s.io/kustomize/kyaml/yaml" 28 | 29 | "github.com/fluxcd/image-automation-controller/internal/testutil" 30 | "github.com/fluxcd/image-automation-controller/pkg/test" 31 | imagev1_reflect "github.com/fluxcd/image-reflector-controller/api/v1beta2" 32 | ) 33 | 34 | func TestUpdateWithSetters(t *testing.T) { 35 | g := NewWithT(t) 36 | 37 | policies := []imagev1_reflect.ImagePolicy{ 38 | { 39 | ObjectMeta: metav1.ObjectMeta{ 40 | Namespace: "automation-ns", 41 | Name: "policy", 42 | }, 43 | Status: imagev1_reflect.ImagePolicyStatus{ 44 | LatestRef: testutil.ImageToRef("index.repo.fake/updated:v1.0.1"), 45 | }, 46 | }, 47 | { 48 | ObjectMeta: metav1.ObjectMeta{ 49 | Namespace: "automation-ns", 50 | Name: "unchanged", 51 | }, 52 | Status: imagev1_reflect.ImagePolicyStatus{ 53 | LatestRef: testutil.ImageToRef("image:v1.0.0"), 54 | }, 55 | }, 56 | { 57 | ObjectMeta: metav1.ObjectMeta{ 58 | Namespace: "automation-ns", 59 | Name: "policy-with-digest", 60 | }, 61 | Status: imagev1_reflect.ImagePolicyStatus{ 62 | LatestRef: testutil.ImageToRef("image:v1.0.0@sha256:6745aaad46d795c9836632e1fb62f24b7e7f4c843144da8e47a5465c411a14be"), 63 | }, 64 | }, 65 | } 66 | 67 | tmp := t.TempDir() 68 | result, err := UpdateWithSetters(logr.Discard(), "testdata/setters/original", tmp, policies) 69 | g.Expect(err).ToNot(HaveOccurred()) 70 | test.ExpectMatchingDirectories(g, tmp, "testdata/setters/expected") 71 | 72 | kustomizeResourceID := ObjectIdentifier{yaml.ResourceIdentifier{ 73 | TypeMeta: yaml.TypeMeta{ 74 | APIVersion: "kustomize.config.k8s.io/v1beta1", 75 | Kind: "Kustomization", 76 | }, 77 | }} 78 | markedResourceID := ObjectIdentifier{yaml.ResourceIdentifier{ 79 | TypeMeta: yaml.TypeMeta{ 80 | APIVersion: "batch/v1beta1", 81 | Kind: "CronJob", 82 | }, 83 | NameMeta: yaml.NameMeta{ 84 | Namespace: "bar", 85 | Name: "foo", 86 | }, 87 | }} 88 | 89 | r, _ := name.ParseReference("index.repo.fake/updated:v1.0.1") 90 | expectedImageRef := imageRef{r, types.NamespacedName{ 91 | Name: "policy", 92 | Namespace: "automation-ns", 93 | }} 94 | 95 | r, _ = name.ParseReference("image:v1.0.0@sha256:6745aaad46d795c9836632e1fb62f24b7e7f4c843144da8e47a5465c411a14be") 96 | expectedImageRefDigest := imageRef{r, types.NamespacedName{ 97 | Name: "policy-with-digest", 98 | Namespace: "automation-ns", 99 | }} 100 | 101 | expectedResult := Result{ 102 | Files: map[string]FileResult{ 103 | "kustomization.yml": { 104 | Objects: map[ObjectIdentifier][]ImageRef{ 105 | kustomizeResourceID: { 106 | expectedImageRef, 107 | expectedImageRefDigest, 108 | }, 109 | }, 110 | }, 111 | "Kustomization": { 112 | Objects: map[ObjectIdentifier][]ImageRef{ 113 | kustomizeResourceID: { 114 | expectedImageRef, 115 | }, 116 | }, 117 | }, 118 | "marked.yaml": { 119 | Objects: map[ObjectIdentifier][]ImageRef{ 120 | markedResourceID: { 121 | expectedImageRef, 122 | }, 123 | }, 124 | }, 125 | }, 126 | } 127 | 128 | g.Expect(result).To(Equal(expectedResult)) 129 | 130 | // Test ResultV2. 131 | tmp2 := t.TempDir() 132 | resultV2, err := UpdateV2WithSetters(logr.Discard(), "testdata/setters/original", tmp2, policies) 133 | g.Expect(err).ToNot(HaveOccurred()) 134 | test.ExpectMatchingDirectories(g, tmp2, "testdata/setters/expected") 135 | 136 | expectedResultV2 := ResultV2{ 137 | ImageResult: expectedResult, 138 | FileChanges: map[string]ObjectChanges{ 139 | "kustomization.yml": { 140 | kustomizeResourceID: []Change{ 141 | { 142 | OldValue: "replaced", 143 | NewValue: "index.repo.fake/updated", 144 | Setter: "automation-ns:policy:name", 145 | }, 146 | { 147 | OldValue: "v1", 148 | NewValue: "v1.0.1", 149 | Setter: "automation-ns:policy:tag", 150 | }, 151 | { 152 | OldValue: "sha256:1234567890abcdef", 153 | NewValue: "sha256:6745aaad46d795c9836632e1fb62f24b7e7f4c843144da8e47a5465c411a14be", 154 | Setter: "automation-ns:policy-with-digest:digest", 155 | }, 156 | { 157 | OldValue: "image", 158 | NewValue: "image:v1.0.0@sha256:6745aaad46d795c9836632e1fb62f24b7e7f4c843144da8e47a5465c411a14be", 159 | Setter: "automation-ns:policy-with-digest", 160 | }, 161 | }, 162 | }, 163 | "Kustomization": { 164 | kustomizeResourceID: []Change{ 165 | { 166 | OldValue: "replaced", 167 | NewValue: "index.repo.fake/updated", 168 | Setter: "automation-ns:policy:name", 169 | }, 170 | { 171 | OldValue: "v1", 172 | NewValue: "v1.0.1", 173 | Setter: "automation-ns:policy:tag", 174 | }, 175 | }, 176 | }, 177 | "marked.yaml": { 178 | markedResourceID: []Change{ 179 | { 180 | OldValue: "image:v1.0.0", 181 | NewValue: "index.repo.fake/updated:v1.0.1", 182 | Setter: "automation-ns:policy", 183 | }, 184 | }, 185 | }, 186 | }, 187 | } 188 | 189 | g.Expect(resultV2).To(Equal(expectedResultV2)) 190 | } 191 | -------------------------------------------------------------------------------- /tests/fuzz/Dockerfile.builder: -------------------------------------------------------------------------------- 1 | FROM gcr.io/oss-fuzz-base/base-builder-go 2 | 3 | RUN wget https://go.dev/dl/go1.24.0.linux-amd64.tar.gz \ 4 | && mkdir temp-go \ 5 | && rm -rf /root/.go/* \ 6 | && tar -C temp-go/ -xzf go1.24.0.linux-amd64.tar.gz \ 7 | && mv temp-go/go/* /root/.go/ 8 | 9 | ENV SRC=$GOPATH/src/github.com/fluxcd/image-automation-controller 10 | ENV ROOT_ORG=$SRC 11 | ENV FLUX_CI=true 12 | 13 | COPY ./ $GOPATH/src/github.com/fluxcd/image-automation-controller/ 14 | RUN wget https://raw.githubusercontent.com/google/oss-fuzz/master/projects/fluxcd/build.sh -O $SRC/build.sh 15 | 16 | WORKDIR $SRC 17 | -------------------------------------------------------------------------------- /tests/fuzz/README.md: -------------------------------------------------------------------------------- 1 | # fuzz testing 2 | 3 | Flux is part of Google's [oss fuzz] program which provides continuous fuzzing for 4 | open source projects. 5 | 6 | The long running fuzzing execution is configured in the [oss-fuzz repository]. 7 | Shorter executions are done on a per-PR basis, configured as a [github workflow]. 8 | 9 | ### Testing locally 10 | 11 | Build fuzzers: 12 | 13 | ```bash 14 | make fuzz-build 15 | ``` 16 | All fuzzers will be built into `./build/fuzz/out`. 17 | 18 | Smoke test fuzzers: 19 | 20 | All the fuzzers will be built and executed once, to ensure they are fully functional. 21 | 22 | ```bash 23 | make fuzz-smoketest 24 | ``` 25 | 26 | Run fuzzer locally: 27 | ```bash 28 | ./build/fuzz/out/fuzz_conditions_match 29 | ``` 30 | 31 | Run fuzzer inside a container: 32 | 33 | ```bash 34 | docker run --rm -ti \ 35 | -v "$(pwd)/build/fuzz/out":/out \ 36 | gcr.io/oss-fuzz/fluxcd \ 37 | /out/fuzz_conditions_match 38 | ``` 39 | 40 | ### Caveats of creating oss-fuzz compatible tests 41 | 42 | #### Segregate fuzz tests 43 | 44 | OSS-Fuzz does not properly support mixed `*_test.go` files, in which there is a combination 45 | of fuzz and non-fuzz tests. To mitigate this problem, ensure your fuzz tests are not in the 46 | same file as other Go tests. As a pattern, call your fuzz test files `*_fuzz_test.go`. 47 | 48 | #### Build tags to avoid conflicts when running Go tests 49 | 50 | Due to the issue above, code duplication will occur when creating fuzz tests that rely on 51 | helper functions that are shared with other tests. To avoid build issues, add a conditional 52 | build tag at the top of the `*_fuzz_test.go` file: 53 | ```go 54 | //go:build gofuzz_libfuzzer 55 | // +build gofuzz_libfuzzer 56 | ``` 57 | 58 | The build tag above is set at [go-118-fuzz-build]. 59 | At this point in time we can't pass on specific tags from [compile_native_go_fuzzer]. 60 | 61 | ### Running oss-fuzz locally 62 | 63 | The `make fuzz-smoketest` is meant to be an easy way to reproduce errors that may occur 64 | upstream. If our checks ever run out of sync with upstream, the upstream tests can be 65 | executed locally with: 66 | 67 | ``` 68 | git clone --depth 1 https://github.com/google/oss-fuzz 69 | cd oss-fuzz 70 | python infra/helper.py build_image fluxcd 71 | python infra/helper.py build_fuzzers --sanitizer address --architecture x86_64 fluxcd 72 | python infra/helper.py check_build --sanitizer address --architecture x86_64 fluxcd 73 | ``` 74 | 75 | For latest info on testing oss-fuzz locally, refer to the [upstream guide]. 76 | 77 | [oss fuzz]: https://github.com/google/oss-fuzz 78 | [oss-fuzz repository]: https://github.com/google/oss-fuzz/tree/master/projects/fluxcd 79 | [github workflow]: .github/workflows/cifuzz.yaml 80 | [upstream guide]: https://google.github.io/oss-fuzz/getting-started/new-project-guide/#testing-locally 81 | [go-118-fuzz-build]: https://github.com/AdamKorcz/go-118-fuzz-build/blob/b2031950a318d4f2dcf3ec3e128f904d5cf84623/main.go#L40 82 | [compile_native_go_fuzzer]: https://github.com/google/oss-fuzz/blob/c2d827cb78529fdc757c9b0b4fea0f1238a54814/infra/base-images/base-builder/compile_native_go_fuzzer#L32 -------------------------------------------------------------------------------- /tests/fuzz/native_go_run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2022 The Flux authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -euxo pipefail 18 | 19 | # This script iterates through all go fuzzing targets, running each one 20 | # through the period of time established by FUZZ_TIME. 21 | 22 | FUZZ_TIME=${FUZZ_TIME:-"5s"} 23 | 24 | # kustomization_fuzzer_test is not fully compatible with Go native fuzz, 25 | # so it is ignored here. 26 | test_files=$(grep -r --include='**_test.go' --files-with-matches 'func Fuzz' . | \ 27 | grep -v "controllers_fuzzer_test.go") 28 | 29 | for file in ${test_files} 30 | do 31 | targets=$(grep -oP 'func \K(Fuzz\w*)' "${file}") 32 | for target_name in ${targets} 33 | do 34 | echo "Running ${file}.${target_name} for ${FUZZ_TIME}." 35 | file_dir=$(dirname "${file}") 36 | 37 | go test -fuzz="^${target_name}\$" -fuzztime "${FUZZ_TIME}" "${file_dir}" 38 | done 39 | done 40 | -------------------------------------------------------------------------------- /tests/fuzz/oss_fuzz_prebuild.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2022 The Flux authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -euxo pipefail 18 | 19 | # This file is executed by upstream oss-fuzz for any requirements that 20 | # are specific for building this project. 21 | 22 | # Some tests requires embedded resources. Embedding does not allow 23 | # for traversing into ascending dirs, therefore we copy those contents here: 24 | mkdir -p internal/controller/testdata/crd 25 | cp config/crd/bases/*.yaml internal/controller/testdata/crd 26 | 27 | # Version of the source-controller from which to get the GitRepository CRD. 28 | # Pulls source-controller/api's version set in go.mod. 29 | SOURCE_VER=$(go list -m github.com/fluxcd/source-controller/api | awk '{print $2}') 30 | 31 | # Version of the image-reflector-controller from which to get the ImagePolicy CRD. 32 | # Pulls image-reflector-controller/api's version set in go.mod. 33 | REFLECTOR_VER=$(go list -m github.com/fluxcd/image-reflector-controller/api | awk '{print $2}') 34 | 35 | if [ -d "../../internal/controller/testdata/crds" ]; then 36 | cp ../../internal/controller/testdata/crds/*.yaml testdata/crds 37 | else 38 | # Fetch the CRDs if not present since we need them when running fuzz tests on CI. 39 | curl -s --fail https://raw.githubusercontent.com/fluxcd/source-controller/${SOURCE_VER}/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml -o internal/controller/testdata/crd/gitrepositories.yaml 40 | curl -s --fail https://raw.githubusercontent.com/fluxcd/image-reflector-controller/${REFLECTOR_VER}/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml -o internal/controller/testdata/crd/imagepolicies.yaml 41 | fi 42 | -------------------------------------------------------------------------------- /tests/fuzz/oss_fuzz_run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2022 The Flux authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -euxo pipefail 18 | 19 | # run each fuzzer once to ensure they are working properly 20 | find /out -type f -iname "fuzz*" -exec echo {} -runs=1 \; | bash -e 21 | --------------------------------------------------------------------------------