├── .github ├── dependabot.yml ├── release.yml └── workflows │ ├── chart.yml │ └── ci.yml ├── .gitignore ├── .golangci.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── charts ├── cr.yaml └── k8s-image-availability-exporter │ ├── Chart.yaml │ ├── README.md │ ├── README.md.gotmpl │ ├── templates │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── poddisruptionbudget.yaml │ ├── prometheus-rule.yaml │ ├── rbac.yaml │ ├── service-account.yaml │ ├── service-monitor.yaml │ └── service.yaml │ └── values.yaml ├── go.mod ├── go.sum ├── main.go └── pkg ├── cli ├── cli.go └── cli_test.go ├── handlers └── healthz.go ├── logging └── log.go ├── providers ├── amazon │ └── amazon.go ├── k8s │ └── k8s.go └── provider.go ├── registry ├── checker.go ├── checker_test.go ├── image_pull.go └── indexers.go ├── store ├── image_store.go └── image_store_test.go └── version └── version.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Dependencies listed in go.mod 4 | - package-ecosystem: "gomod" 5 | directory: "/" # Location of package manifests 6 | schedule: 7 | interval: "weekly" 8 | 9 | # Dependencies listed in .github/workflows/*.yml 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | 15 | # Dependencies listed in Dockerfile 16 | - package-ecosystem: "docker" 17 | directory: "/" 18 | schedule: 19 | interval: "weekly" 20 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore 5 | categories: 6 | - title: Enhancements 🚀 7 | labels: 8 | - enhancement 9 | - title: Bug Fixes 🐛 10 | labels: 11 | - bug 12 | - title: Dependency Updates ⬆️ 13 | labels: 14 | - dependencies 15 | - title: Other Changes 16 | labels: 17 | - "*" 18 | -------------------------------------------------------------------------------- /.github/workflows/chart.yml: -------------------------------------------------------------------------------- 1 | name: Release Charts 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'charts/**' 9 | 10 | jobs: 11 | chart-release: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Configure Git 20 | run: | 21 | git config user.name "$GITHUB_ACTOR" 22 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 23 | 24 | - name: Install Helm 25 | uses: azure/setup-helm@v4 26 | with: 27 | version: v3.7.1 28 | 29 | - name: Run chart-releaser 30 | uses: helm/chart-releaser-action@v1.7.0 31 | with: 32 | charts_dir: charts 33 | config: charts/cr.yaml 34 | env: 35 | CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | env: 13 | REGISTRY: registry-write.deckhouse.io 14 | IMAGE_NAME: k8s-image-availability-exporter/k8s-image-availability-exporter 15 | 16 | jobs: 17 | test: 18 | name: Test 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/setup-go@v5 23 | with: 24 | go-version: '1.23' 25 | 26 | - uses: actions/checkout@v4 27 | 28 | - name: Test With Coverage 29 | run: go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... 30 | 31 | - name: Upload coverage to Codecov 32 | uses: codecov/codecov-action@v5.4.2 33 | with: 34 | files: coverage.txt 35 | 36 | lint: 37 | name: Lint 38 | runs-on: ubuntu-latest 39 | 40 | steps: 41 | - uses: actions/checkout@v4 42 | 43 | - uses: actions/setup-go@v5 44 | with: 45 | go-version: '1.23' 46 | 47 | - name: Download golangci-lint 48 | run: make bin/golangci-lint 49 | 50 | - name: Lint 51 | run: make lint 52 | 53 | build: 54 | name: Build 55 | 56 | runs-on: ubuntu-latest 57 | needs: test 58 | 59 | permissions: 60 | contents: read 61 | packages: write 62 | 63 | steps: 64 | - uses: actions/checkout@v4 65 | 66 | # Login against a Docker registry except on PR 67 | # https://github.com/docker/login-action 68 | - name: Log into registry ${{ env.REGISTRY }} 69 | if: github.event_name != 'pull_request' 70 | uses: docker/login-action@v3.4.0 71 | with: 72 | registry: ${{ env.REGISTRY }} 73 | username: ${{ secrets.DECKHOUSE_REGISTRY_USER }} 74 | password: ${{ secrets.DECKHOUSE_REGISTRY_PASSWORD }} 75 | 76 | # Extract metadata (tags, labels) for Docker 77 | # https://github.com/docker/metadata-action 78 | - name: Extract Docker metadata 79 | id: meta 80 | uses: docker/metadata-action@v5.7.0 81 | with: 82 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 83 | 84 | # Build and push Docker image with Buildx (don't push on PR) 85 | # https://github.com/docker/build-push-action 86 | - name: Build and push Docker image 87 | uses: docker/build-push-action@v6.16.0 88 | with: 89 | context: . 90 | push: ${{ github.event_name != 'pull_request' }} 91 | tags: ${{ steps.meta.outputs.tags }} 92 | labels: ${{ steps.meta.outputs.labels }} 93 | build-args: | 94 | TAG=${{ github.ref_name }} 95 | 96 | chart: 97 | name: Chart 98 | 99 | runs-on: ubuntu-latest 100 | outputs: 101 | changed: ${{ steps.changes.outputs.changed }} 102 | 103 | steps: 104 | - name: Checkout 105 | uses: actions/checkout@v4 106 | with: 107 | fetch-depth: 0 108 | 109 | - name: Set up Helm 110 | uses: azure/setup-helm@v4 111 | with: 112 | version: v3.10.3 113 | 114 | - name: Set up Python 115 | uses: actions/setup-python@v5 116 | with: 117 | python-version: "3.10" 118 | 119 | - name: Set up chart-testing 120 | uses: helm/chart-testing-action@v2.7.0 121 | 122 | - name: Lint 123 | run: ct lint 124 | 125 | - name: Check generated docs 126 | run: | 127 | make docs 128 | test "$(git diff --name-only)" == "" \ 129 | || ( printf >&2 "\nREADME files are not up to date (run 'make docs'), differences:\n\n%s\n\n" "$(git diff)" ; exit 1 ; ) 130 | 131 | - name: Detect changes 132 | id: changes 133 | run: | 134 | changed=$(ct list-changed) 135 | if [[ -n "$changed" ]]; then 136 | echo "changed=true" >> $GITHUB_OUTPUT 137 | fi 138 | 139 | chart-test: 140 | name: Chart Test 141 | runs-on: ubuntu-latest 142 | needs: chart 143 | if: needs.chart.outputs.changed == 'true' 144 | strategy: 145 | fail-fast: false 146 | matrix: 147 | kube: ["1.24", "1.25", "1.26"] 148 | 149 | steps: 150 | - name: Checkout 151 | uses: actions/checkout@v4 152 | with: 153 | fetch-depth: 0 154 | 155 | - name: Set up Helm 156 | uses: azure/setup-helm@v4 157 | with: 158 | version: v3.10.3 159 | 160 | - name: Set up Python 161 | uses: actions/setup-python@v5 162 | with: 163 | python-version: "3.10" 164 | 165 | - name: Set up chart-testing 166 | uses: helm/chart-testing-action@v2.7.0 167 | 168 | # See https://github.com/kubernetes-sigs/kind/releases/tag/v0.17.0 169 | - name: Determine KinD node image version 170 | id: node_image 171 | run: | 172 | case ${{ matrix.kube }} in 173 | 1.24) 174 | NODE_IMAGE=kindest/node:v1.24.7@sha256:577c630ce8e509131eab1aea12c022190978dd2f745aac5eb1fe65c0807eb315 ;; 175 | 1.25) 176 | NODE_IMAGE=kindest/node:v1.25.3@sha256:f52781bc0d7a19fb6c405c2af83abfeb311f130707a0e219175677e366cc45d1 ;; 177 | 1.26) 178 | NODE_IMAGE=kindest/node:v1.26.0@sha256:691e24bd2417609db7e589e1a479b902d2e209892a10ce375fab60a8407c7352 ;; 179 | esac 180 | 181 | echo "image=$NODE_IMAGE" >> $GITHUB_OUTPUT 182 | 183 | - name: Create KinD cluster 184 | uses: helm/kind-action@v1.12.0 185 | with: 186 | version: v0.17.0 187 | node_image: ${{ steps.node_image.outputs.image }} 188 | 189 | - name: Test 190 | run: ct install 191 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/go,intellij+all 3 | # Edit at https://www.gitignore.io/?templates=go,intellij+all 4 | 5 | ### Go ### 6 | # Binaries for programs and plugins 7 | *.exe 8 | *.exe~ 9 | *.dll 10 | *.so 11 | *.dylib 12 | 13 | # Test binary, built with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | # Dependency directories (remove the comment below to include it) 20 | # vendor/ 21 | 22 | ### Go Patch ### 23 | /vendor/ 24 | /Godeps/ 25 | 26 | ### Intellij+all ### 27 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 28 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 29 | 30 | # User-specific stuff 31 | .idea/**/workspace.xml 32 | .idea/**/tasks.xml 33 | .idea/**/usage.statistics.xml 34 | .idea/**/dictionaries 35 | .idea/**/shelf 36 | 37 | # Generated files 38 | .idea/**/contentModel.xml 39 | 40 | # Sensitive or high-churn files 41 | .idea/**/dataSources/ 42 | .idea/**/dataSources.ids 43 | .idea/**/dataSources.local.xml 44 | .idea/**/sqlDataSources.xml 45 | .idea/**/dynamic.xml 46 | .idea/**/uiDesigner.xml 47 | .idea/**/dbnavigator.xml 48 | 49 | # Gradle 50 | .idea/**/gradle.xml 51 | .idea/**/libraries 52 | 53 | # Gradle and Maven with auto-import 54 | # When using Gradle or Maven with auto-import, you should exclude module files, 55 | # since they will be recreated, and may cause churn. Uncomment if using 56 | # auto-import. 57 | # .idea/modules.xml 58 | # .idea/*.iml 59 | # .idea/modules 60 | # *.iml 61 | # *.ipr 62 | 63 | # CMake 64 | cmake-build-*/ 65 | 66 | # Mongo Explorer plugin 67 | .idea/**/mongoSettings.xml 68 | 69 | # File-based project format 70 | *.iws 71 | 72 | # IntelliJ 73 | out/ 74 | 75 | # mpeltonen/sbt-idea plugin 76 | .idea_modules/ 77 | 78 | # JIRA plugin 79 | atlassian-ide-plugin.xml 80 | 81 | # Cursive Clojure plugin 82 | .idea/replstate.xml 83 | 84 | # Crashlytics plugin (for Android Studio and IntelliJ) 85 | com_crashlytics_export_strings.xml 86 | crashlytics.properties 87 | crashlytics-build.properties 88 | fabric.properties 89 | 90 | # Editor-based Rest Client 91 | .idea/httpRequests 92 | 93 | # Android studio 3.1+ serialized cache file 94 | .idea/caches/build_file_checksums.ser 95 | 96 | ### Intellij+all Patch ### 97 | # Ignores the whole .idea folder and all .iml files 98 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 99 | 100 | .idea/ 101 | 102 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 103 | 104 | *.iml 105 | modules.xml 106 | .idea/misc.xml 107 | *.ipr 108 | 109 | # Sonarlint plugin 110 | .idea/sonarlint 111 | 112 | # End of https://www.gitignore.io/api/go,intellij+all -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 10m 3 | 4 | linters: 5 | disable-all: true 6 | enable: 7 | - govet 8 | - revive 9 | - promlinter 10 | - gofmt 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24.3-bullseye as build 2 | 3 | WORKDIR /go/src/app 4 | ADD . /go/src/app 5 | 6 | RUN go get -d -v ./... 7 | 8 | ARG TAG 9 | RUN CGO_ENABLED=0 go build -a \ 10 | -ldflags "-s -w -extldflags '-static' -X github.com/flant/k8s-image-availability-exporter/pkg/version.Version=${TAG}" \ 11 | -o /go/bin/k8s-image-availability-exporter main.go 12 | 13 | FROM gcr.io/distroless/static-debian11 14 | COPY --from=build /go/bin/k8s-image-availability-exporter / 15 | ENTRYPOINT ["/k8s-image-availability-exporter"] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export PATH := $(abspath bin/protoc/bin/):$(abspath bin/):${PATH} 2 | export SHELL := env PATH=$(PATH) /bin/sh 3 | 4 | GOOS?=$(shell go env GOOS) 5 | GOARCH?=$(shell go env GOARCH) 6 | GOLANGCI_VERSION = 1.62.2 7 | HELM_DOCS_VERSION = 1.11.0 8 | 9 | ifeq ($(GOARCH),arm) 10 | ARCH=armv7 11 | else 12 | ARCH=$(GOARCH) 13 | endif 14 | 15 | COMMIT=$(shell git rev-parse --verify HEAD) 16 | 17 | ########### 18 | # BUILDING 19 | ########### 20 | bin/k8s-image-availability-exporter: 21 | GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=0 go build -ldflags "-X github.com/flant/k8s-image-availability-exporter/pkg/version.Version=${COMMIT}" -mod=readonly -o bin/k8s-image-availability-exporter 22 | 23 | build: bin/k8s-image-availability-exporter 24 | 25 | ########### 26 | # LINTING 27 | ########### 28 | bin/golangci-lint: bin/golangci-lint-${GOLANGCI_VERSION} 29 | @ln -sf golangci-lint-${GOLANGCI_VERSION} bin/golangci-lint 30 | 31 | bin/golangci-lint-${GOLANGCI_VERSION}: 32 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | BINARY=golangci-lint bash -s -- v${GOLANGCI_VERSION} 33 | @mv bin/golangci-lint $@ 34 | 35 | ########### 36 | # HELM 37 | ########### 38 | 39 | bin/helm-docs: bin/helm-docs-${HELM_DOCS_VERSION} 40 | @ln -sf helm-docs-${HELM_DOCS_VERSION} bin/helm-docs 41 | bin/helm-docs-${HELM_DOCS_VERSION}: 42 | @mkdir -p bin 43 | curl -L https://github.com/norwoodj/helm-docs/releases/download/v${HELM_DOCS_VERSION}/helm-docs_${HELM_DOCS_VERSION}_$(shell uname)_x86_64.tar.gz | tar -zOxf - helm-docs > ./bin/helm-docs-${HELM_DOCS_VERSION} && chmod +x ./bin/helm-docs-${HELM_DOCS_VERSION} 44 | 45 | .PHONY: lint fix 46 | lint: bin/golangci-lint 47 | bin/golangci-lint run 48 | 49 | fix: bin/golangci-lint 50 | bin/golangci-lint run --fix 51 | 52 | .PHONY: docs 53 | docs: bin/helm-docs 54 | bin/helm-docs -s file -c charts/ -t README.md.gotmpl 55 | 56 | ########### 57 | # TESTING 58 | ########### 59 | test: 60 | go test -race -cover -v ./... 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # k8s-image-availability-exporter 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/flant/k8s-image-availability-exporter.svg)](https://pkg.go.dev/github.com/flant/k8s-image-availability-exporter) 4 | [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/k8s-image-availability-exporter)](https://artifacthub.io/packages/search?repo=k8s-image-availability-exporter) 5 | 6 | k8s-image-availability-exporter (or *k8s-iae* for short) is a Prometheus exporter that warns you proactively about images that are defined in Kubernetes objects (e.g., an `image` field in the Deployment) but are not available in the container registry (such as Docker Registry, etc.). 7 | 8 | Receiving alerts when container images related to running Kubernetes controllers are missing helps you to solve the problem before it manifests itself. For more details on the reasons behind k8s-iae and how it works, please read [this article](https://medium.com/flant-com/prometheus-exporter-to-check-kubernetes-images-availability-26c306c44c08). 9 | 10 | * [Deploying / Installing](#deploying) k8s-iae in your Kubernetes cluster 11 | * [Prometheus integration](#prometheus-integration) to scrape metrics 12 | * [Alerting](#alerting) based on k8s-iae metrics 13 | * [Configuration](#configuration) 14 | * [CLI options](#command-line-options) 15 | * [Metrics](#metrics) for Prometheus provided by k8s-iae 16 | * [Compatibility](#compatibility) 17 | 18 | ## Deploying 19 | 20 | ### Container image 21 | 22 | Ready-to-use container images are available in the Deckhouse registry: 23 | 24 | ```bash 25 | docker pull registry.deckhouse.io/k8s-image-availability-exporter/k8s-image-availability-exporter:latest 26 | ``` 27 | 28 | ### Helm Chart 29 | 30 | The helm chart is available on [artifacthub](https://artifacthub.io/packages/helm/k8s-image-availability-exporter/k8s-image-availability-exporter). Follow instructions on the page to install it. 31 | 32 | ### Prometheus integration 33 | 34 | Here's how you can configure Prometheus or prometheus-operator to scrape metrics from `k8s-image-availability-exporter`. 35 | 36 | #### Prometheus 37 | 38 | ```yaml 39 | - job_name: image-availability-exporter 40 | honor_labels: true 41 | metrics_path: '/metrics' 42 | scheme: http 43 | kubernetes_sd_configs: 44 | - role: pod 45 | namespaces: 46 | names: 47 | - kube-system 48 | relabel_configs: 49 | - source_labels: [__meta_kubernetes_pod_label_app] 50 | regex: image-availability-exporter 51 | action: keep 52 | ``` 53 | 54 | #### prometheus-operator 55 | 56 | ```yaml 57 | apiVersion: monitoring.coreos.com/v1 58 | kind: PodMonitor 59 | metadata: 60 | name: image-availability-exporter 61 | namespace: kube-system 62 | spec: 63 | podMetricsEndpoints: 64 | - port: http-metrics 65 | scheme: http 66 | honorLabels: true 67 | scrapeTimeout: 10s 68 | selector: 69 | matchLabels: 70 | app: image-availability-exporter 71 | namespaceSelector: 72 | matchNames: 73 | - kube-system 74 | ``` 75 | 76 | ### Alerting 77 | 78 | Here's how to alert based on these metrics: 79 | 80 | #### Prometheus 81 | 82 | ```yaml 83 | groups: 84 | - alert: DeploymentImageUnavailable 85 | expr: | 86 | max by (namespace, name, container, image) ( 87 | k8s_image_availability_exporter_available{kind="deployment"} == 0 88 | ) 89 | annotations: 90 | message: > 91 | Image {{`{{ $labels.image }}`}} from container {{`{{ $labels.container }}`}} 92 | in deployment {{`{{ $labels.name }}`}} 93 | from namespace {{`{{ $labels.namespace }}`}} 94 | is not available in docker registry. 95 | labels: 96 | severity: critical 97 | - alert: StatefulSetImageUnavailable 98 | expr: | 99 | max by (namespace, name, container, image) ( 100 | k8s_image_availability_exporter_available{kind="statefulset"} == 0 101 | ) 102 | annotations: 103 | message: > 104 | Image {{`{{ $labels.image }}`}} from container {{`{{ $labels.container }}`}} 105 | in statefulSet {{`{{ $labels.name }}`}} 106 | from namespace {{`{{ $labels.namespace }}`}} 107 | is not available in docker registry. 108 | labels: 109 | severity: critical 110 | - alert: DaemonSetImageUnavailable 111 | expr: | 112 | max by (namespace, name, container, image) ( 113 | k8s_image_availability_exporter_available{kind="daemonset"} == 0 114 | ) 115 | annotations: 116 | message: > 117 | Image {{`{{ $labels.image }}`}} from container {{`{{ $labels.container }}`}} 118 | in daemonSet {{`{{ $labels.name }}`}} 119 | from namespace {{`{{ $labels.namespace }}`}} 120 | is not available in docker registry. 121 | labels: 122 | severity: critical 123 | - alert: CronJobImageUnavailable 124 | expr: | 125 | max by (namespace, name, container, image) ( 126 | k8s_image_availability_exporter_available{kind="cronjob"} == 0 127 | ) 128 | annotations: 129 | message: > 130 | Image {{`{{ $labels.image }}`}} from container {{`{{ $labels.container }}`}} 131 | in cronJob {{`{{ $labels.name }}`}} 132 | from namespace {{`{{ $labels.namespace }}`}} 133 | is not available in docker registry. 134 | labels: 135 | severity: critical 136 | ``` 137 | 138 | ## Configuration 139 | 140 | ### Command-line options 141 | 142 | ``` 143 | Usage of k8s-image-availability-exporter: 144 | -allow-plain-http 145 | whether to fallback to HTTP scheme for registries that don't support HTTPS 146 | -bind-address string 147 | address:port to bind /metrics endpoint to (default ":8080") 148 | -capath value 149 | path to a file that contains CA certificates in the PEM format 150 | -check-interval duration 151 | image re-check interval (default 1m0s) 152 | -default-registry string 153 | default registry to use in absence of a fully qualified image name, defaults to "index.docker.io" 154 | -force-check-disabled-controllers value 155 | comma-separated list of controller kinds for which image is forcibly checked, even when workloads are disabled or suspended. Acceptable values include "Deployment", "StatefulSet", "DaemonSet", "Cronjob" or "*" for all kinds (this option is case-insensitive) 156 | -ignored-images string 157 | tilde-separated image regexes to ignore, each image will be checked against this list of regexes 158 | -image-mirror value 159 | Add a mirror repository (format: original=mirror) 160 | -namespace-label string 161 | namespace label for checks 162 | -skip-registry-cert-verification 163 | whether to skip registries' certificate verification 164 | ``` 165 | 166 | ## Metrics 167 | 168 | The following metrics for Prometheus are provided: 169 | 170 | * `k8s_image_availability_exporter_available` — non-zero indicates *successful* image check. 171 | * `k8s_image_availability_exporter_absent` — non-zero indicates an image's manifest absence from container registry. 172 | * `k8s_image_availability_exporter_bad_image_format` — non-zero indicates incorrect `image` field format. 173 | * `k8s_image_availability_exporter_registry_unavailable` — non-zero indicates general registry unavailiability, perhaps, due to network outage. 174 | * `k8s_image_availability_exporter_authentication_failure` — non-zero indicates authentication error to container registry, verify imagePullSecrets. 175 | * `k8s_image_availability_exporter_authorization_failure` — non-zero indicates authorization error to container registry, verify imagePullSecrets. 176 | * `k8s_image_availability_exporter_unknown_error` — non-zero indicates an error that failed to be classified, consult exporter's logs for additional information. 177 | 178 | Each metric has the following labels: 179 | 180 | * `namespace` - namespace name 181 | * `container` - container name 182 | * `image` - image URL in the registry 183 | * `kind` - Kubernetes controller kind, namely `deployment`, `statefulset`, `daemonset` or `cronjob` 184 | * `name` - controller name 185 | 186 | ## Compatibility 187 | 188 | k8s-image-availability-exporter is compatible with Kubernetes 1.15+ and Docker Registry V2 compliant container registries. 189 | 190 | Since the exporter operates as a Deployment, it *does not* support container registries that should be accessed via authorization on a node. 191 | -------------------------------------------------------------------------------- /charts/cr.yaml: -------------------------------------------------------------------------------- 1 | owner: deckhouse 2 | git-base-url: https://api.github.com/ 3 | -------------------------------------------------------------------------------- /charts/k8s-image-availability-exporter/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "0.11.0" 3 | description: Application for monitoring the cluster workloads image presence in a container registry. 4 | name: k8s-image-availability-exporter 5 | version: "0.17.0" 6 | kubeVersion: ">=1.14.0-0" 7 | maintainers: 8 | - name: nabokihms 9 | email: max.nabokih@gmail.com 10 | url: github.com/nabokihms 11 | sources: 12 | - https://github.com/deckhouse/k8s-image-availability-exporter 13 | keywords: 14 | - kubernetes 15 | - prometheus 16 | - prometheus-exporter 17 | - container-registry 18 | - monitoring 19 | -------------------------------------------------------------------------------- /charts/k8s-image-availability-exporter/README.md: -------------------------------------------------------------------------------- 1 | # k8s-image-availability-exporter 2 | 3 | ![Version: 0.17.0](https://img.shields.io/badge/Version-0.17.0-informational?style=flat-square) ![AppVersion: 0.11.0](https://img.shields.io/badge/AppVersion-0.11.0-informational?style=flat-square) 4 | 5 | Application for monitoring the cluster workloads image presence in a container registry. 6 | 7 | ## Requirements 8 | 9 | Kubernetes: `>=1.14.0-0` 10 | 11 | ## Introduction 12 | 13 | This chart bootstraps a [k8s-image-availability-exporter](https://github.com/flant/k8s-image-availability-exporter) deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager. 14 | 15 | ## Maintainers 16 | 17 | | Name | Email | Url | 18 | | ---- | ------ | --- | 19 | | nabokihms | | | 20 | 21 | ## Values 22 | 23 | | Key | Type | Default | Description | 24 | |-----|------|---------|-------------| 25 | | k8sImageAvailabilityExporter.image.repository | string | `"registry.deckhouse.io/k8s-image-availability-exporter/k8s-image-availability-exporter"` | Repository to use for the k8s-image-availability-exporter deployment | 26 | | k8sImageAvailabilityExporter.image.tag | string | `""` | Image tag override for the default value (chart appVersion) | 27 | | k8sImageAvailabilityExporter.image.pullPolicy | string | `"IfNotPresent"` | Image pull policy to use for the k8s-image-availability-exporter deployment | 28 | | k8sImageAvailabilityExporter.args | list | `["--bind-address=:8080"]` | Command line arguments for the exporter | 29 | | k8sImageAvailabilityExporter.useSecretsForPrivateRepositories | bool | `true` | Setting this to false will prevent k8s-iae having unconstrained cluster-wide secret access | 30 | | annotations | object | `{}` | additional annotations for deployment | 31 | | podAnnotations | object | `{}` | additional annotations added to the pod | 32 | | replicaCount | int | `1` | Number of replicas (pods) to launch. | 33 | | imagePullSecrets | list | `[]` | Reference to one or more secrets to be used when [pulling images](https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/#create-a-pod-that-uses-your-secret) (from private registries). | 34 | | podSecurityContext | object | `{}` | Pod [security context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod). See the [API reference](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#security-context) for details. | 35 | | securityContext | object | `{}` | Container [security context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container). See the [API reference](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#security-context-1) for details. | 36 | | nodeSelector | object | `{}` | [Node selector](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector) configuration. | 37 | | tolerations | list | `[]` | [Tolerations](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) for node taints. See the [API reference](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling) for details. | 38 | | affinity | object | `{}` | [Affinity](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity) configuration. See the [API reference](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling) for details. | 39 | | topologySpreadConstraints | list | `[]` | [TopologySpreadConstraints](https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/) configuration. See the [API reference](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling) for details. | 40 | | strategy | object | `{}` | Deployment [strategy](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy) configuration. | 41 | | revisionHistoryLimit | int | `10` | Define the [count of deployment revisions](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#clean-up-policy) to be kept. May be set to 0 in case of GitOps deployment approach. | 42 | | volumes | list | `[]` | Additional storage [volumes](https://kubernetes.io/docs/concepts/storage/volumes/). See the [API reference](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#volumes-1) for details. | 43 | | volumeMounts | list | `[]` | Additional [volume mounts](https://kubernetes.io/docs/tasks/configure-pod-container/configure-volume-storage/). See the [API reference](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#volumes-1) for details. | 44 | | podDisruptionBudget.enabled | bool | `false` | Enable a [pod distruption budget](https://kubernetes.io/docs/tasks/run-application/configure-pdb/) to help dealing with [disruptions](https://kubernetes.io/docs/concepts/workloads/pods/disruptions/). It is **highly recommended** for webhooks as disruptions can prevent launching new pods. | 45 | | podDisruptionBudget.minAvailable | int/percentage | `nil` | Number or percentage of pods that must remain available. | 46 | | podDisruptionBudget.maxUnavailable | int/percentage | `nil` | Number or percentage of pods that can be unavailable. | 47 | | priorityClassName | string | `""` | Specify a priority class name to set [pod priority](https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/#pod-priority). | 48 | | resources | object | No requests or limits. | Container resource [requests and limits](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/). See the [API reference](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#resources) for details. | 49 | | serviceMonitor.enabled | bool | `false` | Enable Prometheus ServiceMonitor. See the [documentation](https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/design.md#servicemonitor) and the [API reference](https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/api.md#servicemonitor) for details. | 50 | | serviceMonitor.namespace | string | Release namespace. | Namespace where the ServiceMonitor resource should be deployed. | 51 | | serviceMonitor.interval | duration | `"15s"` | Prometheus scrape interval. | 52 | | serviceMonitor.scrapeTimeout | duration | `nil` | Prometheus scrape timeout. | 53 | | serviceMonitor.labels | object | `{}` | Labels to be added to the ServiceMonitor. # ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#prometheusspec | 54 | | serviceMonitor.annotations | object | `{}` | Annotations to be added to the ServiceMonitor. # ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#prometheusspec | 55 | | serviceMonitor.scheme | string | `""` | HTTP scheme to use for scraping. Can be used with `tlsConfig` for example if using istio mTLS. | 56 | | serviceMonitor.path | string | `"/metrics"` | HTTP path to scrape for metrics. | 57 | | serviceMonitor.tlsConfig | object | `{}` | TLS configuration to use when scraping the endpoint. For example if using istio mTLS. # Of type: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#tlsconfig | 58 | | serviceMonitor.honorLabels | bool | `false` | HonorLabels chooses the metric's labels on collisions with target labels. | 59 | | serviceMonitor.metricRelabelings | list | `[]` | Prometheus scrape metric relabel configs to apply to samples before ingestion. # [Metric Relabeling](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#metric_relabel_configs) | 60 | | serviceMonitor.relabelings | list | `[]` | Relabel configs to apply to samples before ingestion. # [Relabeling](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config) | 61 | | prometheusRule.enabled | bool | `false` | Create [Prometheus Operator](https://github.com/coreos/prometheus-operator) prometheusRule resource | 62 | | prometheusRule.defaultGroupsEnabled | bool | `true` | Setup default alerts (works only if prometheusRule.enabled is set to true) | 63 | | prometheusRule.additionalGroups | list | `[]` | Additional PrometheusRule groups | 64 | 65 | Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example, 66 | 67 | ```bash 68 | helm install my-release k8s-image-availability-exporter --set k8sImageAvailabilityExporter.replicas=2 69 | ``` 70 | 71 | Alternatively, one or more YAML files that specify the values for the above parameters can be provided while installing the chart. For example, 72 | 73 | ```bash 74 | helm install my-release k8s-image-availability-exporter -f values1.yaml,values2.yaml 75 | ``` 76 | -------------------------------------------------------------------------------- /charts/k8s-image-availability-exporter/README.md.gotmpl: -------------------------------------------------------------------------------- 1 | {{ template "chart.header" . }} 2 | 3 | {{ template "chart.badgesSection" . }} 4 | 5 | {{ template "chart.description" . }} 6 | 7 | {{ template "chart.requirementsSection" . }} 8 | 9 | {{ template "chart.deprecationWarning" . }} 10 | 11 | ## Introduction 12 | 13 | This chart bootstraps a [k8s-image-availability-exporter](https://github.com/flant/k8s-image-availability-exporter) deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager. 14 | 15 | {{ template "chart.homepageLine" . }} 16 | 17 | {{ template "chart.maintainersSection" . }} 18 | 19 | {{ template "chart.valuesSection" . }} 20 | 21 | Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example, 22 | 23 | ```bash 24 | helm install my-release k8s-image-availability-exporter --set k8sImageAvailabilityExporter.replicas=2 25 | ``` 26 | 27 | Alternatively, one or more YAML files that specify the values for the above parameters can be provided while installing the chart. For example, 28 | 29 | ```bash 30 | helm install my-release k8s-image-availability-exporter -f values1.yaml,values2.yaml 31 | ``` 32 | -------------------------------------------------------------------------------- /charts/k8s-image-availability-exporter/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | 3 | {{/* 4 | Expand the name of the chart. 5 | */}} 6 | {{- define "k8s-image-availability-exporter.name" -}} 7 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 8 | {{- end -}} 9 | 10 | {{/* 11 | Create a default fully qualified app name. 12 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 13 | And depending on the resources the name is completed with an extension. 14 | If release name contains chart name it will be used as a full name. 15 | */}} 16 | {{- define "k8s-image-availability-exporter.fullname" -}} 17 | {{- if .Values.fullnameOverride -}} 18 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 19 | {{- else -}} 20 | {{- $name := default .Chart.Name .Values.nameOverride -}} 21 | {{- if contains $name .Release.Name -}} 22 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 23 | {{- else -}} 24 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 25 | {{- end -}} 26 | {{- end -}} 27 | {{- end -}} 28 | 29 | {{/* 30 | Create chart name and version as used by the chart label. 31 | */}} 32 | {{- define "k8s-image-availability-exporter.chart" -}} 33 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 34 | {{- end -}} 35 | -------------------------------------------------------------------------------- /charts/k8s-image-availability-exporter/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ template "k8s-image-availability-exporter.fullname" . }} 5 | {{- with .Values.annotations }} 6 | annotations: {{- toYaml . | nindent 4 }} 7 | {{- end }} 8 | labels: 9 | helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" 10 | app.kubernetes.io/name: "{{ template "k8s-image-availability-exporter.fullname" . }}" 11 | app.kubernetes.io/instance: {{ .Release.Name | quote }} 12 | app.kubernetes.io/managed-by: {{ .Release.Service | quote }} 13 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 14 | app.kubernetes.io/component: monitoring 15 | spec: 16 | replicas: {{ .Values.replicaCount }} 17 | revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} 18 | {{- with .Values.strategy }} 19 | strategy: 20 | {{- toYaml . | nindent 4 }} 21 | {{- end }} 22 | selector: 23 | matchLabels: 24 | app: {{ template "k8s-image-availability-exporter.fullname" . }} 25 | template: 26 | metadata: 27 | {{- with .Values.podAnnotations }} 28 | annotations: {{- toYaml . | nindent 8 }} 29 | {{- end }} 30 | labels: 31 | app: {{ template "k8s-image-availability-exporter.fullname" . }} 32 | spec: 33 | {{- with .Values.priorityClassName }} 34 | priorityClassName: {{ . | quote }} 35 | {{- end }} 36 | containers: 37 | - name: k8s-image-availability-exporter 38 | {{- if .Values.k8sImageAvailabilityExporter.args }} 39 | args: 40 | {{- range .Values.k8sImageAvailabilityExporter.args }} 41 | - {{ . }} 42 | {{- end }} 43 | {{- end }} 44 | {{- if .Values.k8sImageAvailabilityExporter.env }} 45 | env: 46 | {{- range .Values.k8sImageAvailabilityExporter.env }} 47 | - name: {{ .name }} 48 | value: {{ .value }} 49 | {{- end }} 50 | {{- end }} 51 | ports: 52 | - containerPort: 8080 53 | name: http 54 | image: {{ .Values.k8sImageAvailabilityExporter.image.repository }}:{{ .Values.k8sImageAvailabilityExporter.image.tag | default (printf "v%s" .Chart.AppVersion) }} 55 | imagePullPolicy: {{ .Values.k8sImageAvailabilityExporter.image.imagePullPolicy }} 56 | securityContext: 57 | {{- toYaml .Values.securityContext | nindent 12 }} 58 | livenessProbe: 59 | httpGet: 60 | path: /healthz 61 | port: http 62 | scheme: HTTP 63 | readinessProbe: 64 | httpGet: 65 | path: /healthz 66 | port: http 67 | scheme: HTTP 68 | resources: 69 | {{- toYaml .Values.resources | nindent 12 }} 70 | {{- if gt (len .Values.volumeMounts) 0 }} 71 | {{- with .Values.volumeMounts }} 72 | volumeMounts: 73 | {{- toYaml . | nindent 12 }} 74 | {{- end }} 75 | {{- end }} 76 | {{- if gt (len .Values.volumes) 0 }} 77 | {{- with .Values.volumes }} 78 | volumes: 79 | {{- toYaml . | nindent 8 }} 80 | {{- end }} 81 | {{- end }} 82 | serviceAccountName: {{ template "k8s-image-availability-exporter.fullname" . }} 83 | securityContext: 84 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 85 | {{- with .Values.nodeSelector }} 86 | nodeSelector: 87 | {{- toYaml . | nindent 8 }} 88 | {{- end }} 89 | {{- with .Values.affinity }} 90 | affinity: 91 | {{- toYaml . | nindent 8 }} 92 | {{- end }} 93 | {{- with .Values.topologySpreadConstraints }} 94 | topologySpreadConstraints: 95 | {{- toYaml . | nindent 8 }} 96 | {{- end }} 97 | {{- with .Values.tolerations }} 98 | tolerations: 99 | {{- toYaml . | nindent 8 }} 100 | {{- end }} 101 | {{- with .Values.imagePullSecrets }} 102 | imagePullSecrets: 103 | {{- toYaml . | nindent 8 }} 104 | {{- end }} 105 | -------------------------------------------------------------------------------- /charts/k8s-image-availability-exporter/templates/poddisruptionbudget.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.podDisruptionBudget.enabled }} 2 | {{- if semverCompare ">=1.21-0" .Capabilities.KubeVersion.GitVersion -}} 3 | apiVersion: policy/v1 4 | {{- else -}} 5 | apiVersion: policy/v1beta1 6 | {{- end }} 7 | kind: PodDisruptionBudget 8 | metadata: 9 | name: {{ template "k8s-image-availability-exporter.fullname" . }} 10 | labels: 11 | app: {{ template "k8s-image-availability-exporter.fullname" . }} 12 | spec: 13 | {{- with .Values.podDisruptionBudget.minAvailable }} 14 | minAvailable: {{ . }} 15 | {{- end }} 16 | {{- with .Values.podDisruptionBudget.maxUnavailable }} 17 | maxUnavailable: {{ . }} 18 | {{- end }} 19 | selector: 20 | matchLabels: 21 | app: {{ template "k8s-image-availability-exporter.fullname" . }} 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /charts/k8s-image-availability-exporter/templates/prometheus-rule.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.prometheusRule.enabled }} 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: PrometheusRule 4 | metadata: 5 | name: {{ template "k8s-image-availability-exporter.fullname" . }} 6 | spec: 7 | groups: 8 | {{- if .Values.prometheusRule.defaultGroupsEnabled }} 9 | - name: k8s-image-availability-exporter.rules 10 | rules: 11 | - alert: DeploymentImageUnavailable 12 | expr: | 13 | max by (namespace, name, container, image) ( 14 | k8s_image_availability_exporter_available{kind="deployment"} == 0 15 | ) 16 | annotations: 17 | message: > 18 | Image {{`{{ $labels.image }}`}} from container {{`{{ $labels.container }}`}} 19 | in deployment {{`{{ $labels.name }}`}} 20 | from namespace {{`{{ $labels.namespace }}`}} 21 | is not available in docker registry. 22 | labels: 23 | severity: critical 24 | - alert: StatefulSetImageUnavailable 25 | expr: | 26 | max by (namespace, name, container, image) ( 27 | k8s_image_availability_exporter_available{kind="statefulset"} == 0 28 | ) 29 | annotations: 30 | message: > 31 | Image {{`{{ $labels.image }}`}} from container {{`{{ $labels.container }}`}} 32 | in statefulSet {{`{{ $labels.name }}`}} 33 | from namespace {{`{{ $labels.namespace }}`}} 34 | is not available in docker registry. 35 | labels: 36 | severity: critical 37 | - alert: DaemonSetImageUnavailable 38 | expr: | 39 | max by (namespace, name, container, image) ( 40 | k8s_image_availability_exporter_available{kind="daemonset"} == 0 41 | ) 42 | annotations: 43 | message: > 44 | Image {{`{{ $labels.image }}`}} from container {{`{{ $labels.container }}`}} 45 | in daemonSet {{`{{ $labels.name }}`}} 46 | from namespace {{`{{ $labels.namespace }}`}} 47 | is not available in docker registry. 48 | labels: 49 | severity: critical 50 | - alert: CronJobImageUnavailable 51 | expr: | 52 | max by (namespace, name, container, image) ( 53 | k8s_image_availability_exporter_available{kind="cronjob"} == 0 54 | ) 55 | annotations: 56 | message: > 57 | Image {{`{{ $labels.image }}`}} from container {{`{{ $labels.container }}`}} 58 | in cronJob {{`{{ $labels.name }}`}} 59 | from namespace {{`{{ $labels.namespace }}`}} 60 | is not available in docker registry. 61 | labels: 62 | severity: critical 63 | {{- end }} 64 | 65 | {{- if .Values.prometheusRule.additionalGroups }} 66 | {{ .Values.prometheusRule.additionalGroups | toYaml | indent 2}} 67 | {{- end }} 68 | 69 | {{- end }} 70 | -------------------------------------------------------------------------------- /charts/k8s-image-availability-exporter/templates/rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: {{ template "k8s-image-availability-exporter.fullname" . }} 5 | rules: 6 | {{- if .Values.k8sImageAvailabilityExporter.useSecretsForPrivateRepositories }} 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - secrets 11 | verbs: 12 | - list 13 | - watch 14 | - get 15 | {{- end }} 16 | - apiGroups: 17 | - "" 18 | resources: 19 | - namespaces 20 | verbs: 21 | - list 22 | - watch 23 | - get 24 | - apiGroups: 25 | - "" 26 | resources: 27 | - serviceaccounts 28 | verbs: 29 | - get 30 | - list 31 | - watch 32 | - apiGroups: 33 | - extensions 34 | - apps 35 | resources: 36 | - deployments 37 | - daemonsets 38 | - statefulsets 39 | verbs: 40 | - list 41 | - watch 42 | - get 43 | - apiGroups: 44 | - batch 45 | resources: 46 | - cronjobs 47 | verbs: 48 | - list 49 | - watch 50 | - get 51 | --- 52 | apiVersion: rbac.authorization.k8s.io/v1 53 | kind: ClusterRoleBinding 54 | metadata: 55 | name: {{ template "k8s-image-availability-exporter.fullname" . }} 56 | roleRef: 57 | apiGroup: rbac.authorization.k8s.io 58 | kind: ClusterRole 59 | name: {{ template "k8s-image-availability-exporter.fullname" . }} 60 | subjects: 61 | - kind: ServiceAccount 62 | name: {{ template "k8s-image-availability-exporter.fullname" . }} 63 | namespace: {{ .Release.Namespace }} 64 | -------------------------------------------------------------------------------- /charts/k8s-image-availability-exporter/templates/service-account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: {{ template "k8s-image-availability-exporter.fullname" . }} 5 | -------------------------------------------------------------------------------- /charts/k8s-image-availability-exporter/templates/service-monitor.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceMonitor.enabled }} 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | {{- with .Values.serviceMonitor.annotations }} 6 | annotations: 7 | {{- toYaml . | nindent 4 }} 8 | {{- end }} 9 | name: {{ template "k8s-image-availability-exporter.fullname" . }} 10 | {{- with .Values.serviceMonitor.namespace }} 11 | namespace: {{ . }} 12 | {{- end }} 13 | labels: 14 | app: {{ template "k8s-image-availability-exporter.fullname" . }} 15 | {{- with .Values.serviceMonitor.labels }} 16 | {{- toYaml . | nindent 4 }} 17 | {{- end }} 18 | spec: 19 | endpoints: 20 | - port: http 21 | {{- with .Values.serviceMonitor.interval }} 22 | interval: {{ . }} 23 | {{- end }} 24 | {{- with .Values.serviceMonitor.scheme }} 25 | scheme: {{ . }} 26 | {{- end }} 27 | {{- with .Values.serviceMonitor.bearerTokenFile }} 28 | bearerTokenFile: {{ . }} 29 | {{- end }} 30 | {{- with .Values.serviceMonitor.tlsConfig }} 31 | tlsConfig: 32 | {{- toYaml .| nindent 6 }} 33 | {{- end }} 34 | {{- with .Values.serviceMonitor.scrapeTimeout }} 35 | scrapeTimeout: {{ . }} 36 | {{- end }} 37 | path: {{ .Values.serviceMonitor.path }} 38 | honorLabels: {{ .Values.serviceMonitor.honorLabels }} 39 | {{- with .Values.serviceMonitor.metricRelabelings }} 40 | metricRelabelings: 41 | {{- tpl (toYaml . | nindent 6) $ }} 42 | {{- end }} 43 | {{- with .Values.serviceMonitor.relabelings }} 44 | relabelings: 45 | {{- toYaml . | nindent 6 }} 46 | {{- end }} 47 | jobLabel: {{ template "k8s-image-availability-exporter.fullname" . }} 48 | selector: 49 | matchLabels: 50 | app: {{ template "k8s-image-availability-exporter.fullname" . }} 51 | namespaceSelector: 52 | matchNames: 53 | - {{ .Release.Namespace }} 54 | {{- end }} 55 | -------------------------------------------------------------------------------- /charts/k8s-image-availability-exporter/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ template "k8s-image-availability-exporter.fullname" . }} 5 | labels: 6 | app: {{ template "k8s-image-availability-exporter.fullname" . }} 7 | spec: 8 | ports: 9 | - name: http 10 | port: 8080 11 | protocol: TCP 12 | targetPort: http 13 | selector: 14 | app: {{ template "k8s-image-availability-exporter.fullname" . }} 15 | -------------------------------------------------------------------------------- /charts/k8s-image-availability-exporter/values.yaml: -------------------------------------------------------------------------------- 1 | k8sImageAvailabilityExporter: 2 | image: 3 | # -- Repository to use for the k8s-image-availability-exporter deployment 4 | repository: registry.deckhouse.io/k8s-image-availability-exporter/k8s-image-availability-exporter 5 | # -- Image tag override for the default value (chart appVersion) 6 | tag: "" 7 | # -- Image pull policy to use for the k8s-image-availability-exporter deployment 8 | pullPolicy: IfNotPresent 9 | # -- Command line arguments for the exporter 10 | args: 11 | - --bind-address=:8080 12 | 13 | # -- Setting this to false will prevent k8s-iae having unconstrained cluster-wide secret access 14 | useSecretsForPrivateRepositories: true 15 | 16 | # -- additional annotations for deployment 17 | annotations: {} 18 | 19 | # -- additional annotations added to the pod 20 | podAnnotations: {} 21 | 22 | # -- Number of replicas (pods) to launch. 23 | replicaCount: 1 24 | 25 | # -- Reference to one or more secrets to be used when [pulling images](https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/#create-a-pod-that-uses-your-secret) (from private registries). 26 | imagePullSecrets: [] 27 | 28 | # -- Pod [security context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod). 29 | # See the [API reference](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#security-context) for details. 30 | podSecurityContext: {} 31 | # fsGroup: 2000 32 | 33 | # -- Container [security context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container). 34 | # See the [API reference](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#security-context-1) for details. 35 | securityContext: {} 36 | # capabilities: 37 | # drop: 38 | # - ALL 39 | # readOnlyRootFilesystem: true 40 | # runAsNonRoot: true 41 | # runAsUser: 1000 42 | 43 | # -- [Node selector](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector) configuration. 44 | nodeSelector: {} 45 | 46 | # -- [Tolerations](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) for node taints. 47 | # See the [API reference](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling) for details. 48 | tolerations: [] 49 | 50 | # -- [Affinity](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity) configuration. 51 | # See the [API reference](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling) for details. 52 | affinity: {} 53 | 54 | # -- [TopologySpreadConstraints](https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/) configuration. 55 | # See the [API reference](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling) for details. 56 | topologySpreadConstraints: [] 57 | 58 | # -- Deployment [strategy](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy) configuration. 59 | strategy: {} 60 | # rollingUpdate: 61 | # maxUnavailable: 1 62 | # type: RollingUpdate 63 | 64 | # -- Define the [count of deployment revisions](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#clean-up-policy) to be kept. 65 | # May be set to 0 in case of GitOps deployment approach. 66 | revisionHistoryLimit: 10 67 | 68 | # -- Additional storage [volumes](https://kubernetes.io/docs/concepts/storage/volumes/). 69 | # See the [API reference](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#volumes-1) for details. 70 | volumes: [] 71 | 72 | # -- Additional [volume mounts](https://kubernetes.io/docs/tasks/configure-pod-container/configure-volume-storage/). 73 | # See the [API reference](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#volumes-1) for details. 74 | volumeMounts: [] 75 | 76 | podDisruptionBudget: 77 | # -- Enable a [pod distruption budget](https://kubernetes.io/docs/tasks/run-application/configure-pdb/) to help dealing with [disruptions](https://kubernetes.io/docs/concepts/workloads/pods/disruptions/). 78 | # It is **highly recommended** for webhooks as disruptions can prevent launching new pods. 79 | enabled: false 80 | 81 | # -- (int/percentage) Number or percentage of pods that must remain available. 82 | minAvailable: 83 | 84 | # -- (int/percentage) Number or percentage of pods that can be unavailable. 85 | maxUnavailable: 86 | 87 | # -- Specify a priority class name to set [pod priority](https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/#pod-priority). 88 | priorityClassName: "" 89 | 90 | # -- Container resource [requests and limits](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/). 91 | # See the [API reference](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#resources) for details. 92 | # @default -- No requests or limits. 93 | resources: {} 94 | # We usually recommend not to specify default resources and to leave this as a conscious 95 | # choice for the user. This also increases chances charts run on environments with little 96 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 97 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 98 | # limits: 99 | # cpu: 100m 100 | # memory: 128Mi 101 | # requests: 102 | # cpu: 100m 103 | # memory: 128Mi 104 | 105 | serviceMonitor: 106 | # -- Enable Prometheus ServiceMonitor. 107 | # See the [documentation](https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/design.md#servicemonitor) and the [API reference](https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/api.md#servicemonitor) for details. 108 | enabled: false 109 | 110 | # -- Namespace where the ServiceMonitor resource should be deployed. 111 | # @default -- Release namespace. 112 | namespace: "" 113 | 114 | # -- (duration) Prometheus scrape interval. 115 | interval: 15s 116 | 117 | # -- (duration) Prometheus scrape timeout. 118 | scrapeTimeout: 119 | 120 | # -- Labels to be added to the ServiceMonitor. 121 | ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#prometheusspec 122 | labels: {} 123 | 124 | # -- Annotations to be added to the ServiceMonitor. 125 | ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#prometheusspec 126 | annotations: {} 127 | 128 | # -- HTTP scheme to use for scraping. 129 | # Can be used with `tlsConfig` for example if using istio mTLS. 130 | scheme: "" 131 | 132 | # -- HTTP path to scrape for metrics. 133 | path: /metrics 134 | 135 | # -- TLS configuration to use when scraping the endpoint. 136 | # For example if using istio mTLS. 137 | ## Of type: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#tlsconfig 138 | tlsConfig: {} 139 | 140 | # -- HonorLabels chooses the metric's labels on collisions with target labels. 141 | honorLabels: false 142 | 143 | # -- Prometheus scrape metric relabel configs 144 | # to apply to samples before ingestion. 145 | ## [Metric Relabeling](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#metric_relabel_configs) 146 | metricRelabelings: [] 147 | # - action: keep 148 | # regex: 'kube_(daemonset|deployment|pod|namespace|node|statefulset).+' 149 | # sourceLabels: [__name__] 150 | 151 | # -- Relabel configs to apply 152 | # to samples before ingestion. 153 | ## [Relabeling](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config) 154 | relabelings: [] 155 | # - sourceLabels: [__meta_kubernetes_pod_node_name] 156 | # separator: ; 157 | # regex: ^(.*)$ 158 | # targetLabel: nodename 159 | # replacement: $1 160 | # action: replace 161 | 162 | prometheusRule: 163 | # -- Create [Prometheus Operator](https://github.com/coreos/prometheus-operator) prometheusRule resource 164 | enabled: false 165 | # -- Setup default alerts (works only if prometheusRule.enabled is set to true) 166 | defaultGroupsEnabled: true 167 | # -- Additional PrometheusRule groups 168 | additionalGroups: [] 169 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/flant/k8s-image-availability-exporter 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.2 6 | 7 | require ( 8 | github.com/aws/aws-node-termination-handler v1.25.0 9 | github.com/aws/aws-sdk-go-v2/config v1.29.14 10 | github.com/aws/aws-sdk-go-v2/service/ecr v1.44.0 11 | github.com/gammazero/deque v0.2.1 12 | github.com/google/go-containerregistry v0.20.3 13 | github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20240129192428-8dadbe76ff8c 14 | github.com/prometheus/client_golang v1.22.0 15 | github.com/sirupsen/logrus v1.9.3 16 | github.com/stretchr/testify v1.10.0 17 | k8s.io/api v0.32.3 18 | k8s.io/apimachinery v0.32.3 19 | k8s.io/client-go v0.32.3 20 | k8s.io/sample-controller v0.32.3 21 | sigs.k8s.io/controller-runtime v0.20.4 22 | ) 23 | 24 | require ( 25 | github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect 26 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect 27 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect 28 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect 29 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect 30 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect 31 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect 32 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect 33 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect 34 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect 35 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect 36 | github.com/aws/smithy-go v1.22.2 // indirect 37 | github.com/beorn7/perks v1.0.1 // indirect 38 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 39 | github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect 40 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 41 | github.com/docker/cli v27.5.0+incompatible // indirect 42 | github.com/docker/distribution v2.8.3+incompatible // indirect 43 | github.com/docker/docker-credential-helpers v0.8.2 // indirect 44 | github.com/emicklei/go-restful/v3 v3.11.3 // indirect 45 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 46 | github.com/go-logr/logr v1.4.2 // indirect 47 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 48 | github.com/go-openapi/jsonreference v0.20.4 // indirect 49 | github.com/go-openapi/swag v0.23.0 // indirect 50 | github.com/gogo/protobuf v1.3.2 // indirect 51 | github.com/golang/protobuf v1.5.4 // indirect 52 | github.com/google/gnostic-models v0.6.8 // indirect 53 | github.com/google/go-cmp v0.7.0 // indirect 54 | github.com/google/gofuzz v1.2.0 // indirect 55 | github.com/google/uuid v1.6.0 // indirect 56 | github.com/josharian/intern v1.0.0 // indirect 57 | github.com/json-iterator/go v1.1.12 // indirect 58 | github.com/klauspost/compress v1.18.0 // indirect 59 | github.com/mailru/easyjson v0.7.7 // indirect 60 | github.com/mattn/go-colorable v0.1.13 // indirect 61 | github.com/mattn/go-isatty v0.0.17 // indirect 62 | github.com/mitchellh/go-homedir v1.1.0 // indirect 63 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 64 | github.com/modern-go/reflect2 v1.0.2 // indirect 65 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 66 | github.com/opencontainers/go-digest v1.0.0 // indirect 67 | github.com/opencontainers/image-spec v1.1.0 // indirect 68 | github.com/pkg/errors v0.9.1 // indirect 69 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 70 | github.com/prometheus/client_model v0.6.1 // indirect 71 | github.com/prometheus/common v0.62.0 // indirect 72 | github.com/prometheus/procfs v0.15.1 // indirect 73 | github.com/rs/zerolog v1.29.0 // indirect 74 | github.com/spf13/pflag v1.0.5 // indirect 75 | github.com/vbatts/tar-split v0.11.6 // indirect 76 | github.com/x448/float16 v0.8.4 // indirect 77 | golang.org/x/net v0.38.0 // indirect 78 | golang.org/x/oauth2 v0.25.0 // indirect 79 | golang.org/x/sync v0.12.0 // indirect 80 | golang.org/x/sys v0.31.0 // indirect 81 | golang.org/x/term v0.30.0 // indirect 82 | golang.org/x/text v0.23.0 // indirect 83 | golang.org/x/time v0.7.0 // indirect 84 | google.golang.org/protobuf v1.36.5 // indirect 85 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 86 | gopkg.in/inf.v0 v0.9.1 // indirect 87 | gopkg.in/yaml.v3 v3.0.1 // indirect 88 | k8s.io/klog/v2 v2.130.1 // indirect 89 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 90 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 91 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 92 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 93 | sigs.k8s.io/yaml v1.4.0 // indirect 94 | ) 95 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-node-termination-handler v1.25.0 h1:PaNjFYokT70al0SLzdHzX4HPGeJkVqmHnKWoz+A+JUk= 2 | github.com/aws/aws-node-termination-handler v1.25.0/go.mod h1:2Az1GI92+TjltOjkKzOUexFJ7t24wakAGunmagC3CnQ= 3 | github.com/aws/aws-sdk-go v1.55.4 h1:u7sFWQQs5ivGuYvCxi7gJI8nN/P9Dq04huLaw39a4lg= 4 | github.com/aws/aws-sdk-go v1.55.4/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= 5 | github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= 6 | github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= 7 | github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= 8 | github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= 9 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= 10 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ= 11 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= 12 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= 13 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= 14 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= 15 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= 16 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= 17 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= 18 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= 19 | github.com/aws/aws-sdk-go-v2/service/ecr v1.44.0 h1:E+UTVTDH6XTSjqxHWRuY8nB6s+05UllneWxnycplHFk= 20 | github.com/aws/aws-sdk-go-v2/service/ecr v1.44.0/go.mod h1:iQ1skgw1XRK+6Lgkb0I9ODatAP72WoTILh0zXQ5DtbU= 21 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= 22 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= 23 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= 24 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= 25 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= 26 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= 27 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako= 28 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= 29 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= 30 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= 31 | github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= 32 | github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= 33 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 34 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 35 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 36 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 37 | github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= 38 | github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= 39 | github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 40 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 41 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 42 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 43 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 44 | github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM= 45 | github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= 46 | github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= 47 | github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 48 | github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= 49 | github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= 50 | github.com/emicklei/go-restful/v3 v3.11.3 h1:yagOQz/38xJmcNeZJtrUcKjkHRltIaIFXKWeG1SkWGE= 51 | github.com/emicklei/go-restful/v3 v3.11.3/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 52 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 53 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 54 | github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0= 55 | github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU= 56 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 57 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 58 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 59 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 60 | github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= 61 | github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= 62 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 63 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 64 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 65 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 66 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 67 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 68 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 69 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 70 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 71 | github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= 72 | github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 73 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 74 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 75 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 76 | github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= 77 | github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI= 78 | github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20240129192428-8dadbe76ff8c h1:kTvQam8K98GB13IABdbPUt9QCUq55OPlpmyPeKUi2/g= 79 | github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20240129192428-8dadbe76ff8c/go.mod h1:5sSbf/SbGGvjWIlMlt2bkEqOq+ufOIBYrBevLuxbfSs= 80 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 81 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 82 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 83 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 84 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 85 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 86 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 87 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 88 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 89 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 90 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 91 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 92 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 93 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 94 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 95 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 96 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 97 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 98 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 99 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 100 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 101 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 102 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 103 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 104 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 105 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 106 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 107 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 108 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 109 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 110 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 111 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 112 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 113 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 114 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 115 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 116 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 117 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 118 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 119 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 120 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 121 | github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= 122 | github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 123 | github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= 124 | github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 125 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 126 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 127 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= 128 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 129 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 130 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 131 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 132 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 133 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 134 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 135 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 136 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 137 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 138 | github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= 139 | github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 140 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 141 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 142 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 143 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 144 | github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 145 | github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= 146 | github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= 147 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 148 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 149 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 150 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 151 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 152 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 153 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 154 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 155 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 156 | github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs= 157 | github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI= 158 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 159 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 160 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 161 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 162 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 163 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 164 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 165 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 166 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 167 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 168 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 169 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 170 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 171 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 172 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 173 | golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= 174 | golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 175 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 176 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 177 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 178 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 179 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 180 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 181 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 182 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 183 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 184 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 185 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 186 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 187 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 188 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 189 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 190 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 191 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 192 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 193 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 194 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 195 | golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= 196 | golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 197 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 198 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 199 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 200 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 201 | golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= 202 | golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= 203 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 204 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 205 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 206 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 207 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 208 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 209 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 210 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 211 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 212 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 213 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 214 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 215 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 216 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 217 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 218 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 219 | gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk= 220 | gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ= 221 | k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= 222 | k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= 223 | k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= 224 | k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= 225 | k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= 226 | k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= 227 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 228 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 229 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= 230 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= 231 | k8s.io/sample-controller v0.32.3 h1:Pt1Gct08Jb2Nx1Xl3F+aaT+oxQgierbD3kK3jDSUEXo= 232 | k8s.io/sample-controller v0.32.3/go.mod h1:we/NGKVMfegIBA5N8xDa1kPrW+Z9XHTWxQD4JfCnAuc= 233 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= 234 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 235 | sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= 236 | sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= 237 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= 238 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= 239 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= 240 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= 241 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 242 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 243 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "net/http" 8 | "regexp" 9 | "strings" 10 | "time" 11 | 12 | "github.com/flant/k8s-image-availability-exporter/pkg/cli" 13 | "github.com/flant/k8s-image-availability-exporter/pkg/handlers" 14 | "github.com/flant/k8s-image-availability-exporter/pkg/logging" 15 | "github.com/flant/k8s-image-availability-exporter/pkg/registry" 16 | "github.com/flant/k8s-image-availability-exporter/pkg/version" 17 | "github.com/google/go-containerregistry/pkg/name" 18 | 19 | "github.com/sirupsen/logrus" 20 | 21 | "github.com/prometheus/client_golang/prometheus" 22 | "github.com/prometheus/client_golang/prometheus/promhttp" 23 | 24 | "k8s.io/apimachinery/pkg/util/wait" 25 | "k8s.io/client-go/kubernetes" 26 | "k8s.io/client-go/tools/clientcmd" 27 | "k8s.io/sample-controller/pkg/signals" 28 | _ "sigs.k8s.io/controller-runtime/pkg/metrics" 29 | ) 30 | 31 | func main() { 32 | cp := newCaPaths() 33 | mirrors := newMirrorMap() 34 | forceCheckDisabledControllerKindsParser := cli.NewForceCheckDisabledControllerKindsParser() 35 | 36 | imageCheckInterval := flag.Duration("check-interval", time.Minute, "image re-check interval") 37 | ignoredImagesStr := flag.String("ignored-images", "", "tilde-separated image regexes to ignore, each image will be checked against this list of regexes") 38 | allowedImagesStr := flag.String("allowed-images", "", "tilde-separated image regexes to allow, each image will be checked against this list of regexes") 39 | bindAddr := flag.String("bind-address", ":8080", "address:port to bind /metrics endpoint to") 40 | namespaceLabels := flag.String("namespace-label", "", "namespace label for checks") 41 | insecureSkipVerify := flag.Bool("skip-registry-cert-verification", false, "whether to skip registries' certificate verification") 42 | plainHTTP := flag.Bool("allow-plain-http", false, "whether to fallback to HTTP scheme for registries that don't support HTTPS") // named after the ctr cli flag 43 | defaultRegistry := flag.String("default-registry", "", fmt.Sprintf("default registry to use in absence of a fully qualified image name, defaults to %q", name.DefaultRegistry)) 44 | flag.Var(&cp, "capath", "path to a file that contains CA certificates in the PEM format") // named after the curl cli flag 45 | flag.Var(&mirrors, "image-mirror", "Add a mirror repository (format: original=mirror)") 46 | flag.Func("force-check-disabled-controllers", `comma-separated list of controller kinds for which image is forcibly checked, even when workloads are disabled or suspended. Acceptable values include "Deployment", "StatefulSet", "DaemonSet", "Cronjob" or "*" for all kinds (this option is case-insensitive)`, forceCheckDisabledControllerKindsParser.Parse) 47 | 48 | flag.Parse() 49 | 50 | logrus.SetFormatter(&logrus.TextFormatter{ 51 | FullTimestamp: true, 52 | }) 53 | logrus.AddHook(logging.NewPrometheusHook()) 54 | logrus.Infof("Starting k8s-image-availability-exporter %s", version.Version) 55 | 56 | // set up signals, so we handle the first shutdown signal gracefully 57 | stopCh := signals.SetupSignalHandler() 58 | 59 | cfg, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(clientcmd.NewDefaultClientConfigLoadingRules(), &clientcmd.ConfigOverrides{}).ClientConfig() 60 | if err != nil { 61 | logrus.Fatalf("Couldn't get Kubernetes default config: %s", err) 62 | } 63 | 64 | kubeClient, err := kubernetes.NewForConfig(cfg) 65 | if err != nil { 66 | logrus.Fatalf("Error building kubernetes clientset: %s", err.Error()) 67 | } 68 | 69 | liveTicksCounter := prometheus.NewCounter( 70 | prometheus.CounterOpts{ 71 | Namespace: "k8s_image_availability_exporter", 72 | Name: "completed_rechecks_total", 73 | Help: "Number of image rechecks completed.", 74 | }, 75 | ) 76 | prometheus.MustRegister(liveTicksCounter) 77 | 78 | var ignoredImgRegexes []regexp.Regexp 79 | if *ignoredImagesStr != "" { 80 | regexStrings := strings.Split(*ignoredImagesStr, "~") 81 | for _, regexStr := range regexStrings { 82 | ignoredImgRegexes = append(ignoredImgRegexes, *regexp.MustCompile(regexStr)) 83 | } 84 | } 85 | 86 | var allowedImgRegexes []regexp.Regexp 87 | if *allowedImagesStr != "" { 88 | regexStrings := strings.Split(*allowedImagesStr, "~") 89 | for _, regexStr := range regexStrings { 90 | allowedImgRegexes = append(allowedImgRegexes, *regexp.MustCompile(regexStr)) 91 | } 92 | } 93 | 94 | registryChecker := registry.NewChecker( 95 | stopCh.Done(), 96 | kubeClient, 97 | *insecureSkipVerify, 98 | *plainHTTP, 99 | cp, 100 | forceCheckDisabledControllerKindsParser.ParsedKinds, 101 | ignoredImgRegexes, 102 | allowedImgRegexes, 103 | *defaultRegistry, 104 | *namespaceLabels, 105 | mirrors, 106 | ) 107 | prometheus.MustRegister(registryChecker) 108 | 109 | http.Handle("/metrics", promhttp.Handler()) 110 | http.HandleFunc("/healthz", handlers.Healthz) 111 | go func() { 112 | logrus.Fatal(http.ListenAndServe(*bindAddr, nil)) 113 | }() 114 | 115 | handlers.UpdateHealth(true) 116 | 117 | wait.Until(func() { 118 | registryChecker.Tick() 119 | liveTicksCounter.Inc() 120 | }, *imageCheckInterval, stopCh.Done()) 121 | } 122 | 123 | /* Custom flag types */ 124 | 125 | var ( 126 | _ flag.Value = (*caPaths)(nil) 127 | _ flag.Value = (*mirrorMap)(nil) 128 | ) 129 | 130 | // caPaths is a custom flag type for a list of paths to CA certificates 131 | type caPaths []string 132 | 133 | func newCaPaths() caPaths { 134 | return caPaths{} 135 | } 136 | 137 | func (c *caPaths) String() string { 138 | return fmt.Sprintf("%v", *c) 139 | } 140 | 141 | func (c *caPaths) Set(value string) error { 142 | *c = append(*c, value) 143 | return nil 144 | } 145 | 146 | // mirrorMap is a custom flag type for a map of original to mirror repository names 147 | type mirrorMap map[string]string 148 | 149 | func newMirrorMap() mirrorMap { 150 | return make(mirrorMap) 151 | } 152 | 153 | func (m *mirrorMap) String() string { 154 | return fmt.Sprintf("%v", *m) 155 | } 156 | 157 | func (m *mirrorMap) Set(value string) error { 158 | result := strings.Split(value, "=") 159 | if len(result) != 2 { 160 | return errors.New("invalid format for mirror, must be original=mirror") 161 | } 162 | (*m)[result[0]] = result[1] 163 | return nil 164 | } 165 | -------------------------------------------------------------------------------- /pkg/cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | "strings" 7 | ) 8 | 9 | type ForceCheckDisabledControllerKindsParser struct { 10 | allowedControllerKinds []string 11 | ParsedKinds []string 12 | } 13 | 14 | func (parser *ForceCheckDisabledControllerKindsParser) Parse(flagValue string) error { 15 | if flagValue == "*" { 16 | parser.ParsedKinds = parser.allowedControllerKinds 17 | return nil 18 | } 19 | 20 | parser.ParsedKinds = []string{} 21 | 22 | flagLoop: 23 | for _, kind := range strings.Split(flagValue, ",") { 24 | kind = strings.ToLower(kind) 25 | 26 | for _, allowedControllerKind := range parser.allowedControllerKinds { 27 | if kind == allowedControllerKind { 28 | if !slices.Contains(parser.ParsedKinds, kind) { 29 | parser.ParsedKinds = append(parser.ParsedKinds, kind) 30 | } 31 | continue flagLoop 32 | } 33 | } 34 | 35 | return fmt.Errorf(`must be one of %s or * for all kinds`, strings.Join(parser.allowedControllerKinds, ", ")) 36 | } 37 | 38 | return nil 39 | } 40 | 41 | func NewForceCheckDisabledControllerKindsParser() *ForceCheckDisabledControllerKindsParser { 42 | parser := &ForceCheckDisabledControllerKindsParser{} 43 | parser.allowedControllerKinds = []string{"deployment", "statefulset", "daemonset", "cronjob"} 44 | return parser 45 | } 46 | -------------------------------------------------------------------------------- /pkg/cli/cli_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_ForceCheckDisabledControllerKindsParser(t *testing.T) { 12 | const ( 13 | allKinds = "*" 14 | goodKinds = "deployment,statefulset" 15 | goodKindsWithDuplicates = "deployment,deployment,statefulset,cronjob,cronjob" 16 | goodKindsWithWildcard = "deployment,statefulset,*" 17 | badKinds = "deployment,job" 18 | ) 19 | parser := NewForceCheckDisabledControllerKindsParser() 20 | expectedErr := fmt.Errorf(`must be one of %s or * for all kinds`, strings.Join(parser.allowedControllerKinds, ", ")) 21 | 22 | err := parser.Parse(allKinds) 23 | require.NoError(t, err) 24 | require.Equal(t, parser.ParsedKinds, parser.allowedControllerKinds) 25 | 26 | err = parser.Parse(goodKinds) 27 | require.NoError(t, err) 28 | require.Equal(t, parser.ParsedKinds, []string{"deployment", "statefulset"}) 29 | 30 | err = parser.Parse(goodKindsWithDuplicates) 31 | require.NoError(t, err) 32 | require.Equal(t, parser.ParsedKinds, []string{"deployment", "statefulset", "cronjob"}) 33 | 34 | err = parser.Parse(goodKindsWithWildcard) 35 | require.Error(t, expectedErr, err) 36 | 37 | err = parser.Parse(badKinds) 38 | require.Error(t, expectedErr, err) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/handlers/healthz.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | "sync" 6 | ) 7 | 8 | var ( 9 | mutex sync.RWMutex 10 | healthy = false 11 | ) 12 | 13 | func UpdateHealth(isHealthy bool) { 14 | mutex.Lock() 15 | healthy = isHealthy 16 | mutex.Unlock() 17 | } 18 | 19 | func Healthz(w http.ResponseWriter, _ *http.Request) { 20 | mutex.RLock() 21 | isHealthy := healthy 22 | mutex.RUnlock() 23 | if isHealthy { 24 | w.WriteHeader(http.StatusOK) 25 | _, _ = w.Write([]byte("OK")) 26 | } else { 27 | w.WriteHeader(http.StatusInternalServerError) 28 | _, _ = w.Write([]byte("Unhealthy")) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pkg/logging/log.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | func NewPrometheusHook() *PrometheusHook { 10 | counter := promauto.NewCounterVec( 11 | prometheus.CounterOpts{ 12 | Name: "log_statements_total", 13 | Help: "Number of log statements, differentiated by log level.", 14 | }, 15 | []string{"level"}, 16 | ) 17 | 18 | return &PrometheusHook{ 19 | counter: counter, 20 | } 21 | } 22 | 23 | type PrometheusHook struct { 24 | counter *prometheus.CounterVec 25 | } 26 | 27 | func (h *PrometheusHook) Levels() []logrus.Level { 28 | return logrus.AllLevels 29 | } 30 | 31 | func (h *PrometheusHook) Fire(e *logrus.Entry) error { 32 | h.counter.WithLabelValues(e.Level.String()).Inc() 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /pkg/providers/amazon/amazon.go: -------------------------------------------------------------------------------- 1 | package amazon 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "fmt" 7 | "strings" 8 | "time" 9 | 10 | "github.com/aws/aws-node-termination-handler/pkg/ec2metadata" 11 | "github.com/aws/aws-sdk-go-v2/config" 12 | "github.com/aws/aws-sdk-go-v2/service/ecr" 13 | "github.com/google/go-containerregistry/pkg/authn" 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | type Provider struct { 18 | ecrClient *ecr.Client 19 | authToken authn.AuthConfig 20 | authTokenExpiry time.Time 21 | name string 22 | } 23 | 24 | func NewProvider() *Provider { 25 | cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(requestEC2Region())) 26 | if err != nil { 27 | logrus.Warn("error while loading config for new aws provider ", err) 28 | } 29 | ecrClient := ecr.NewFromConfig(cfg) 30 | return &Provider{ecrClient: ecrClient, name: "amazon"} 31 | } 32 | 33 | func (p *Provider) GetAuthKeychain(_ string) (authn.Keychain, error) { 34 | const bufferPeriod = time.Hour 35 | 36 | if p.authToken.Username != "" && time.Now().Before(p.authTokenExpiry.Add(-bufferPeriod)) { 37 | return &customKeychain{authenticator: authn.FromConfig(p.authToken)}, nil 38 | } 39 | 40 | authTokenOutput, err := p.ecrClient.GetAuthorizationToken(context.TODO(), &ecr.GetAuthorizationTokenInput{}) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | if len(authTokenOutput.AuthorizationData) == 0 { 46 | return nil, fmt.Errorf("no authorization data received from ECR") 47 | } 48 | 49 | authData := authTokenOutput.AuthorizationData[0] 50 | 51 | if authData.AuthorizationToken == nil || *authData.AuthorizationToken == "" { 52 | return nil, fmt.Errorf("authorization token is missing or empty") 53 | } 54 | 55 | decodedToken, err := base64.StdEncoding.DecodeString(*authData.AuthorizationToken) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | credentials := strings.SplitN(string(decodedToken), ":", 2) 61 | if len(credentials) != 2 { 62 | return nil, fmt.Errorf("invalid authorization token format") 63 | } 64 | 65 | p.authToken = authn.AuthConfig{ 66 | Username: credentials[0], 67 | Password: credentials[1], 68 | } 69 | p.authTokenExpiry = *authData.ExpiresAt 70 | 71 | return &customKeychain{authenticator: authn.FromConfig(p.authToken)}, nil 72 | } 73 | 74 | func requestEC2Region() string { 75 | ec2metadataClient := ec2metadata.New("http://169.254.169.254", 1) 76 | metadata := ec2metadataClient.GetNodeMetadata() 77 | 78 | return metadata.Region 79 | } 80 | 81 | type customKeychain struct { 82 | authenticator authn.Authenticator 83 | } 84 | 85 | func (kc *customKeychain) Resolve(_ authn.Resource) (authn.Authenticator, error) { 86 | return kc.authenticator, nil 87 | } 88 | 89 | func (p Provider) GetName() string { 90 | return p.name 91 | } 92 | -------------------------------------------------------------------------------- /pkg/providers/k8s/k8s.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | 9 | kubeauth "github.com/google/go-containerregistry/pkg/authn/kubernetes" 10 | corev1 "k8s.io/api/core/v1" 11 | 12 | "github.com/google/go-containerregistry/pkg/authn" 13 | ) 14 | 15 | type Provider struct { 16 | pullSecretsGetter func(image string) []corev1.Secret 17 | name string 18 | } 19 | 20 | func NewProvider(pullSecretsGetter func(image string) []corev1.Secret) *Provider { 21 | return &Provider{ 22 | pullSecretsGetter: pullSecretsGetter, 23 | name: "k8s", 24 | } 25 | } 26 | 27 | func (p Provider) correctDockerRegistry(secrets []corev1.Secret) ([]corev1.Secret, error) { 28 | for i, secret := range secrets { 29 | if secret.Type != corev1.SecretTypeDockerConfigJson && secret.Type != corev1.SecretTypeDockercfg { 30 | continue 31 | } 32 | 33 | var data []byte 34 | var exists bool 35 | if secret.Type == corev1.SecretTypeDockerConfigJson { 36 | data, exists = secret.Data[corev1.DockerConfigJsonKey] 37 | } else { 38 | data, exists = secret.Data[corev1.DockerConfigKey] 39 | } 40 | if !exists { 41 | continue 42 | } 43 | 44 | dockerConfig, err := parseDockerConfig(data) 45 | if err != nil { 46 | return nil, fmt.Errorf("failed to parse docker config for secret %d: %w", i, err) 47 | } 48 | 49 | if err := updateDockerRegistryAuths(dockerConfig); err != nil { 50 | return nil, fmt.Errorf("failed to update docker registry auths for secret %d: %w", i, err) 51 | } 52 | 53 | updatedData, err := json.Marshal(dockerConfig) 54 | if err != nil { 55 | return nil, fmt.Errorf("failed to marshal updated docker config for secret %d: %w", i, err) 56 | } 57 | 58 | if secret.Type == corev1.SecretTypeDockerConfigJson { 59 | secrets[i].Data[corev1.DockerConfigJsonKey] = updatedData 60 | } else { 61 | secrets[i].Data[corev1.DockerConfigKey] = updatedData 62 | } 63 | } 64 | return secrets, nil 65 | } 66 | 67 | // Example of incoming data for DockerConfigJson: 68 | // { 69 | // "auths": { 70 | // "https://index.docker.io/v1/": {"auth": "xxxx=="}, 71 | // "https://docker.io/v1/": {"auth": "xxxx=="} 72 | // } 73 | // } 74 | // Example of outgoing data for DockerConfig: 75 | // { 76 | // "https://index.docker.io/v1/": {"auth": "xxxx=="} 77 | // } 78 | 79 | func parseDockerConfig(data []byte) (map[string]json.RawMessage, error) { 80 | var dockerConfig map[string]json.RawMessage 81 | 82 | if err := json.Unmarshal(data, &dockerConfig); err != nil { 83 | return nil, fmt.Errorf("unmarshalling docker config: %w", err) 84 | } 85 | 86 | return dockerConfig, nil 87 | } 88 | 89 | func updateDockerRegistryAuths(dockerConfig map[string]json.RawMessage) error { 90 | if authsRaw, exists := dockerConfig["auths"]; exists { 91 | var authsMap map[string]json.RawMessage 92 | if err := json.Unmarshal(authsRaw, &authsMap); err != nil { 93 | return fmt.Errorf("unmarshalling 'auths': %w", err) 94 | } 95 | 96 | for url, creds := range authsMap { 97 | if strings.Contains(url, "docker.io") && url != "https://index.docker.io/v1/" { 98 | authsMap["https://index.docker.io/v1/"] = creds 99 | delete(authsMap, url) 100 | } 101 | } 102 | 103 | updatedAuthsRaw, err := json.Marshal(authsMap) 104 | if err != nil { 105 | return fmt.Errorf("marshalling updated 'auths': %w", err) 106 | } 107 | dockerConfig["auths"] = updatedAuthsRaw 108 | } else { 109 | for url, creds := range dockerConfig { 110 | if strings.Contains(url, "docker.io") && url != "https://index.docker.io/v1/" { 111 | dockerConfig["https://index.docker.io/v1/"] = creds 112 | delete(dockerConfig, url) 113 | break 114 | } 115 | } 116 | } 117 | 118 | return nil 119 | } 120 | 121 | func (p Provider) GetAuthKeychain(registry string) (authn.Keychain, error) { 122 | dereferencedPullSecrets := p.pullSecretsGetter(registry) 123 | correctedSecrets, err := p.correctDockerRegistry(dereferencedPullSecrets) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | kc, err := kubeauth.NewFromPullSecrets(context.TODO(), correctedSecrets) 129 | if err != nil { 130 | return nil, fmt.Errorf("error while processing keychain from secrets: %w", err) 131 | } 132 | return kc, nil 133 | } 134 | 135 | func (p Provider) GetName() string { 136 | return p.name 137 | } 138 | -------------------------------------------------------------------------------- /pkg/providers/provider.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | "regexp" 6 | 7 | "github.com/google/go-containerregistry/pkg/authn" 8 | ) 9 | 10 | type Provider interface { 11 | GetName() string 12 | GetAuthKeychain(registry string) (authn.Keychain, error) 13 | } 14 | 15 | type ProviderRegistry map[string]Provider 16 | 17 | func NewProviderChain(providers ...Provider) ProviderRegistry { 18 | p := make(ProviderRegistry) 19 | 20 | for _, provider := range providers { 21 | p[provider.GetName()] = provider 22 | } 23 | 24 | return p 25 | } 26 | 27 | type ImagePullSecretsFunc func(image string) []corev1.Secret 28 | 29 | var ( 30 | amazonURLRegex = regexp.MustCompile(`^(\d{12})\.dkr\.ecr\.([a-z0-9-]+)\.amazonaws\.com(?:\.cn)?/([^:]+):(.+)$`) 31 | ) 32 | 33 | func (p ProviderRegistry) GetAuthKeychain(registry string) (authn.Keychain, error) { 34 | switch { 35 | case amazonURLRegex.MatchString(registry): 36 | return p["amazon"].GetAuthKeychain(registry) 37 | default: 38 | return p["k8s"].GetAuthKeychain(registry) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pkg/registry/checker.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "errors" 8 | "fmt" 9 | "github.com/flant/k8s-image-availability-exporter/pkg/providers" 10 | "github.com/flant/k8s-image-availability-exporter/pkg/providers/amazon" 11 | "github.com/flant/k8s-image-availability-exporter/pkg/providers/k8s" 12 | "github.com/flant/k8s-image-availability-exporter/pkg/version" 13 | "github.com/google/go-containerregistry/pkg/authn" 14 | "github.com/google/go-containerregistry/pkg/v1/remote/transport" 15 | "k8s.io/apimachinery/pkg/util/wait" 16 | "k8s.io/client-go/tools/cache" 17 | "net/http" 18 | "os" 19 | "regexp" 20 | "strings" 21 | "time" 22 | 23 | "github.com/prometheus/client_golang/prometheus" 24 | 25 | "github.com/google/go-containerregistry/pkg/name" 26 | "github.com/google/go-containerregistry/pkg/v1/remote" 27 | "github.com/sirupsen/logrus" 28 | 29 | k8sapierrors "k8s.io/apimachinery/pkg/api/errors" 30 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 | appsv1informers "k8s.io/client-go/informers/apps/v1" 32 | batchv1informers "k8s.io/client-go/informers/batch/v1" 33 | corev1informers "k8s.io/client-go/informers/core/v1" 34 | 35 | "k8s.io/client-go/informers" 36 | 37 | "k8s.io/client-go/kubernetes" 38 | 39 | "github.com/flant/k8s-image-availability-exporter/pkg/store" 40 | ) 41 | 42 | const ( 43 | failedCheckBatchSize = 20 44 | checkBatchSize = 50 45 | ) 46 | 47 | type registryCheckerConfig struct { 48 | defaultRegistry string 49 | plainHTTP bool 50 | mirrorsMap map[string]string 51 | } 52 | 53 | type Checker struct { 54 | imageStore *store.ImageStore 55 | 56 | serviceAccountInformer corev1informers.ServiceAccountInformer 57 | namespacesInformer corev1informers.NamespaceInformer 58 | deploymentsInformer appsv1informers.DeploymentInformer 59 | statefulSetsInformer appsv1informers.StatefulSetInformer 60 | daemonSetsInformer appsv1informers.DaemonSetInformer 61 | cronJobsInformer batchv1informers.CronJobInformer 62 | secretsInformer corev1informers.SecretInformer 63 | 64 | controllerIndexers ControllerIndexers 65 | 66 | ignoredImagesRegex []regexp.Regexp 67 | allowedImagesRegex []regexp.Regexp 68 | 69 | registryTransport http.RoundTripper 70 | 71 | kubeClient *kubernetes.Clientset 72 | 73 | config registryCheckerConfig 74 | 75 | providerRegistry providers.ProviderRegistry 76 | } 77 | 78 | func NewChecker( 79 | stopCh <-chan struct{}, 80 | kubeClient *kubernetes.Clientset, 81 | skipVerify bool, 82 | plainHTTP bool, 83 | caPths []string, 84 | forceCheckDisabledControllerKinds []string, 85 | ignoredImages []regexp.Regexp, 86 | allowedImages []regexp.Regexp, 87 | defaultRegistry string, 88 | namespaceLabel string, 89 | mirrorsMap map[string]string, 90 | ) *Checker { 91 | informerFactory := informers.NewSharedInformerFactory(kubeClient, time.Hour) 92 | 93 | customTransport := http.DefaultTransport.(*http.Transport).Clone() 94 | if skipVerify { 95 | customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 96 | } else if len(caPths) > 0 { 97 | rootCAs, _ := x509.SystemCertPool() 98 | if rootCAs == nil { 99 | rootCAs = x509.NewCertPool() 100 | } 101 | for _, caPath := range caPths { 102 | pemCerts, err := os.ReadFile(caPath) 103 | if err != nil { 104 | logrus.Fatalf("Failed to open file %q: %v", caPath, err) 105 | } 106 | if ok := rootCAs.AppendCertsFromPEM(pemCerts); !ok { 107 | logrus.Fatalf("Error parsing %q content as a PEM encoded certificate", caPath) 108 | } 109 | } 110 | customTransport.TLSClientConfig = &tls.Config{RootCAs: rootCAs} 111 | } 112 | 113 | roundTripper := transport.NewUserAgent(customTransport, fmt.Sprintf("k8s-image-availability-exporter/%s", version.Version)) 114 | 115 | rc := &Checker{ 116 | serviceAccountInformer: informerFactory.Core().V1().ServiceAccounts(), 117 | namespacesInformer: informerFactory.Core().V1().Namespaces(), 118 | deploymentsInformer: informerFactory.Apps().V1().Deployments(), 119 | statefulSetsInformer: informerFactory.Apps().V1().StatefulSets(), 120 | daemonSetsInformer: informerFactory.Apps().V1().DaemonSets(), 121 | cronJobsInformer: informerFactory.Batch().V1().CronJobs(), 122 | secretsInformer: informerFactory.Core().V1().Secrets(), 123 | 124 | ignoredImagesRegex: ignoredImages, 125 | allowedImagesRegex: allowedImages, 126 | 127 | registryTransport: roundTripper, 128 | 129 | kubeClient: kubeClient, 130 | 131 | config: registryCheckerConfig{ 132 | defaultRegistry: defaultRegistry, 133 | plainHTTP: plainHTTP, 134 | mirrorsMap: mirrorsMap, 135 | }, 136 | } 137 | 138 | rc.imageStore = store.NewImageStore(rc.Check, checkBatchSize, failedCheckBatchSize) 139 | 140 | err := rc.namespacesInformer.Informer().AddIndexers(namespaceIndexers(namespaceLabel)) 141 | if err != nil { 142 | panic(err) 143 | } 144 | rc.controllerIndexers.namespaceIndexer = rc.namespacesInformer.Informer().GetIndexer() 145 | 146 | if err != nil { 147 | panic(err) 148 | } 149 | rc.controllerIndexers.serviceAccountIndexer = rc.serviceAccountInformer.Informer().GetIndexer() 150 | 151 | _, _ = rc.deploymentsInformer.Informer().AddEventHandlerWithResyncPeriod(cache.ResourceEventHandlerFuncs{ 152 | AddFunc: func(obj interface{}) { 153 | rc.reconcile(obj) 154 | }, 155 | UpdateFunc: func(_, newObj interface{}) { 156 | rc.reconcile(newObj) 157 | }, 158 | DeleteFunc: func(obj interface{}) { 159 | rc.reconcile(obj) 160 | }, 161 | }, time.Minute) 162 | err = rc.deploymentsInformer.Informer().AddIndexers(imageIndexers) 163 | if err != nil { 164 | panic(err) 165 | } 166 | err = rc.deploymentsInformer.Informer().SetTransform(getImagesFromDeployment) 167 | if err != nil { 168 | panic(err) 169 | } 170 | rc.controllerIndexers.deploymentIndexer = rc.deploymentsInformer.Informer().GetIndexer() 171 | 172 | _, _ = rc.statefulSetsInformer.Informer().AddEventHandlerWithResyncPeriod(cache.ResourceEventHandlerFuncs{ 173 | AddFunc: func(obj interface{}) { 174 | rc.reconcile(obj) 175 | }, 176 | UpdateFunc: func(_, newObj interface{}) { 177 | rc.reconcile(newObj) 178 | }, 179 | DeleteFunc: func(obj interface{}) { 180 | rc.reconcile(obj) 181 | }, 182 | }, time.Minute) 183 | err = rc.statefulSetsInformer.Informer().AddIndexers(imageIndexers) 184 | if err != nil { 185 | panic(err) 186 | } 187 | err = rc.statefulSetsInformer.Informer().SetTransform(getImagesFromStatefulSet) 188 | if err != nil { 189 | panic(err) 190 | } 191 | rc.controllerIndexers.statefulSetIndexer = rc.statefulSetsInformer.Informer().GetIndexer() 192 | 193 | _, _ = rc.daemonSetsInformer.Informer().AddEventHandlerWithResyncPeriod(cache.ResourceEventHandlerFuncs{ 194 | AddFunc: func(obj interface{}) { 195 | rc.reconcile(obj) 196 | }, 197 | UpdateFunc: func(_, newObj interface{}) { 198 | rc.reconcile(newObj) 199 | }, 200 | DeleteFunc: func(obj interface{}) { 201 | rc.reconcile(obj) 202 | }, 203 | }, time.Minute) 204 | err = rc.daemonSetsInformer.Informer().AddIndexers(imageIndexers) 205 | if err != nil { 206 | panic(err) 207 | } 208 | err = rc.daemonSetsInformer.Informer().SetTransform(getImagesFromDaemonSet) 209 | if err != nil { 210 | panic(err) 211 | } 212 | rc.controllerIndexers.daemonSetIndexer = rc.daemonSetsInformer.Informer().GetIndexer() 213 | 214 | _, _ = rc.cronJobsInformer.Informer().AddEventHandlerWithResyncPeriod(cache.ResourceEventHandlerFuncs{ 215 | AddFunc: func(obj interface{}) { 216 | rc.reconcile(obj) 217 | }, 218 | UpdateFunc: func(_, newObj interface{}) { 219 | rc.reconcile(newObj) 220 | }, 221 | DeleteFunc: func(obj interface{}) { 222 | rc.reconcile(obj) 223 | }, 224 | }, time.Minute) 225 | err = rc.cronJobsInformer.Informer().AddIndexers(imageIndexers) 226 | if err != nil { 227 | panic(err) 228 | } 229 | err = rc.cronJobsInformer.Informer().SetTransform(getImagesFromCronJob) 230 | if err != nil { 231 | panic(err) 232 | } 233 | rc.controllerIndexers.cronJobIndexer = rc.cronJobsInformer.Informer().GetIndexer() 234 | 235 | namespace := "default" 236 | // Create a context 237 | ctx := context.Background() 238 | // Attempt to list secrets in the default namespace 239 | _, enumerr := kubeClient.CoreV1().Secrets(namespace).List(ctx, metav1.ListOptions{ResourceVersion: "0"}) 240 | if statusError, isStatus := enumerr.(*k8sapierrors.StatusError); isStatus { 241 | if statusError.ErrStatus.Code == 401 { 242 | logrus.Warn("The provided ServiceAccount is not able to list secrets. The check for images in private registries requires 'spec.imagePullSecrets' to be configured correctly.") 243 | } else { 244 | logrus.WithFields(logrus.Fields{ 245 | "error_message": statusError.ErrStatus.Message, 246 | }).Error("Error trying to list secrets") 247 | } 248 | } else if err != nil { 249 | logrus.Fatal(err.Error()) 250 | } else { 251 | rc.controllerIndexers.secretIndexer = rc.secretsInformer.Informer().GetIndexer() 252 | } 253 | 254 | rc.controllerIndexers.forceCheckDisabledControllerKinds = forceCheckDisabledControllerKinds 255 | 256 | go informerFactory.Start(stopCh) 257 | logrus.Info("Waiting for cache sync") 258 | informerFactory.WaitForCacheSync(stopCh) 259 | logrus.Info("Caches populated successfully") 260 | 261 | rc.imageStore.RunGC(rc.controllerIndexers.GetContainerInfosForImage) 262 | registry := providers.NewProviderChain( 263 | amazon.NewProvider(), 264 | k8s.NewProvider(rc.controllerIndexers.GetImagePullSecrets), 265 | ) 266 | rc.providerRegistry = registry 267 | 268 | return rc 269 | } 270 | 271 | // Collect implements prometheus.Collector. 272 | func (rc *Checker) Collect(ch chan<- prometheus.Metric) { 273 | metrics := rc.imageStore.ExtractMetrics() 274 | 275 | for _, m := range metrics { 276 | ch <- m 277 | } 278 | } 279 | 280 | // Describe implements prometheus.Collector. 281 | func (rc *Checker) Describe(_ chan<- *prometheus.Desc) {} 282 | 283 | func (rc *Checker) Tick() { 284 | rc.imageStore.Check() 285 | } 286 | 287 | func (rc *Checker) reconcile(obj interface{}) { 288 | cis := getCis(obj) 289 | 290 | imagesLoop: 291 | for _, image := range cis.containerToImages { 292 | for _, allowedImagesRegex := range rc.allowedImagesRegex { 293 | if !allowedImagesRegex.MatchString(image) { 294 | continue imagesLoop 295 | } 296 | } 297 | 298 | for _, ignoredImageRegex := range rc.ignoredImagesRegex { 299 | if ignoredImageRegex.MatchString(image) { 300 | continue imagesLoop 301 | } 302 | } 303 | 304 | containerInfos := rc.controllerIndexers.GetContainerInfosForImage(image) 305 | 306 | rc.imageStore.ReconcileImage(image, containerInfos) 307 | } 308 | } 309 | 310 | func (rc *Checker) Check(imageName string) store.AvailabilityMode { 311 | keyChain, err := rc.providerRegistry.GetAuthKeychain(imageName) 312 | if err != nil { 313 | logrus.Warn("error while getting keychain for: ", err) 314 | return store.AuthnFailure 315 | } 316 | log := logrus.WithField("image_name", imageName) 317 | return rc.checkImageAvailability(log, imageName, keyChain) 318 | } 319 | 320 | func getImageWithMirror(originalImage string, mirrors map[string]string) string { 321 | for originalRepo, mirrorRepo := range mirrors { 322 | if strings.HasPrefix(originalImage, originalRepo) { 323 | return strings.Replace(originalImage, originalRepo, mirrorRepo, 1) 324 | } 325 | } 326 | 327 | return originalImage 328 | } 329 | 330 | func (rc *Checker) checkImageAvailability(log *logrus.Entry, imageName string, kc authn.Keychain) (availMode store.AvailabilityMode) { 331 | if len(rc.config.mirrorsMap) > 0 { 332 | imageName = getImageWithMirror(imageName, rc.config.mirrorsMap) 333 | } 334 | 335 | ref, err := parseImageName(imageName, rc.config.defaultRegistry, rc.config.plainHTTP) 336 | if err != nil { 337 | return checkImageNameParseErr(log, err) 338 | } 339 | 340 | imgErr := wait.ExponentialBackoff(wait.Backoff{ 341 | Duration: time.Second, 342 | Factor: 2, 343 | Steps: 2, 344 | }, func() (bool, error) { 345 | var err error 346 | availMode, err = check(ref, kc, rc.registryTransport) 347 | 348 | return availMode == store.Available, err 349 | }) 350 | 351 | if availMode != store.Available { 352 | log.WithField("availability_mode", availMode.String()).Error(imgErr) 353 | } 354 | 355 | return 356 | } 357 | 358 | func checkImageNameParseErr(log *logrus.Entry, err error) store.AvailabilityMode { 359 | var parseErr *name.ErrBadName 360 | if errors.As(err, &parseErr) { 361 | log.WithField("availability_mode", store.BadImageName.String()).Error(err) 362 | return store.BadImageName 363 | } 364 | 365 | log.WithField("availability_mode", store.UnknownError.String()).Error(err) 366 | return store.UnknownError 367 | } 368 | 369 | func parseImageName(image string, defaultRegistry string, plainHTTP bool) (name.Reference, error) { 370 | var ( 371 | ref name.Reference 372 | err error 373 | ) 374 | 375 | opts := make([]name.Option, 0) 376 | // Fallback to http scheme by default. See: 377 | // go-containerregistry https://github.com/jonjohnsonjr/go-containerregistry/blob/2a0d58f7c5f77f2a03c2a0cda47fc6da26ac1564/pkg/v1/remote/transport/schemer.go#L35-L44 378 | if plainHTTP { 379 | opts = append(opts, name.Insecure) 380 | } 381 | 382 | if len(defaultRegistry) > 0 { 383 | opts = append(opts, name.WithDefaultRegistry(defaultRegistry)) 384 | } 385 | 386 | ref, err = name.ParseReference(image, opts...) 387 | if err != nil { 388 | return nil, err 389 | } 390 | 391 | return ref, nil 392 | } 393 | 394 | func check(ref name.Reference, kc authn.Keychain, registryTransport http.RoundTripper) (store.AvailabilityMode, error) { 395 | var imgErr error 396 | 397 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 398 | defer cancel() 399 | 400 | // Fallback to default keychain if image is not found in the provided one. 401 | // This is a behavior that is close to what CRI does. Because, there is maybe an image pull secret, but with 402 | // the wrong credentials. Yet, the image may be available with the default keychain. 403 | if kc != nil { 404 | kc = authn.NewMultiKeychain(kc, authn.DefaultKeychain) 405 | } else { 406 | kc = authn.DefaultKeychain 407 | } 408 | 409 | _, imgErr = remote.Head( 410 | ref, 411 | remote.WithAuthFromKeychain(kc), 412 | remote.WithTransport(registryTransport), 413 | remote.WithContext(ctx), 414 | ) 415 | 416 | var availMode store.AvailabilityMode 417 | if IsAbsent(imgErr) { 418 | availMode = store.Absent 419 | } else if IsAuthnFail(imgErr) { 420 | availMode = store.AuthnFailure 421 | } else if IsAuthzFail(imgErr) { 422 | availMode = store.AuthzFailure 423 | } else if IsOldRegistry(imgErr) { 424 | availMode = store.Available 425 | } else if imgErr != nil { 426 | availMode = store.UnknownError 427 | } 428 | 429 | return availMode, imgErr 430 | } 431 | -------------------------------------------------------------------------------- /pkg/registry/checker_test.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "path" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_parseImageName(t *testing.T) { 11 | const ( 12 | goodImageName = "docker.io/test:test" 13 | goodImageNameWithoutRegistry = "test:test" 14 | badImageName = "te*^#@@st" 15 | 16 | defaultRegistryName = "test-registry.io" 17 | ) 18 | 19 | _, err := parseImageName(goodImageName, "", false) 20 | require.NoError(t, err) 21 | 22 | _, err = parseImageName(badImageName, "", false) 23 | require.Error(t, err) 24 | 25 | ref, err := parseImageName(goodImageNameWithoutRegistry, defaultRegistryName, false) 26 | require.NoError(t, err) 27 | require.Equal(t, path.Join(defaultRegistryName, goodImageNameWithoutRegistry), ref.Name()) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/registry/image_pull.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/google/go-containerregistry/pkg/v1/remote" 8 | "github.com/google/go-containerregistry/pkg/v1/remote/transport" 9 | ) 10 | 11 | func IsAbsent(err error) bool { 12 | var transpErr *transport.Error 13 | errors.As(err, &transpErr) 14 | 15 | if transpErr == nil { 16 | return false 17 | } 18 | 19 | return transpErr.StatusCode == http.StatusNotFound 20 | } 21 | 22 | func IsAuthnFail(err error) bool { 23 | var transpErr *transport.Error 24 | errors.As(err, &transpErr) 25 | 26 | if transpErr == nil { 27 | return false 28 | } 29 | 30 | return transpErr.StatusCode == http.StatusUnauthorized 31 | } 32 | 33 | func IsAuthzFail(err error) bool { 34 | var transpErr *transport.Error 35 | errors.As(err, &transpErr) 36 | 37 | if transpErr == nil { 38 | return false 39 | } 40 | 41 | return transpErr.StatusCode == http.StatusForbidden 42 | } 43 | 44 | func IsOldRegistry(err error) bool { 45 | return errors.Is(err, remote.ErrSchema1) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/registry/indexers.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | "strings" 7 | 8 | "github.com/flant/k8s-image-availability-exporter/pkg/store" 9 | "github.com/sirupsen/logrus" 10 | appsv1 "k8s.io/api/apps/v1" 11 | batchv1 "k8s.io/api/batch/v1" 12 | corev1 "k8s.io/api/core/v1" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/client-go/tools/cache" 15 | ) 16 | 17 | const ( 18 | imageIndexName = "image" 19 | labeledNSIndexName = "labeledNS" 20 | ) 21 | 22 | type ControllerIndexers struct { 23 | namespaceIndexer cache.Indexer 24 | serviceAccountIndexer cache.Indexer 25 | deploymentIndexer cache.Indexer 26 | statefulSetIndexer cache.Indexer 27 | daemonSetIndexer cache.Indexer 28 | cronJobIndexer cache.Indexer 29 | secretIndexer cache.Indexer 30 | forceCheckDisabledControllerKinds []string 31 | } 32 | 33 | type controllerWithContainerInfos struct { 34 | metav1.ObjectMeta 35 | controllerKind string 36 | containerToImages map[string]string 37 | pullSecretReferences []corev1.LocalObjectReference 38 | serviceAccountName string 39 | enabled bool 40 | } 41 | 42 | var ( 43 | imageIndexers = cache.Indexers{ 44 | imageIndexName: func(obj interface{}) (images []string, err error) { 45 | for _, v := range obj.(*controllerWithContainerInfos).containerToImages { 46 | images = append(images, v) 47 | } 48 | return 49 | }, 50 | } 51 | ) 52 | 53 | func (ci ControllerIndexers) validCi(cis *controllerWithContainerInfos) bool { 54 | if !cis.enabled && !slices.Contains(ci.forceCheckDisabledControllerKinds, strings.ToLower(cis.controllerKind)) { 55 | return false 56 | } 57 | 58 | nsList, _ := ci.namespaceIndexer.ByIndex(labeledNSIndexName, cis.Namespace) 59 | 60 | return len(nsList) != 0 61 | } 62 | 63 | func namespaceIndexers(nsLabel string) cache.Indexers { 64 | return cache.Indexers{ 65 | labeledNSIndexName: func(obj interface{}) ([]string, error) { 66 | ns := obj.(*corev1.Namespace) 67 | 68 | if len(nsLabel) == 0 { 69 | return []string{ns.GetName()}, nil 70 | } 71 | 72 | labels := ns.GetLabels() 73 | if len(labels) > 0 { 74 | if _, ok := labels[nsLabel]; ok { 75 | return []string{ns.GetName()}, nil 76 | } 77 | } 78 | 79 | return nil, nil 80 | }, 81 | } 82 | } 83 | 84 | func getImagesFromDeployment(obj interface{}) (interface{}, error) { 85 | if cis, ok := obj.(*controllerWithContainerInfos); ok { 86 | return cis, nil 87 | } 88 | 89 | deployment := obj.(*appsv1.Deployment) 90 | 91 | deploymentCopy := deployment.DeepCopy() 92 | 93 | return &controllerWithContainerInfos{ 94 | ObjectMeta: deploymentCopy.ObjectMeta, 95 | controllerKind: "Deployment", 96 | containerToImages: extractImagesFromContainers(deploymentCopy.Spec.Template.Spec.Containers), 97 | pullSecretReferences: deploymentCopy.Spec.Template.Spec.ImagePullSecrets, 98 | serviceAccountName: deploymentCopy.Spec.Template.Spec.ServiceAccountName, 99 | enabled: *deploymentCopy.Spec.Replicas > 0, 100 | }, nil 101 | } 102 | 103 | func getImagesFromStatefulSet(obj interface{}) (interface{}, error) { 104 | if cis, ok := obj.(*controllerWithContainerInfos); ok { 105 | return cis, nil 106 | } 107 | 108 | statefulSet := obj.(*appsv1.StatefulSet) 109 | 110 | statefulSetCopy := statefulSet.DeepCopy() 111 | 112 | return &controllerWithContainerInfos{ 113 | ObjectMeta: statefulSetCopy.ObjectMeta, 114 | controllerKind: "StatefulSet", 115 | containerToImages: extractImagesFromContainers(statefulSetCopy.Spec.Template.Spec.Containers), 116 | pullSecretReferences: statefulSetCopy.Spec.Template.Spec.ImagePullSecrets, 117 | serviceAccountName: statefulSetCopy.Spec.Template.Spec.ServiceAccountName, 118 | enabled: *statefulSetCopy.Spec.Replicas > 0, 119 | }, nil 120 | } 121 | 122 | func getImagesFromDaemonSet(obj interface{}) (interface{}, error) { 123 | if cis, ok := obj.(*controllerWithContainerInfos); ok { 124 | return cis, nil 125 | } 126 | 127 | daemonSet := obj.(*appsv1.DaemonSet) 128 | 129 | daemonSetCopy := daemonSet.DeepCopy() 130 | 131 | return &controllerWithContainerInfos{ 132 | ObjectMeta: daemonSetCopy.ObjectMeta, 133 | controllerKind: "DaemonSet", 134 | containerToImages: extractImagesFromContainers(daemonSetCopy.Spec.Template.Spec.Containers), 135 | pullSecretReferences: daemonSetCopy.Spec.Template.Spec.ImagePullSecrets, 136 | serviceAccountName: daemonSetCopy.Spec.Template.Spec.ServiceAccountName, 137 | enabled: daemonSetCopy.Status.CurrentNumberScheduled > 0, 138 | }, nil 139 | } 140 | 141 | func getImagesFromCronJob(obj interface{}) (interface{}, error) { 142 | if cis, ok := obj.(*controllerWithContainerInfos); ok { 143 | return cis, nil 144 | } 145 | 146 | cronJob := obj.(*batchv1.CronJob) 147 | 148 | cronJobCopy := cronJob.DeepCopy() 149 | 150 | return &controllerWithContainerInfos{ 151 | ObjectMeta: cronJobCopy.ObjectMeta, 152 | controllerKind: "CronJob", 153 | containerToImages: extractImagesFromContainers(cronJobCopy.Spec.JobTemplate.Spec.Template.Spec.Containers), 154 | pullSecretReferences: cronJobCopy.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets, 155 | serviceAccountName: cronJobCopy.Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName, 156 | enabled: !*cronJobCopy.Spec.Suspend, 157 | }, nil 158 | } 159 | 160 | func extractImagesFromContainers(containers []corev1.Container) map[string]string { 161 | ret := make(map[string]string) 162 | 163 | for _, container := range containers { 164 | ret[container.Name] = container.Image 165 | } 166 | 167 | return ret 168 | } 169 | 170 | func extractPullSecretKeysFromServiceAccount(namespace string, sa *corev1.ServiceAccount) (ret []string) { 171 | for _, ref := range sa.ImagePullSecrets { 172 | ret = append(ret, namespace+"/"+ref.Name) 173 | } 174 | 175 | return 176 | } 177 | 178 | func getCis(obj interface{}) *controllerWithContainerInfos { 179 | cis := obj.(*controllerWithContainerInfos) 180 | 181 | return cis 182 | } 183 | 184 | func (ci ControllerIndexers) ExtractPullSecretRefs(obj interface{}) (ret []string) { 185 | cis := obj.(*controllerWithContainerInfos) 186 | var pullSecretRefs []string 187 | for _, saRef := range cis.pullSecretReferences { 188 | pullSecretRefs = append(pullSecretRefs, fmt.Sprintf("%s/%s", cis.Namespace, saRef.Name)) 189 | } 190 | 191 | // Image pull secret defined in Pod's `spec.ImagePullSecrets` takes preference over the secret from ServiceAccount. 192 | // We are acting the same way as kubelet does: 193 | // https://github.com/kubernetes/kubernetes/blob/88b31814f4a55c0af1c7d2712ce736a8fe08887e/plugin/pkg/admission/serviceaccount/admission.go#L163-L168. 194 | if len(pullSecretRefs) == 0 { 195 | var serviceAccountName string 196 | if len(cis.serviceAccountName) > 0 { 197 | serviceAccountName = cis.serviceAccountName 198 | } else { 199 | serviceAccountName = "default" 200 | } 201 | 202 | saRaw, exists, err := ci.serviceAccountIndexer.GetByKey(fmt.Sprintf("%s/%s", cis.Namespace, serviceAccountName)) 203 | if err != nil { 204 | logrus.Warn(err) 205 | return 206 | } 207 | 208 | if exists { 209 | pullSecretRefs = append(pullSecretRefs, extractPullSecretKeysFromServiceAccount(cis.Namespace, saRaw.(*corev1.ServiceAccount))...) 210 | } 211 | } 212 | 213 | ret = append(ret, pullSecretRefs...) 214 | 215 | return 216 | } 217 | 218 | func (ci ControllerIndexers) GetObjectsByImageIndex(image string) (ret []interface{}) { 219 | for _, indexer := range []cache.Indexer{ci.deploymentIndexer, ci.statefulSetIndexer, ci.daemonSetIndexer, ci.cronJobIndexer} { 220 | objs, err := indexer.ByIndex(imageIndexName, image) 221 | if err != nil { 222 | panic(err) 223 | } 224 | 225 | ret = append(ret, objs...) 226 | } 227 | 228 | return 229 | } 230 | 231 | func (ci ControllerIndexers) GetContainerInfosForImage(image string) (ret []store.ContainerInfo) { 232 | objs := ci.GetObjectsByImageIndex(image) 233 | 234 | for _, obj := range objs { 235 | controllerWithInfos := obj.(*controllerWithContainerInfos) 236 | if !ci.validCi(controllerWithInfos) { 237 | continue 238 | } 239 | 240 | for k, v := range controllerWithInfos.containerToImages { 241 | if v != image { 242 | continue 243 | } 244 | 245 | ret = append(ret, store.ContainerInfo{ 246 | Namespace: controllerWithInfos.Namespace, 247 | ControllerKind: controllerWithInfos.controllerKind, 248 | ControllerName: controllerWithInfos.Name, 249 | Container: k, 250 | }) 251 | } 252 | } 253 | 254 | return 255 | } 256 | 257 | func (ci ControllerIndexers) GetImagePullSecrets(image string) []corev1.Secret { 258 | objs := ci.GetObjectsByImageIndex(image) 259 | 260 | var refSet = map[string]struct{}{} 261 | for _, obj := range objs { 262 | pullSecretRefs := ci.ExtractPullSecretRefs(obj) 263 | for _, ref := range pullSecretRefs { 264 | refSet[ref] = struct{}{} 265 | } 266 | } 267 | 268 | var dereferencedPullSecrets []corev1.Secret 269 | for ref := range refSet { 270 | secretObj, exists, err := ci.secretIndexer.GetByKey(ref) 271 | if err != nil { 272 | panic(err) 273 | } 274 | if !exists { 275 | continue 276 | } 277 | secretPtr := secretObj.(*corev1.Secret) 278 | dereferencedPullSecrets = append(dereferencedPullSecrets, *secretPtr) 279 | } 280 | 281 | if len(dereferencedPullSecrets) == 0 { 282 | return nil 283 | } 284 | return dereferencedPullSecrets 285 | } 286 | -------------------------------------------------------------------------------- /pkg/store/image_store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | "time" 7 | 8 | "github.com/gammazero/deque" 9 | "github.com/prometheus/client_golang/prometheus" 10 | "k8s.io/apimachinery/pkg/util/wait" 11 | ) 12 | 13 | type AvailabilityMode int 14 | 15 | const ( 16 | Available AvailabilityMode = iota 17 | Absent 18 | BadImageName 19 | RegistryUnavailable 20 | AuthnFailure 21 | AuthzFailure 22 | UnknownError 23 | ) 24 | 25 | var AvailabilityModeDescMap = map[AvailabilityMode]string{ 26 | Available: "available", 27 | Absent: "absent", 28 | BadImageName: "bad_image_format", 29 | RegistryUnavailable: "registry_unavailable", 30 | AuthnFailure: "authentication_failure", 31 | AuthzFailure: "authorization_failure", 32 | UnknownError: "unknown_error", 33 | } 34 | 35 | func (a AvailabilityMode) String() string { 36 | return AvailabilityModeDescMap[a] 37 | } 38 | 39 | type ContainerInfo struct { 40 | Namespace string 41 | ControllerKind string 42 | ControllerName string 43 | Container string 44 | } 45 | 46 | type ImageInfo struct { 47 | ContainerInfo map[ContainerInfo]struct{} 48 | AvailMode AvailabilityMode 49 | } 50 | 51 | type ImageStore struct { 52 | lock sync.RWMutex 53 | 54 | imageSet map[string]ImageInfo 55 | queue *deque.Deque[string] 56 | errQueue *deque.Deque[string] 57 | 58 | check checkFunc 59 | 60 | concurrentNormalChecks int 61 | concurrentErrorChecks int 62 | } 63 | 64 | type checkFunc func(imageName string) AvailabilityMode 65 | type gcFunc func(image string) []ContainerInfo 66 | 67 | func NewImageStore(check checkFunc, concurrentNormalChecks, concurrentErrorChecks int) *ImageStore { 68 | return &ImageStore{ 69 | imageSet: make(map[string]ImageInfo), 70 | queue: deque.New[string](2048, 2048), 71 | errQueue: deque.New[string](512, 512), 72 | 73 | check: check, 74 | 75 | concurrentNormalChecks: concurrentNormalChecks, 76 | concurrentErrorChecks: concurrentErrorChecks, 77 | } 78 | } 79 | 80 | func (s *ImageStore) RunGC(gc gcFunc) { 81 | go wait.Forever(func() { 82 | s.lock.Lock() 83 | defer s.lock.Unlock() 84 | 85 | for image, imgInfo := range s.imageSet { 86 | ci := gc(image) 87 | 88 | if len(ci) == 0 { 89 | delete(s.imageSet, image) 90 | 91 | continue 92 | } 93 | 94 | imgInfo.ContainerInfo = containerInfoSliceToSet(ci) 95 | s.imageSet[image] = imgInfo 96 | } 97 | 98 | }, 5*time.Minute) 99 | } 100 | 101 | func (s *ImageStore) ExtractMetrics() (ret []prometheus.Metric) { 102 | s.lock.RLock() 103 | defer s.lock.RUnlock() 104 | 105 | for imageName, info := range s.imageSet { 106 | for containerInfo := range info.ContainerInfo { 107 | ret = append(ret, newNamedConstMetrics(containerInfo.ControllerKind, containerInfo.ControllerName, 108 | containerInfo.Namespace, containerInfo.Container, imageName, info.AvailMode)...) 109 | } 110 | } 111 | 112 | return 113 | } 114 | 115 | func (s *ImageStore) ReconcileImage(imageName string, containerInfos []ContainerInfo) { 116 | s.lock.Lock() 117 | defer s.lock.Unlock() 118 | 119 | if len(containerInfos) == 0 { 120 | return 121 | } 122 | 123 | imageInfo, ok := s.imageSet[imageName] 124 | if !ok { 125 | containerInfoMap := containerInfoSliceToSet(containerInfos) 126 | 127 | s.imageSet[imageName] = ImageInfo{ContainerInfo: containerInfoMap} 128 | s.queue.PushBack(imageName) 129 | 130 | return 131 | } 132 | 133 | for _, ci := range containerInfos { 134 | imageInfo.ContainerInfo[ci] = struct{}{} 135 | } 136 | 137 | s.imageSet[imageName] = imageInfo 138 | } 139 | 140 | func (s *ImageStore) Check() { 141 | var ( 142 | normalChecks = s.concurrentNormalChecks 143 | errChecks = s.concurrentErrorChecks 144 | ) 145 | 146 | if qLen := s.queue.Len(); qLen < s.concurrentNormalChecks { 147 | normalChecks = qLen 148 | } 149 | if qLen := s.errQueue.Len(); qLen < s.concurrentErrorChecks { 150 | errChecks = qLen 151 | } 152 | 153 | errPops := s.popCheckPush(true, errChecks) 154 | 155 | if errPops < errChecks { 156 | normalChecks += errChecks - errPops 157 | } 158 | 159 | _ = s.popCheckPush(false, normalChecks) 160 | } 161 | 162 | func (s *ImageStore) popCheckPush(errQ bool, count int) (pops int) { 163 | for pops < count { 164 | s.lock.Lock() 165 | var imageRaw interface{} 166 | if errQ { 167 | imageRaw = s.errQueue.PopFront() 168 | } else { 169 | imageRaw = s.queue.PopFront() 170 | } 171 | pops++ 172 | image := imageRaw.(string) 173 | 174 | _, ok := s.imageSet[image] 175 | if !ok { 176 | s.lock.Unlock() 177 | continue 178 | } 179 | s.lock.Unlock() 180 | 181 | availMode := s.check(image) 182 | 183 | s.lock.Lock() 184 | 185 | imageInfo, ok := s.imageSet[image] 186 | if !ok { 187 | s.lock.Unlock() 188 | continue 189 | } 190 | imageInfo.AvailMode = availMode 191 | s.imageSet[image] = imageInfo 192 | 193 | if availMode == Available { 194 | s.queue.PushBack(image) 195 | } else { 196 | s.errQueue.PushBack(image) 197 | } 198 | 199 | s.lock.Unlock() 200 | } 201 | 202 | return 203 | } 204 | 205 | func containerInfoSliceToSet(containerInfos []ContainerInfo) map[ContainerInfo]struct{} { 206 | var containerInfoMap = make(map[ContainerInfo]struct{}) 207 | for _, ci := range containerInfos { 208 | containerInfoMap[ci] = struct{}{} 209 | } 210 | 211 | return containerInfoMap 212 | } 213 | 214 | func newNamedConstMetrics(ownerKind, ownerName, namespace, container, image string, avalMode AvailabilityMode) (ret []prometheus.Metric) { 215 | labels := map[string]string{ 216 | "namespace": namespace, 217 | "container": container, 218 | "image": image, 219 | "kind": strings.ToLower(ownerKind), 220 | "name": ownerName, 221 | } 222 | 223 | return getMetric(labels, avalMode) 224 | } 225 | 226 | func getMetric(labels map[string]string, mode AvailabilityMode) (ret []prometheus.Metric) { 227 | for availMode, desc := range AvailabilityModeDescMap { 228 | var value float64 229 | if availMode == mode { 230 | value = 1 231 | } 232 | 233 | ret = append(ret, prometheus.MustNewConstMetric( 234 | prometheus.NewDesc("k8s_image_availability_exporter_"+desc, "", nil, labels), 235 | prometheus.GaugeValue, 236 | value, 237 | )) 238 | } 239 | 240 | return 241 | } 242 | -------------------------------------------------------------------------------- /pkg/store/image_store_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/prometheus/client_golang/prometheus" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func insertImagesIntoStore(t *testing.T, store *ImageStore, successfulChecks, failedChecks int, info []ContainerInfo) { 14 | t.Helper() 15 | 16 | for i := 0; i < successfulChecks; i++ { 17 | store.ReconcileImage(fmt.Sprintf("test_%d", i), info) 18 | } 19 | for i := 0; i < failedChecks; i++ { 20 | store.ReconcileImage(fmt.Sprintf("fail_%d", i), info) 21 | } 22 | } 23 | 24 | func TestImageStore_AddOrUpdateImage(t *testing.T) { 25 | store := NewImageStore(reconcile(t), 2, 3) 26 | 27 | info := []ContainerInfo{ 28 | { 29 | Namespace: "test", 30 | ControllerKind: "Deployment", 31 | ControllerName: "test", 32 | Container: "test", 33 | }, 34 | { 35 | Namespace: "test", 36 | ControllerKind: "StatefulSet", 37 | ControllerName: "test", 38 | Container: "test", 39 | }, 40 | } 41 | 42 | insertImagesIntoStore(t, store, 3, 2, info) 43 | 44 | store.Check() 45 | 46 | metrics := store.ExtractMetrics() 47 | require.Len(t, metrics, 70) 48 | } 49 | 50 | func reconcile(t *testing.T) func(imageName string) AvailabilityMode { 51 | t.Helper() 52 | 53 | return func(imageName string) AvailabilityMode { 54 | if strings.HasPrefix(imageName, "fail_") { 55 | return UnknownError 56 | } 57 | 58 | return Available 59 | } 60 | } 61 | 62 | func TestImageStore_ExtractMetrics(t *testing.T) { 63 | t.Parallel() 64 | 65 | t.Run("no images", func(t *testing.T) { 66 | t.Parallel() 67 | 68 | store := NewImageStore(reconcile(t), 2, 3) 69 | insertImagesIntoStore(t, store, 0, 0, nil) 70 | store.Check() 71 | 72 | metrics := store.ExtractMetrics() 73 | assert.Empty(t, metrics) 74 | }) 75 | 76 | t.Run("one container", func(t *testing.T) { 77 | t.Parallel() 78 | 79 | store := NewImageStore(reconcile(t), 2, 3) 80 | 81 | info := []ContainerInfo{ 82 | { 83 | Namespace: "test_ns", 84 | ControllerKind: "Deployment", 85 | ControllerName: "test_name", 86 | Container: "test_container", 87 | }, 88 | } 89 | 90 | expectedMetrics := []*prometheus.Desc{ 91 | prometheus.NewDesc( 92 | "k8s_image_availability_exporter_registry_unavailable", 93 | "", 94 | nil, 95 | prometheus.Labels{ 96 | "container": "test_container", 97 | "image": "test_0", 98 | "kind": "deployment", 99 | "name": "test_name", 100 | "namespace": "test_ns", 101 | }, 102 | ), 103 | prometheus.NewDesc( 104 | "k8s_image_availability_exporter_authentication_failure", 105 | "", 106 | nil, 107 | prometheus.Labels{ 108 | "container": "test_container", 109 | "image": "test_0", 110 | "kind": "deployment", 111 | "name": "test_name", 112 | "namespace": "test_ns", 113 | }, 114 | ), 115 | prometheus.NewDesc( 116 | "k8s_image_availability_exporter_authorization_failure", 117 | "", 118 | nil, 119 | prometheus.Labels{ 120 | "container": "test_container", 121 | "image": "test_0", 122 | "kind": "deployment", 123 | "name": "test_name", 124 | "namespace": "test_ns", 125 | }, 126 | ), 127 | prometheus.NewDesc( 128 | "k8s_image_availability_exporter_unknown_error", 129 | "", 130 | nil, 131 | prometheus.Labels{ 132 | "container": "test_container", 133 | "image": "test_0", 134 | "kind": "deployment", 135 | "name": "test_name", 136 | "namespace": "test_ns", 137 | }, 138 | ), 139 | prometheus.NewDesc( 140 | "k8s_image_availability_exporter_available", 141 | "", 142 | nil, 143 | prometheus.Labels{ 144 | "container": "test_container", 145 | "image": "test_0", 146 | "kind": "deployment", 147 | "name": "test_name", 148 | "namespace": "test_ns", 149 | }, 150 | ), 151 | prometheus.NewDesc( 152 | "k8s_image_availability_exporter_absent", 153 | "", 154 | nil, 155 | prometheus.Labels{ 156 | "container": "test_container", 157 | "image": "test_0", 158 | "kind": "deployment", 159 | "name": "test_name", 160 | "namespace": "test_ns", 161 | }, 162 | ), 163 | prometheus.NewDesc( 164 | "k8s_image_availability_exporter_bad_image_format", 165 | "", 166 | nil, 167 | prometheus.Labels{ 168 | "container": "test_container", 169 | "image": "test_0", 170 | "kind": "deployment", 171 | "name": "test_name", 172 | "namespace": "test_ns", 173 | }, 174 | ), 175 | } 176 | 177 | insertImagesIntoStore(t, store, 1, 0, info) 178 | store.Check() 179 | 180 | metrics := store.ExtractMetrics() 181 | require.Len(t, metrics, len(expectedMetrics)) 182 | 183 | expectedMetricsStr := make([]string, 0, len(expectedMetrics)) 184 | for _, em := range expectedMetrics { 185 | expectedMetricsStr = append(expectedMetricsStr, em.String()) 186 | } 187 | 188 | returnedMetricsStr := make([]string, 0, len(metrics)) 189 | for _, m := range metrics { 190 | returnedMetricsStr = append(returnedMetricsStr, m.Desc().String()) 191 | } 192 | 193 | assert.ElementsMatch(t, expectedMetricsStr, returnedMetricsStr) 194 | }) 195 | 196 | t.Run("two containers, different kind", func(t *testing.T) { 197 | t.Parallel() 198 | 199 | store := NewImageStore(reconcile(t), 2, 3) 200 | 201 | info := []ContainerInfo{ 202 | { 203 | Namespace: "test_ns", 204 | ControllerKind: "Deployment", 205 | ControllerName: "test_name", 206 | Container: "test_container", 207 | }, 208 | { 209 | Namespace: "test_ns2", 210 | ControllerKind: "StatefulSet", 211 | ControllerName: "test_name2", 212 | Container: "test_container2", 213 | }, 214 | } 215 | 216 | expectedMetrics := []*prometheus.Desc{ 217 | prometheus.NewDesc( 218 | "k8s_image_availability_exporter_registry_unavailable", 219 | "", 220 | nil, 221 | prometheus.Labels{ 222 | "container": "test_container", 223 | "image": "test_0", 224 | "kind": "deployment", 225 | "name": "test_name", 226 | "namespace": "test_ns", 227 | }, 228 | ), 229 | prometheus.NewDesc( 230 | "k8s_image_availability_exporter_authentication_failure", 231 | "", 232 | nil, 233 | prometheus.Labels{ 234 | "container": "test_container", 235 | "image": "test_0", 236 | "kind": "deployment", 237 | "name": "test_name", 238 | "namespace": "test_ns", 239 | }, 240 | ), 241 | prometheus.NewDesc( 242 | "k8s_image_availability_exporter_authorization_failure", 243 | "", 244 | nil, 245 | prometheus.Labels{ 246 | "container": "test_container", 247 | "image": "test_0", 248 | "kind": "deployment", 249 | "name": "test_name", 250 | "namespace": "test_ns", 251 | }, 252 | ), 253 | prometheus.NewDesc( 254 | "k8s_image_availability_exporter_unknown_error", 255 | "", 256 | nil, 257 | prometheus.Labels{ 258 | "container": "test_container", 259 | "image": "test_0", 260 | "kind": "deployment", 261 | "name": "test_name", 262 | "namespace": "test_ns", 263 | }, 264 | ), 265 | prometheus.NewDesc( 266 | "k8s_image_availability_exporter_available", 267 | "", 268 | nil, 269 | prometheus.Labels{ 270 | "container": "test_container", 271 | "image": "test_0", 272 | "kind": "deployment", 273 | "name": "test_name", 274 | "namespace": "test_ns", 275 | }, 276 | ), 277 | prometheus.NewDesc( 278 | "k8s_image_availability_exporter_absent", 279 | "", 280 | nil, 281 | prometheus.Labels{ 282 | "container": "test_container", 283 | "image": "test_0", 284 | "kind": "deployment", 285 | "name": "test_name", 286 | "namespace": "test_ns", 287 | }, 288 | ), 289 | prometheus.NewDesc( 290 | "k8s_image_availability_exporter_bad_image_format", 291 | "", 292 | nil, 293 | prometheus.Labels{ 294 | "container": "test_container", 295 | "image": "test_0", 296 | "kind": "deployment", 297 | "name": "test_name", 298 | "namespace": "test_ns", 299 | }, 300 | ), 301 | prometheus.NewDesc( 302 | "k8s_image_availability_exporter_registry_unavailable", 303 | "", 304 | nil, 305 | prometheus.Labels{ 306 | "container": "test_container2", 307 | "image": "test_0", 308 | "kind": "statefulset", 309 | "name": "test_name2", 310 | "namespace": "test_ns2", 311 | }, 312 | ), 313 | prometheus.NewDesc( 314 | "k8s_image_availability_exporter_authentication_failure", 315 | "", 316 | nil, 317 | prometheus.Labels{ 318 | "container": "test_container2", 319 | "image": "test_0", 320 | "kind": "statefulset", 321 | "name": "test_name2", 322 | "namespace": "test_ns2", 323 | }, 324 | ), 325 | prometheus.NewDesc( 326 | "k8s_image_availability_exporter_authorization_failure", 327 | "", 328 | nil, 329 | prometheus.Labels{ 330 | "container": "test_container2", 331 | "image": "test_0", 332 | "kind": "statefulset", 333 | "name": "test_name2", 334 | "namespace": "test_ns2", 335 | }, 336 | ), 337 | prometheus.NewDesc( 338 | "k8s_image_availability_exporter_unknown_error", 339 | "", 340 | nil, 341 | prometheus.Labels{ 342 | "container": "test_container2", 343 | "image": "test_0", 344 | "kind": "statefulset", 345 | "name": "test_name2", 346 | "namespace": "test_ns2", 347 | }, 348 | ), 349 | prometheus.NewDesc( 350 | "k8s_image_availability_exporter_available", 351 | "", 352 | nil, 353 | prometheus.Labels{ 354 | "container": "test_container2", 355 | "image": "test_0", 356 | "kind": "statefulset", 357 | "name": "test_name2", 358 | "namespace": "test_ns2", 359 | }, 360 | ), 361 | prometheus.NewDesc( 362 | "k8s_image_availability_exporter_absent", 363 | "", 364 | nil, 365 | prometheus.Labels{ 366 | "container": "test_container2", 367 | "image": "test_0", 368 | "kind": "statefulset", 369 | "name": "test_name2", 370 | "namespace": "test_ns2", 371 | }, 372 | ), 373 | prometheus.NewDesc( 374 | "k8s_image_availability_exporter_bad_image_format", 375 | "", 376 | nil, 377 | prometheus.Labels{ 378 | "container": "test_container2", 379 | "image": "test_0", 380 | "kind": "statefulset", 381 | "name": "test_name2", 382 | "namespace": "test_ns2", 383 | }, 384 | ), 385 | } 386 | 387 | insertImagesIntoStore(t, store, 1, 0, info) 388 | store.Check() 389 | 390 | metrics := store.ExtractMetrics() 391 | require.Len(t, metrics, len(expectedMetrics)) 392 | 393 | expectedMetricsStr := make([]string, 0, len(expectedMetrics)) 394 | for _, em := range expectedMetrics { 395 | expectedMetricsStr = append(expectedMetricsStr, em.String()) 396 | } 397 | 398 | returnedMetricsStr := make([]string, 0, len(metrics)) 399 | for _, m := range metrics { 400 | returnedMetricsStr = append(returnedMetricsStr, m.Desc().String()) 401 | } 402 | 403 | assert.ElementsMatch(t, expectedMetricsStr, returnedMetricsStr) 404 | }) 405 | } 406 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | // Version can be set via: 4 | // -ldflags="-X 'github.com/flant/k8s-image-availability-exporter/pkg/version.Version=$TAG'" 5 | var Version = "dev" 6 | --------------------------------------------------------------------------------