├── .gitignore ├── .dockerignore ├── Makefile ├── .github └── workflows │ ├── test.yml │ └── publish.yml ├── Dockerfile ├── deploy ├── cronjob.yaml └── serviceaccount.yaml ├── go.mod ├── README.md ├── LICENSE ├── go.sum └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | imago 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | ** 2 | !*.go 3 | !go.mod 4 | !go.sum 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: build 2 | .PHONY: all 3 | 4 | build: main.go 5 | go build 6 | .PHONY: build 7 | 8 | docker: 9 | docker build --pull -t philpep/imago . 10 | .PHONY: docker 11 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: checkout 14 | uses: actions/checkout@v4 15 | - name: setup 16 | uses: actions/setup-go@v4 17 | with: 18 | go-version: "1.24" 19 | - name: lint 20 | uses: golangci/golangci-lint-action@v8 21 | - name: build 22 | run: | 23 | go build 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-alpine as builder 2 | RUN apk add --no-cache git ca-certificates 3 | RUN adduser -D -u 1000 -h /var/lib/imago imago 4 | USER imago 5 | WORKDIR /var/lib/imago 6 | COPY . . 7 | RUN CGO_ENABLED=0 go build 8 | 9 | FROM scratch 10 | COPY --from=builder /var/lib/imago/imago /usr/bin/ 11 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 12 | COPY --from=builder /var/empty /var/lib/imago 13 | USER 1000 14 | ENV USER=imago 15 | ENV HOME=/var/lib/imago 16 | WORKDIR /var/lib/imago 17 | ENTRYPOINT ["/usr/bin/imago"] 18 | -------------------------------------------------------------------------------- /deploy/cronjob.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: CronJob 3 | metadata: 4 | name: imago 5 | namespace: default 6 | spec: 7 | schedule: "0 4 * * *" 8 | concurrencyPolicy: Forbid 9 | jobTemplate: 10 | spec: 11 | template: 12 | metadata: 13 | labels: 14 | k8s-app: imago 15 | spec: 16 | restartPolicy: Never 17 | serviceAccount: imago 18 | serviceAccountName: imago 19 | containers: 20 | - name: imago 21 | image: philpep/imago 22 | imagePullPolicy: Always 23 | args: ["--update"] 24 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: checkout 13 | uses: actions/checkout@v5 14 | - name: setup 15 | uses: actions/setup-go@v6 16 | with: 17 | go-version: "1.24" 18 | - name: build 19 | run: | 20 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o imago-linux-amd64 21 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o imago-darwin-amd64 22 | - uses: softprops/action-gh-release@v2 23 | with: 24 | files: | 25 | imago-linux-amd64 26 | imago-darwin-amd64 27 | -------------------------------------------------------------------------------- /deploy/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: imago 6 | namespace: default 7 | --- 8 | apiVersion: rbac.authorization.k8s.io/v1 9 | kind: ClusterRole 10 | metadata: 11 | name: imago 12 | rules: 13 | - apiGroups: 14 | - "" 15 | - apps 16 | resources: 17 | - pods 18 | - replicasets 19 | - statefulsets 20 | verbs: 21 | - list 22 | - apiGroups: 23 | - "" 24 | - batch 25 | resources: 26 | - cronjobs 27 | verbs: 28 | - get 29 | - list 30 | - update 31 | - apiGroups: 32 | - "" 33 | - apps 34 | resources: 35 | - daemonsets 36 | - deployments 37 | - statefulsets 38 | - replicasets 39 | verbs: 40 | - get 41 | - list 42 | - update 43 | --- 44 | kind: ClusterRoleBinding 45 | apiVersion: rbac.authorization.k8s.io/v1 46 | metadata: 47 | name: imago 48 | roleRef: 49 | kind: ClusterRole 50 | name: imago 51 | apiGroup: rbac.authorization.k8s.io 52 | subjects: 53 | - kind: ServiceAccount 54 | name: imago 55 | namespace: default 56 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/philpep/imago 2 | 3 | go 1.23.3 4 | 5 | toolchain go1.24.4 6 | 7 | require ( 8 | github.com/containers/image/v5 v5.36.2 9 | k8s.io/api v0.32.10 10 | k8s.io/apimachinery v0.32.10 11 | k8s.io/client-go v0.32.10 12 | ) 13 | 14 | require ( 15 | github.com/BurntSushi/toml v1.5.0 // indirect 16 | github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect 17 | github.com/containers/ocicrypt v1.2.1 // indirect 18 | github.com/containers/storage v1.59.1 // indirect 19 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 20 | github.com/distribution/reference v0.6.0 // indirect 21 | github.com/docker/distribution v2.8.3+incompatible // indirect 22 | github.com/docker/docker v28.3.3+incompatible // indirect 23 | github.com/docker/docker-credential-helpers v0.9.3 // indirect 24 | github.com/docker/go-connections v0.5.0 // indirect 25 | github.com/docker/go-units v0.5.0 // indirect 26 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 27 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 28 | github.com/go-logr/logr v1.4.3 // indirect 29 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 30 | github.com/go-openapi/jsonreference v0.20.2 // indirect 31 | github.com/go-openapi/swag v0.23.0 // indirect 32 | github.com/gogo/protobuf v1.3.2 // indirect 33 | github.com/golang/protobuf v1.5.4 // indirect 34 | github.com/google/gnostic-models v0.6.8 // indirect 35 | github.com/google/go-cmp v0.7.0 // indirect 36 | github.com/google/gofuzz v1.2.0 // indirect 37 | github.com/google/uuid v1.6.0 // indirect 38 | github.com/gorilla/mux v1.8.1 // indirect 39 | github.com/josharian/intern v1.0.0 // indirect 40 | github.com/json-iterator/go v1.1.12 // indirect 41 | github.com/mailru/easyjson v0.7.7 // indirect 42 | github.com/moby/sys/capability v0.4.0 // indirect 43 | github.com/moby/sys/mountinfo v0.7.2 // indirect 44 | github.com/moby/sys/user v0.4.0 // indirect 45 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 46 | github.com/modern-go/reflect2 v1.0.2 // indirect 47 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 48 | github.com/opencontainers/go-digest v1.0.0 // indirect 49 | github.com/opencontainers/image-spec v1.1.1 // indirect 50 | github.com/opencontainers/runtime-spec v1.2.1 // indirect 51 | github.com/pkg/errors v0.9.1 // indirect 52 | github.com/sirupsen/logrus v1.9.3 // indirect 53 | github.com/spf13/pflag v1.0.5 // indirect 54 | github.com/x448/float16 v0.8.4 // indirect 55 | golang.org/x/net v0.41.0 // indirect 56 | golang.org/x/oauth2 v0.30.0 // indirect 57 | golang.org/x/sys v0.34.0 // indirect 58 | golang.org/x/term v0.33.0 // indirect 59 | golang.org/x/text v0.27.0 // indirect 60 | golang.org/x/time v0.11.0 // indirect 61 | google.golang.org/protobuf v1.36.6 // indirect 62 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 63 | gopkg.in/inf.v0 v0.9.1 // indirect 64 | gopkg.in/yaml.v3 v3.0.1 // indirect 65 | k8s.io/klog/v2 v2.130.1 // indirect 66 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 67 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 68 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 69 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 70 | sigs.k8s.io/yaml v1.4.0 // indirect 71 | ) 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Imago 2 | 3 | [![Build Status](https://travis-ci.org/philpep/imago.svg?branch=master)](https://travis-ci.org/philpep/imago) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/philpep/imago)](https://goreportcard.com/report/github.com/philpep/imago) 5 | 6 | This project aims to ease continuous delivery of docker images in a 7 | kubernetes cluster. 8 | 9 | [Imago](https://en.wikipedia.org/wiki/Imago) is the last stage of an 10 | insect, it also refer to `image` and `go` (golang). 11 | 12 | `imago` looks for kubernetes `Deployments`, `DaemonSets`, `StatefulSet` and `CronJobs` 13 | configuration and update them to use the latest image sha256 digest from 14 | the docker repository. 15 | 16 | This is useful to handle the following cases: 17 | 18 | - image is rebuilt for security fixes 19 | - ensure all pods use exactly the same image 20 | - image is rebuilt by CI for continuous delivery 21 | 22 | `imago` ensure your pods are running the latest build. 23 | 24 | ## How it works ? 25 | 26 | `imago` looks for `Deployments`, `DaemonSets`, `StatefulSet` and `CronJob` configuration, get the 27 | latest sha256 digest from registry and update containers specifications 28 | to set image to the corresponding `registry/image@sha256:...` notation. 29 | It track the original image specification in the `imago-config-spec` 30 | annotation. 31 | 32 | Alternatively, with the `-restart` option, it check running pods sha256 and 33 | just restart resource that need to use newer images (assuming imagePullPolicy 34 | is Always). This method is slower than `-update` but it leave the container 35 | image in manifests untouched. 36 | 37 | ## Arguments 38 | 39 | $ imago --help 40 | Usage of imago: 41 | -A Check deployments and daemonsets on all namespaces (shorthand) (default false) 42 | -all-namespaces 43 | Check deployments and daemonsets on all namespaces (default false) 44 | -check-pods 45 | check image digests of running pods (default false) 46 | -field-selector string 47 | Kubernetes field-selector 48 | example: metadata.name=myapp 49 | -kubeconfig string 50 | kube config file (default "~/.kube/config") 51 | -l string 52 | Kubernetes labels selectors 53 | Warning: applies to Deployment, DaemonSet, StatefulSet and CronJob, not pods ! 54 | -n value 55 | Check deployments and daemonsets in given namespaces (default to current namespace) 56 | -restart 57 | rollout restart deployments and daemonsets to use newer images, implies -check-pods and assume imagePullPolicy is Always (default false) 58 | -update 59 | update deployments and daemonsets to use newer images (default false) 60 | -x value 61 | Check deployments and daemonsets in all namespaces except given namespaces (implies --all-namespaces) 62 | 63 | By default, `imago` doesn't update your deployments, unless invoked with 64 | `--update`. 65 | 66 | The `--check-pods` is a less intrusive mode where update is done only if 67 | one of the running pods doesn't run on latest digest image. 68 | 69 | ## Example output 70 | 71 | $ imago --update 72 | 2019/02/11 17:55:21 checking default/Deployment/aptly: 73 | 2019/02/11 17:55:21 aptly ok 74 | 2019/02/11 17:55:21 nginx ok 75 | 2019/02/11 17:55:22 checking default/Deployment/kibana: 76 | 2019/02/11 17:55:22 kibana ok 77 | 2019/02/11 17:55:22 nginx ok 78 | 2019/02/11 17:55:22 update default/Deployment/philpep.org 79 | 2019/02/11 17:55:22 checking DaemonSet/fluentd: 80 | 2019/02/11 17:55:22 fluentd has to be updated from r.in.philpep.org/fluentd to r.in.philpep.org/fluentd@sha256:6a92af8a9db2ca243e0eba8d401cec11b124822e15b558b35ab45825ed4d1f54 81 | 2019/02/11 17:55:22 update default/DaemonSet/fluentd 82 | 83 | 84 | ## Install and run 85 | 86 | ### From the command line 87 | 88 | Assuming you have a working `~/.kube/config` file, just download and 89 | build the code: 90 | 91 | $ go get github.com/philpep/imago/... 92 | $ $(go env GOPATH)/bin/imago --help 93 | 94 | ### From the docker image 95 | 96 | Assuming you have a working `~/.kube/config` file: 97 | 98 | $ docker pull philpep/imago 99 | $ docker run --rm -it -u $(id -u) -v ~/.kube/config:/var/lib/imago/.kube/config philpep/imago --help 100 | 101 | ### From a pre-built binary 102 | 103 | Check [releases page](https://github.com/philpep/imago/releases). 104 | 105 | ### Inside the cluster 106 | 107 | You can run `imago` inside the cluster, for instance in a `CronJob` 108 | kubernetes object that runs every day. 109 | 110 | See the 111 | [ServiceAccount](https://raw.githubusercontent.com/philpep/imago/master/deploy/serviceaccount.yaml) 112 | and 113 | [CronJob](https://raw.githubusercontent.com/philpep/imago/master/deploy/cronjob.yaml) 114 | objects. 115 | 116 | $ kubectl apply -f deploy/serviceaccount.yaml 117 | $ kubectl apply -f deploy/cronjob.yaml 118 | 119 | 120 | ## Docker credentials 121 | 122 | Image will looks for docker registry credentials in ~/.docker/config.json (e.g. 123 | /var/lib/imago/.docker/config.json in docker image). 124 | So, in case you're using `imagePullSecrets`, you will have to mount the secret here. 125 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= 2 | github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 3 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 4 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 5 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 6 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= 8 | github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= 9 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 10 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 11 | github.com/containers/image/v5 v5.36.2 h1:GcxYQyAHRF/pLqR4p4RpvKllnNL8mOBn0eZnqJbfTwk= 12 | github.com/containers/image/v5 v5.36.2/go.mod h1:b4GMKH2z/5t6/09utbse2ZiLK/c72GuGLFdp7K69eA4= 13 | github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA= 14 | github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY= 15 | github.com/containers/ocicrypt v1.2.1 h1:0qIOTT9DoYwcKmxSt8QJt+VzMY18onl9jUXsxpVhSmM= 16 | github.com/containers/ocicrypt v1.2.1/go.mod h1:aD0AAqfMp0MtwqWgHM1bUwe1anx0VazI108CRrSKINQ= 17 | github.com/containers/storage v1.59.1 h1:11Zu68MXsEQGBBd+GadPrHPpWeqjKS8hJDGiAHgIqDs= 18 | github.com/containers/storage v1.59.1/go.mod h1:KoAYHnAjP3/cTsRS+mmWZGkufSY2GACiKQ4V3ZLQnR0= 19 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 20 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 23 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 25 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 26 | github.com/docker/cli v28.3.2+incompatible h1:mOt9fcLE7zaACbxW1GeS65RI67wIJrTnqS3hP2huFsY= 27 | github.com/docker/cli v28.3.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= 28 | github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= 29 | github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 30 | github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= 31 | github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 32 | github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= 33 | github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= 34 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 35 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 36 | github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= 37 | github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= 38 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 39 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 40 | github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= 41 | github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 42 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 43 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 44 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 45 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 46 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 47 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 48 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 49 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 50 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 51 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 52 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 53 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 54 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 55 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 56 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 57 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 58 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 59 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 60 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 61 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 62 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 63 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 64 | github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= 65 | github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 66 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 67 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 68 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 69 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 70 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 71 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 72 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 73 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 74 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 75 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 76 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 77 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 78 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 79 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 80 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 81 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 82 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 83 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 84 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 85 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 86 | github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= 87 | github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= 88 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 89 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 90 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 91 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 92 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 93 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 94 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 95 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 96 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 97 | github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk= 98 | github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I= 99 | github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= 100 | github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= 101 | github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= 102 | github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= 103 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 104 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 105 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 106 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 107 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 108 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 109 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 110 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 111 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 112 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 113 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 114 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 115 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 116 | github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= 117 | github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= 118 | github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww= 119 | github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= 120 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 121 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 122 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 123 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 124 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 125 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 126 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 127 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 128 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 129 | github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= 130 | github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 131 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 132 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 133 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 134 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 135 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 136 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 137 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 138 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 139 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 140 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 141 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 142 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 143 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 144 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 145 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 146 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 147 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 148 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 149 | github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= 150 | github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 151 | github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= 152 | github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= 153 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 154 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 155 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 156 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 157 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 158 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 159 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= 160 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= 161 | go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= 162 | go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= 163 | go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= 164 | go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 165 | go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= 166 | go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= 167 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 168 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 169 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 170 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 171 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 172 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 173 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 174 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 175 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 176 | golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= 177 | golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= 178 | golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= 179 | golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 180 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 181 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 182 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 183 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 184 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 185 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 186 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 187 | golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= 188 | golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 189 | golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= 190 | golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= 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.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= 194 | golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= 195 | golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 196 | golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 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.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= 202 | golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= 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.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 208 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 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 | k8s.io/api v0.32.10 h1:ocp4turNfa1V40TuBW/LuA17TeXG9g/GI2ebg0KxBNk= 220 | k8s.io/api v0.32.10/go.mod h1:AsMsc4b6TuampYqgMEGSv0HBFpRS4BlKTXAVCAa7oF4= 221 | k8s.io/apimachinery v0.32.10 h1:SAg2kUPLYRcBJQj66oniP1BnXSqw+l1GvJFsJlBmVvQ= 222 | k8s.io/apimachinery v0.32.10/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= 223 | k8s.io/client-go v0.32.10 h1:MFmIjsKtcnn7mStjrJG1ZW2WzLsKKn6ZtL9hHM/W0xU= 224 | k8s.io/client-go v0.32.10/go.mod h1:qJy/Ws3zSwnu/nD75D+/of1uxbwWHxrYT5P3FuobVLI= 225 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 226 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 227 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= 228 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= 229 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= 230 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 231 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= 232 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= 233 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= 234 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= 235 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 236 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 237 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Philippe Pepiot 3 | 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | package main 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "flag" 21 | "fmt" 22 | "log" 23 | "os" 24 | "os/user" 25 | "path/filepath" 26 | "regexp" 27 | "strings" 28 | "time" 29 | 30 | v1 "k8s.io/api/core/v1" 31 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 | "k8s.io/client-go/kubernetes" 33 | _ "k8s.io/client-go/plugin/pkg/client/auth" 34 | "k8s.io/client-go/rest" 35 | "k8s.io/client-go/tools/clientcmd" 36 | "k8s.io/client-go/util/retry" 37 | 38 | "github.com/containers/image/v5/docker" 39 | "github.com/containers/image/v5/docker/reference" 40 | "github.com/containers/image/v5/manifest" 41 | ) 42 | 43 | var digestCache = map[string]string{} 44 | 45 | // GetDigest return the docker digest of given image name 46 | func GetDigest(ctx context.Context, name string) (string, error) { 47 | if digestCache[name] != "" { 48 | return digestCache[name], nil 49 | } 50 | ref, err := docker.ParseReference("//" + name) 51 | if err != nil { 52 | return "", err 53 | } 54 | img, err := ref.NewImage(ctx, nil) 55 | if err != nil { 56 | return "", err 57 | } 58 | defer func() { 59 | if err := img.Close(); err != nil { 60 | log.Print(err) 61 | } 62 | }() 63 | b, _, err := img.Manifest(ctx) 64 | if err != nil { 65 | return "", err 66 | } 67 | digest, err := manifest.Digest(b) 68 | if err != nil { 69 | return "", err 70 | } 71 | digeststr := string(digest) 72 | digestCache[name] = digeststr 73 | return digeststr, nil 74 | } 75 | 76 | // Config represent a imago configuration 77 | type Config struct { 78 | cluster *kubernetes.Clientset 79 | namespace string 80 | policy string 81 | checkpods bool 82 | xnamespace *arrayFlags 83 | context context.Context 84 | } 85 | 86 | // NewConfig initialize a new imago config 87 | func NewConfig(kubeconfig string, namespace string, allnamespaces bool, xnamespace *arrayFlags, policy string, checkpods bool, ctx context.Context) (*Config, error) { 88 | c := &Config{policy: policy, checkpods: checkpods, xnamespace: xnamespace, context: ctx} 89 | var err error 90 | var clusterConfig *rest.Config 91 | 92 | setNamespace := func(incluster bool) error { 93 | if allnamespaces { 94 | c.namespace = "" 95 | } else if namespace != "" { 96 | c.namespace = namespace 97 | } else { 98 | if incluster { 99 | c.namespace = inClusterNamespace() 100 | } else { 101 | c.namespace = outClusterNamespace(kubeconfig) 102 | } 103 | if c.namespace == "" { 104 | c.namespace = "default" 105 | } 106 | } 107 | return nil 108 | } 109 | 110 | if inClusterClientPossible() { 111 | clusterConfig, err = rest.InClusterConfig() 112 | if err != nil { 113 | return nil, err 114 | } 115 | if err = setNamespace(true); err != nil { 116 | return nil, err 117 | } 118 | } else { 119 | clusterConfig, err = clientcmd.BuildConfigFromFlags("", kubeconfig) 120 | if err != nil { 121 | return nil, err 122 | } 123 | if err = setNamespace(false); err != nil { 124 | return nil, err 125 | } 126 | } 127 | c.cluster, err = kubernetes.NewForConfig(clusterConfig) 128 | if err != nil { 129 | return nil, err 130 | } 131 | return c, nil 132 | } 133 | 134 | // Update Deployment, DaemonSet and CronJob matching given selectors 135 | func (c *Config) Update(fieldSelector, labelSelector string) error { 136 | ctx := c.context 137 | client := c.cluster.AppsV1() 138 | opts := metav1.ListOptions{FieldSelector: fieldSelector, LabelSelector: labelSelector} 139 | deployments, err := client.Deployments(c.namespace).List(ctx, opts) 140 | if err != nil { 141 | return err 142 | } 143 | failed := make([]string, 0) 144 | for _, d := range deployments.Items { 145 | if err = c.process("Deployment", &d.ObjectMeta, &d.Spec.Template); err != nil { 146 | log.Print(err) 147 | failed = append(failed, fmt.Sprintf("failed to check %s/Deployment/%s: %s", d.Namespace, d.Name, err)) 148 | } 149 | } 150 | daemonsets, err := client.DaemonSets(c.namespace).List(ctx, opts) 151 | if err != nil { 152 | return err 153 | } 154 | for _, ds := range daemonsets.Items { 155 | if err := c.process("DaemonSet", &ds.ObjectMeta, &ds.Spec.Template); err != nil { 156 | failed = append(failed, fmt.Sprintf("failed to check %s/DaemonSet/%s: %s", ds.Namespace, ds.Name, err)) 157 | } 158 | } 159 | statefulsets, err := client.StatefulSets(c.namespace).List(ctx, opts) 160 | if err != nil { 161 | return err 162 | } 163 | for _, sts := range statefulsets.Items { 164 | if err := c.process("StatefulSet", &sts.ObjectMeta, &sts.Spec.Template); err != nil { 165 | failed = append(failed, fmt.Sprintf("failed to check %s/StatefulSet/%s: %s", sts.Namespace, sts.Name, err)) 166 | } 167 | } 168 | cronjobs, err := c.cluster.BatchV1().CronJobs(c.namespace).List(ctx, opts) 169 | if err != nil { 170 | return err 171 | } 172 | for _, cron := range cronjobs.Items { 173 | if err := c.process("CronJob", &cron.ObjectMeta, &cron.Spec.JobTemplate.Spec.Template); err != nil { 174 | failed = append(failed, fmt.Sprintf("failed to check %s/CronJob/%s: %s", cron.Namespace, cron.Name, err)) 175 | } 176 | } 177 | if len(failed) > 0 { 178 | return fmt.Errorf("%s", strings.Join(failed, "\n")) 179 | } 180 | return nil 181 | } 182 | 183 | type configAnnotationImageSpec struct { 184 | Name string `json:"name"` 185 | Image string `json:"image"` 186 | } 187 | 188 | type configAnnotation struct { 189 | Containers []configAnnotationImageSpec `json:"containers"` 190 | InitContainers []configAnnotationImageSpec `json:"initContainers"` 191 | } 192 | 193 | const imagoConfigAnnotation = "imago-config-spec" 194 | const imagoRestartedAtAnnotation = "imago/restartedAt" 195 | 196 | func mergeContainers(configContainers []configAnnotationImageSpec, containers []v1.Container) []configAnnotationImageSpec { 197 | specImages := make(map[string]string) 198 | for _, c := range containers { 199 | specImages[c.Name] = c.Image 200 | } 201 | re := regexp.MustCompile(".*@(sha256:.*)") 202 | configImages := make(map[string]string) 203 | for _, c := range configContainers { 204 | // drop containers in spec but not in config 205 | image := specImages[c.Name] 206 | if image != "" { 207 | match := re.FindStringSubmatch(image) 208 | if len(match) > 1 { 209 | // keep stored config 210 | configImages[c.Name] = c.Image 211 | } else { 212 | // use newer image 213 | configImages[c.Name] = specImages[c.Name] 214 | } 215 | } 216 | } 217 | for name, image := range specImages { 218 | if configImages[name] == "" { 219 | configImages[name] = image 220 | } 221 | } 222 | result := make([]configAnnotationImageSpec, 0) 223 | for name, image := range configImages { 224 | result = append(result, configAnnotationImageSpec{ 225 | Name: name, Image: image}) 226 | } 227 | return result 228 | } 229 | 230 | func getConfigAnnotation(meta *metav1.ObjectMeta, spec *v1.PodSpec) (*configAnnotation, error) { 231 | config := configAnnotation{} 232 | rawConfig := meta.GetAnnotations()[imagoConfigAnnotation] 233 | if len(rawConfig) > 0 { 234 | err := json.Unmarshal([]byte(rawConfig), &config) 235 | if err != nil { 236 | return nil, err 237 | } 238 | } 239 | config.Containers = mergeContainers(config.Containers, spec.Containers) 240 | config.InitContainers = mergeContainers(config.InitContainers, spec.InitContainers) 241 | return &config, nil 242 | } 243 | 244 | func needUpdate(name string, image string, specImage string) bool { 245 | if image != specImage { 246 | log.Printf(" %s need to be updated from %s to %s", name, specImage, image) 247 | return true 248 | } 249 | log.Printf(" %s ok", name) 250 | return false 251 | } 252 | 253 | func needRestart(name string, image string, running map[string]string) bool { 254 | result := false 255 | 256 | // If `image` contains a SHA-256 digest, then it doesn't need to match exactly the name of the 257 | // running image, as long as the digests match up. To check this, we first extract the digest 258 | // from `image`. 259 | digest := "" 260 | re := regexp.MustCompile(".*@(sha256:.*)") 261 | match := re.FindStringSubmatch(image) 262 | if len(match) > 1 { 263 | digest = match[1] 264 | } 265 | 266 | for pod, runningImage := range running { 267 | if runningImage == image { 268 | log.Printf(" %s on %s ok", name, pod) 269 | continue 270 | } 271 | // The image names don't exactly match, but maybe they have matching SHA-256 digests: 272 | if digest != "" { 273 | match = re.FindStringSubmatch(runningImage) 274 | if len(match) > 1 && match[1] == digest { 275 | log.Printf(" %s on %s ok", name, pod) 276 | continue 277 | } 278 | } 279 | log.Printf(" %s on %s need to be updated from %s to %s", name, pod, runningImage, image) 280 | result = true 281 | } 282 | return result 283 | } 284 | 285 | func (c *Config) getUpdates(configContainers []configAnnotationImageSpec, containers []v1.Container, running map[string]map[string]string) map[string]string { 286 | ctx := c.context 287 | reDigest := regexp.MustCompile(".*@(sha256:.*)") 288 | // We construct a regex to obtain the image name without tag by removing the tag if present. 289 | // A tag can contain lower case letters, upper case letters, digits, underscore, dot, and dash. 290 | reImageName := regexp.MustCompile(`(.*)(:[a-zA-Z0-9_\.-]*)?`) 291 | update := make(map[string]string) 292 | for _, container := range configContainers { 293 | match := reDigest.FindStringSubmatch(container.Image) 294 | if len(match) > 1 { 295 | log.Printf(" %s ok (fixed digest)", container.Name) 296 | continue 297 | } 298 | digest, err := GetDigest(ctx, container.Image) 299 | if err != nil { 300 | log.Printf(" %s unable to get digest: %s", container.Name, err) 301 | continue 302 | } 303 | match = reImageName.FindStringSubmatch(container.Image) 304 | image := match[1] + "@" + digest 305 | if !c.checkpods { 306 | for _, specContainer := range containers { 307 | if specContainer.Name != container.Name { 308 | continue 309 | } 310 | if needUpdate(container.Name, image, specContainer.Image) { 311 | update[container.Name] = image 312 | } 313 | } 314 | } else if needRestart(container.Name, image, running[container.Name]) { 315 | update[container.Name] = image 316 | } 317 | } 318 | return update 319 | } 320 | 321 | func getSelector(labels map[string]string) string { 322 | filters := make([]string, 0) 323 | for key, value := range labels { 324 | filters = append(filters, fmt.Sprintf("%s=%s", key, value)) 325 | } 326 | return strings.Join(filters, ", ") 327 | } 328 | 329 | func (c *Config) getRunningContainers(kind string, meta *metav1.ObjectMeta, template *v1.PodTemplateSpec) (map[string]map[string]string, map[string]map[string]string, error) { 330 | ctx := c.context 331 | runningInitContainers, runningContainers := make(map[string]map[string]string), make(map[string]map[string]string) 332 | if !c.checkpods { 333 | return runningInitContainers, runningContainers, nil 334 | } 335 | labelSelector := getSelector(template.Labels) 336 | running, err := c.cluster.CoreV1().Pods(meta.Namespace).List(ctx, metav1.ListOptions{FieldSelector: "status.phase=Running", LabelSelector: labelSelector}) 337 | if err != nil { 338 | return runningInitContainers, runningContainers, err 339 | } 340 | match := func(pod *v1.Pod) bool { 341 | for _, owner := range pod.OwnerReferences { 342 | switch owner.Kind { 343 | case "ReplicaSet": 344 | rs, err := c.cluster.AppsV1().ReplicaSets(meta.Namespace).Get(ctx, owner.Name, metav1.GetOptions{}) 345 | if err != nil { 346 | log.Print(err) 347 | continue 348 | } 349 | for _, rsOwner := range rs.OwnerReferences { 350 | if rsOwner.Kind == kind && rsOwner.Name == meta.Name { 351 | return true 352 | } 353 | } 354 | case "DaemonSet": 355 | if owner.Kind == kind && owner.Name == meta.Name { 356 | return true 357 | } 358 | case "StatefulSet": 359 | if owner.Kind == kind && owner.Name == meta.Name { 360 | return true 361 | } 362 | } 363 | } 364 | return false 365 | } 366 | re := regexp.MustCompile("(.*://)?(.*@sha256:.*)") 367 | addImage := func(containers map[string]map[string]string, name string, podName string, image string, imageId string) { 368 | reMatch := re.FindStringSubmatch(imageId) 369 | newImageId := "" 370 | if len(reMatch) < 3 { 371 | if regexp.MustCompile("[a-z0-9]+").MatchString(image) { 372 | ref, err := docker.ParseReference("//" + image) 373 | if err != nil { 374 | log.Print(err) 375 | return 376 | } 377 | newImageId = reference.TrimNamed(ref.DockerReference()).Name() 378 | } else { 379 | log.Printf("Unable to parse image digest (image %s, imageId %s)", image, imageId) 380 | return 381 | } 382 | } else { 383 | newImageId = reMatch[2] 384 | } 385 | if containers[name] == nil { 386 | containers[name] = make(map[string]string) 387 | } 388 | containers[name][podName] = newImageId 389 | } 390 | for _, pod := range running.Items { 391 | if match(&pod) { 392 | runningInitContainers[pod.Name] = make(map[string]string) 393 | runningContainers[pod.Name] = make(map[string]string) 394 | for _, container := range pod.Status.InitContainerStatuses { 395 | addImage(runningInitContainers, container.Name, pod.Name, container.Image, container.ImageID) 396 | } 397 | for _, container := range pod.Status.ContainerStatuses { 398 | addImage(runningContainers, container.Name, pod.Name, container.Image, container.ImageID) 399 | } 400 | } 401 | } 402 | return runningInitContainers, runningContainers, nil 403 | } 404 | 405 | func (c *Config) process(kind string, meta *metav1.ObjectMeta, template *v1.PodTemplateSpec) error { 406 | ctx := c.context 407 | if c.xnamespace.Contains(meta.Namespace) { 408 | // namespace excluded from selection 409 | return nil 410 | } 411 | log.Printf("checking %s/%s/%s", meta.Namespace, kind, meta.Name) 412 | config, err := getConfigAnnotation(meta, &template.Spec) 413 | if err != nil { 414 | return err 415 | } 416 | runningInitContainers, runningContainers, err := c.getRunningContainers(kind, meta, template) 417 | if err != nil { 418 | return err 419 | } 420 | updateInitContainers := c.getUpdates(config.InitContainers, template.Spec.InitContainers, runningInitContainers) 421 | updateContainers := c.getUpdates(config.Containers, template.Spec.Containers, runningContainers) 422 | if c.policy == "" || (len(updateContainers) == 0 && len(updateInitContainers) == 0) { 423 | return nil 424 | } 425 | log.Printf("%s %s/%s/%s", c.policy, meta.Namespace, kind, meta.Name) 426 | var policyUpdateResource func(*metav1.ObjectMeta, *v1.PodTemplateSpec) error 427 | switch c.policy { 428 | case "update": 429 | policyUpdateResource = func(meta *metav1.ObjectMeta, template *v1.PodTemplateSpec) error { 430 | jsonConfig, err := json.Marshal(config) 431 | if err != nil { 432 | return err 433 | } 434 | jsonConfigString := string(jsonConfig) 435 | if meta.Annotations == nil { 436 | meta.Annotations = make(map[string]string) 437 | } 438 | meta.Annotations[imagoConfigAnnotation] = jsonConfigString 439 | var updateSpec = func(containers []v1.Container, update map[string]string) { 440 | for i, container := range containers { 441 | if newImage, ok := update[container.Name]; ok { 442 | containers[i].Image = newImage 443 | } 444 | } 445 | } 446 | updateSpec(template.Spec.Containers, updateContainers) 447 | updateSpec(template.Spec.InitContainers, updateInitContainers) 448 | return nil 449 | } 450 | case "restart": 451 | policyUpdateResource = func(meta *metav1.ObjectMeta, template *v1.PodTemplateSpec) error { 452 | if meta.Annotations[imagoConfigAnnotation] != "" { 453 | log.Printf("deleting %s annotation and reset images", imagoConfigAnnotation) 454 | delete(meta.Annotations, imagoConfigAnnotation) 455 | var updateSpec = func(containers []v1.Container, updates []configAnnotationImageSpec) { 456 | for i, container := range containers { 457 | for _, origContainer := range updates { 458 | if origContainer.Name == container.Name { 459 | containers[i].Image = origContainer.Image 460 | } 461 | } 462 | } 463 | } 464 | updateSpec(template.Spec.Containers, config.Containers) 465 | updateSpec(template.Spec.InitContainers, config.InitContainers) 466 | } 467 | if kind == "CronJob" { 468 | return nil 469 | } 470 | if template.Annotations == nil { 471 | template.Annotations = make(map[string]string) 472 | } 473 | template.Annotations[imagoRestartedAtAnnotation] = time.Now().Format(time.RFC3339) 474 | return nil 475 | } 476 | } 477 | var updateResource func() error 478 | switch kind { 479 | case "Deployment": 480 | updateResource = func() error { 481 | client := c.cluster.AppsV1().Deployments(meta.Namespace) 482 | resource, err := client.Get(ctx, meta.Name, metav1.GetOptions{}) 483 | if err != nil { 484 | return err 485 | } 486 | if err = policyUpdateResource(&resource.ObjectMeta, &resource.Spec.Template); err != nil { 487 | return err 488 | } 489 | _, err = client.Update(ctx, resource, metav1.UpdateOptions{}) 490 | return err 491 | } 492 | case "DaemonSet": 493 | updateResource = func() error { 494 | client := c.cluster.AppsV1().DaemonSets(meta.Namespace) 495 | resource, err := client.Get(ctx, meta.Name, metav1.GetOptions{}) 496 | if err != nil { 497 | return err 498 | } 499 | if err = policyUpdateResource(&resource.ObjectMeta, &resource.Spec.Template); err != nil { 500 | return err 501 | } 502 | _, err = client.Update(ctx, resource, metav1.UpdateOptions{}) 503 | return err 504 | } 505 | case "StatefulSet": 506 | updateResource = func() error { 507 | client := c.cluster.AppsV1().StatefulSets(meta.Namespace) 508 | resource, err := client.Get(ctx, meta.Name, metav1.GetOptions{}) 509 | if err != nil { 510 | return err 511 | } 512 | if err = policyUpdateResource(&resource.ObjectMeta, &resource.Spec.Template); err != nil { 513 | return err 514 | } 515 | _, err = client.Update(ctx, resource, metav1.UpdateOptions{}) 516 | return err 517 | } 518 | case "CronJob": 519 | updateResource = func() error { 520 | client := c.cluster.BatchV1().CronJobs(meta.Namespace) 521 | resource, err := client.Get(ctx, meta.Name, metav1.GetOptions{}) 522 | if err != nil { 523 | return err 524 | } 525 | if err = policyUpdateResource(&resource.ObjectMeta, &resource.Spec.JobTemplate.Spec.Template); err != nil { 526 | return err 527 | } 528 | _, err = client.Update(ctx, resource, metav1.UpdateOptions{}) 529 | return err 530 | } 531 | default: 532 | return fmt.Errorf("unhandled kind %s", kind) 533 | } 534 | if err := retry.RetryOnConflict(retry.DefaultRetry, updateResource); err != nil { 535 | return err 536 | } 537 | return nil 538 | } 539 | 540 | func inClusterClientPossible() bool { 541 | fi, err := os.Stat("/var/run/secrets/kubernetes.io/serviceaccount/token") 542 | return os.Getenv("KUBERNETES_SERVICE_HOST") != "" && 543 | os.Getenv("KUBERNETES_SERVICE_PORT") != "" && 544 | err == nil && !fi.IsDir() 545 | } 546 | 547 | func defaultKubeConfig() string { 548 | kubeconfig := os.Getenv("KUBECONFIG") 549 | if kubeconfig == "" { 550 | kubeconfig = filepath.Join(homeDir(), ".kube", "config") 551 | } 552 | return kubeconfig 553 | } 554 | 555 | func inClusterNamespace() string { 556 | data, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") 557 | if err != nil { 558 | log.Fatal(err) 559 | } 560 | if ns := strings.TrimSpace(string(data)); len(ns) > 0 { 561 | return ns 562 | } 563 | return "" 564 | } 565 | 566 | func outClusterNamespace(kubeconfig string) string { 567 | config := clientcmd.GetConfigFromFileOrDie(kubeconfig) 568 | if len(config.Contexts) == 0 || config.Contexts[config.CurrentContext] == nil { 569 | log.Fatal("No kubernetes contexts availables") 570 | } 571 | return config.Contexts[config.CurrentContext].Namespace 572 | } 573 | 574 | func homeDir() string { 575 | user, err := user.Current() 576 | if err != nil { 577 | log.Fatal(err) 578 | } 579 | return user.HomeDir 580 | } 581 | 582 | type arrayFlags []string 583 | 584 | func (i *arrayFlags) String() string { 585 | return "" 586 | } 587 | 588 | func (i *arrayFlags) Set(value string) error { 589 | *i = append(*i, value) 590 | return nil 591 | } 592 | 593 | func (i *arrayFlags) Contains(value string) bool { 594 | for _, x := range *i { 595 | if x == value { 596 | return true 597 | } 598 | } 599 | return false 600 | } 601 | 602 | func main() { 603 | var kubeconfig string 604 | var labelSelector string 605 | var fieldSelector string 606 | var allnamespaces bool 607 | var namespace arrayFlags 608 | var xnamespace arrayFlags 609 | var update bool 610 | var restart bool 611 | var checkpods bool 612 | flag.StringVar(&kubeconfig, "kubeconfig", defaultKubeConfig(), "kube config file") 613 | flag.Var(&namespace, "n", "Check deployments and daemonsets in given namespaces (default to current namespace)") 614 | flag.Var(&xnamespace, "x", "Check deployments and daemonsets in all namespaces except given namespaces (implies --all-namespaces)") 615 | flag.StringVar(&labelSelector, "l", "", "Kubernetes labels selectors\nWarning: applies to Deployment, DaemonSet, StatefulSet and CronJob, not pods !") 616 | flag.StringVar(&fieldSelector, "field-selector", "", "Kubernetes field-selector\nexample: metadata.name=myapp") 617 | flag.BoolVar(&allnamespaces, "all-namespaces", false, "Check deployments and daemonsets on all namespaces (default false)") 618 | flag.BoolVar(&allnamespaces, "A", false, "Check deployments and daemonsets on all namespaces (shorthand) (default false)") 619 | flag.BoolVar(&update, "update", false, "update deployments and daemonsets to use newer images (default false)") 620 | flag.BoolVar(&restart, "restart", false, "rollout restart deployments and daemonsets to use newer images, implies -check-pods and assume imagePullPolicy is Always (default false)") 621 | flag.BoolVar(&checkpods, "check-pods", false, "check image digests of running pods (default false)") 622 | flag.Parse() 623 | if allnamespaces && len(namespace) > 0 { 624 | log.Fatal("You can't use -n with --all-namespaces") 625 | } 626 | if len(namespace) == 0 { 627 | namespace = append(namespace, "") 628 | } 629 | if len(xnamespace) > 0 { 630 | allnamespaces = true 631 | } 632 | var policy string 633 | if restart { 634 | policy = "restart" 635 | checkpods = true 636 | } else if update { 637 | policy = "update" 638 | } 639 | for _, ns := range namespace { 640 | ctx := context.Background() 641 | c, err := NewConfig(kubeconfig, ns, allnamespaces, &xnamespace, policy, checkpods, ctx) 642 | if err != nil { 643 | log.Fatal(err) 644 | } 645 | if err := c.Update(fieldSelector, labelSelector); err != nil { 646 | log.Fatal(err) 647 | } 648 | } 649 | } 650 | --------------------------------------------------------------------------------