├── .github └── workflows │ ├── codeql.yml │ ├── lock.yml │ ├── release.yml │ ├── stale.yml │ └── test.yml ├── .gitignore ├── .ko.yaml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build ├── examples │ ├── cronjob.yaml │ ├── deployment.yaml │ ├── pod-diff-registry.yaml │ └── pod.yaml └── test │ ├── request-authn.json │ └── request.json ├── cmd ├── function │ └── function.go ├── version │ └── version.go └── webhook │ ├── webhook.go │ ├── webhook_suite_test.go │ └── webhook_test.go ├── docs ├── authentication.md ├── build.md ├── common-issues.md ├── development.md ├── motivation.md ├── recommendations.md ├── release.md ├── troubleshooting.md └── workload-identity.md ├── go.mod ├── go.sum ├── main.go ├── manifests ├── Kptfile ├── Kustomization ├── README.md ├── cluster-role-binding.yaml ├── cluster-role.yaml ├── deployment.yaml ├── mutating-webhook-configuration.yaml ├── namespace.yaml ├── role-binding.yaml ├── role.yaml ├── secret.yaml ├── service-account.yaml └── service.yaml ├── pkg ├── handler │ ├── handler.go │ └── handler_test.go ├── keychain │ ├── keychain.go │ ├── keychain_stub_test.go │ └── keychain_test.go ├── logging │ ├── discard.go │ ├── doc.go │ ├── klog.go │ ├── std.go │ └── zap.go ├── resolve │ ├── resolve.go │ ├── resolve_stub_test.go │ └── resolve_test.go ├── util │ ├── debug.go │ ├── doc.go │ ├── pod_info.go │ └── stringarray.go └── version │ ├── version.go │ └── version.txt └── skaffold.yaml /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 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 | # Adapted from sethvargo/gcs-cacher 16 | 17 | name: "CodeQL" 18 | 19 | on: 20 | push: 21 | branches: 22 | - main 23 | paths-ignore: 24 | - "**.md" 25 | - "docs/**" 26 | pull_request: 27 | branches: 28 | - main 29 | paths-ignore: 30 | - "**.md" 31 | - "docs/**" 32 | schedule: 33 | - cron: "30 0 * * 1" 34 | 35 | jobs: 36 | analyze: 37 | name: Analyze 38 | runs-on: ubuntu-latest 39 | 40 | strategy: 41 | fail-fast: false 42 | matrix: 43 | language: ["go"] 44 | 45 | steps: 46 | - name: Checkout code 47 | uses: actions/checkout@v2 48 | with: 49 | fetch-depth: 2 50 | 51 | - run: git checkout HEAD^2 52 | if: ${{ github.event_name == 'pull_request' }} 53 | 54 | - name: Initialize CodeQL 55 | uses: github/codeql-action/init@v1 56 | with: 57 | languages: ${{ matrix.language }} 58 | 59 | - name: Perform CodeQL Analysis 60 | uses: github/codeql-action/analyze@v1 61 | -------------------------------------------------------------------------------- /.github/workflows/lock.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 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 | # Adapted from sethvargo/gcs-cacher 16 | 17 | name: Lock closed 18 | 19 | on: 20 | schedule: 21 | - cron: "0 0 * * *" 22 | 23 | jobs: 24 | lock: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: dessant/lock-threads@v2 28 | with: 29 | github-token: "${{ github.token }}" 30 | issue-lock-inactive-days: 28 31 | issue-lock-comment: |- 32 | This issue has been automatically locked since there has not been any 33 | recent activity after it was closed. Please open a new issue for 34 | related bugs. 35 | 36 | pr-lock-inactive-days: 28 37 | pr-lock-comment: |- 38 | This pull request has been automatically locked since there has not 39 | been any recent activity after it was closed. Please open a new 40 | issue for related bugs. 41 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 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 | name: Release 16 | 17 | on: 18 | push: 19 | tags: 20 | - "v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+" 21 | paths-ignore: 22 | - "**.md" 23 | - "docs/**" 24 | 25 | jobs: 26 | release: 27 | runs-on: ubuntu-latest 28 | 29 | env: 30 | CGO_ENABLED: "0" 31 | COSIGN_VERSION: v2.4.0 32 | GGCR_VERSION: v0.20.2 33 | KUBEBUILDER_VERSION: "2.3.2" 34 | KUBECTL_VERSION: v1.30.3 35 | REGISTRY: ghcr.io 36 | SKAFFOLD_VERSION: v2.13.1 37 | SKAFFOLD_CACHE_ARTIFACTS: "false" 38 | SKAFFOLD_DETECT_MINIKUBE: "false" 39 | SKAFFOLD_INTERACTIVE: "false" 40 | SKAFFOLD_OFFLINE: "true" 41 | SKAFFOLD_UPDATE_CHECK: "false" 42 | 43 | permissions: 44 | contents: write 45 | id-token: write 46 | packages: write 47 | 48 | steps: 49 | - name: Checkout code 50 | uses: actions/checkout@v3 51 | 52 | - name: Setup Go 53 | uses: actions/setup-go@v4 54 | with: 55 | go-version-file: go.mod 56 | 57 | - name: Set image env vars 58 | run: | 59 | echo IMAGE_REPO=$(echo $GITHUB_REPOSITORY | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV 60 | RC_VERSION=${GITHUB_REF#refs/tags/} 61 | echo VERSION=${RC_VERSION%-rc*} >> $GITHUB_ENV 62 | 63 | - name: Print go env 64 | run: | 65 | echo HOME=$HOME 66 | echo PATH=$PATH 67 | go version 68 | go env 69 | 70 | - name: Run unit tests 71 | run: | 72 | go test -v -count=1 -short -timeout=5m -vet=asmdecl,assign,atomic,bools,buildtag,cgocall,composites,copylocks,errorsas,httpresponse,loopclosure,lostcancel,nilfunc,printf,shift,stdmethods,structtag,tests,unmarshal,unreachable,unsafeptr,unusedresult ./... 73 | 74 | - name: Install kube-apiserver and etcd 75 | run: | 76 | mkdir -p ${HOME}/.local/bin 77 | curl -sSL "https://github.com/kubernetes-sigs/kubebuilder/releases/download/v${KUBEBUILDER_VERSION}/kubebuilder_${KUBEBUILDER_VERSION}_linux_amd64.tar.gz" | tar --strip-components 2 -xzC ${HOME}/.local/bin kubebuilder_${KUBEBUILDER_VERSION}_linux_amd64/bin/etcd kubebuilder_${KUBEBUILDER_VERSION}_linux_amd64/bin/kube-apiserver kubebuilder_${KUBEBUILDER_VERSION}_linux_amd64/bin/kubebuilder 78 | echo "KUBEBUILDER_ASSETS=${HOME}/.local/bin" >> $GITHUB_ENV 79 | 80 | - name: Run integration tests 81 | run: | 82 | go test -v -timeout=10m ./cmd/webhook/... 83 | env: 84 | KUBEBUILDER_CONTROLPLANE_START_TIMEOUT: 180s 85 | 86 | - name: Create release branch 87 | run: | 88 | git checkout -B release-$VERSION 89 | 90 | - name: Populate version number in embedded file 91 | run: | 92 | echo "$VERSION" > pkg/version/version.txt 93 | 94 | - name: Commit and push to release branch with new version number 95 | run: | 96 | git add pkg/version/version.txt 97 | git config user.name github-actions 98 | git config user.email github-actions@github.com 99 | git commit -m "Update binary version to $VERSION" 100 | git push --force origin release-$VERSION 101 | 102 | - name: Set image label env vars 103 | run: | 104 | echo REVISION=$(git rev-parse HEAD) >> $GITHUB_ENV 105 | echo SOURCE=${{ github.server_url }}/${{ github.repository }}.git >> $GITHUB_ENV 106 | echo URL=${{ github.server_url }}/${{ github.repository }} >> $GITHUB_ENV 107 | 108 | - name: Build binaries 109 | run: | 110 | GOOS=darwin GOARCH=amd64 go build -v -trimpath -ldflags="-s -w" -o digester_Darwin_x86_64 . 111 | GOOS=darwin GOARCH=arm64 go build -v -trimpath -ldflags="-s -w" -o digester_Darwin_arm64 . 112 | GOOS=linux GOARCH=amd64 go build -v -trimpath -ldflags="-s -w" -o digester_Linux_x86_64 . 113 | GOOS=linux GOARCH=arm64 go build -v -trimpath -ldflags="-s -w" -o digester_Linux_aarch64 . 114 | 115 | - name: Install Skaffold 116 | run: | 117 | mkdir -p $HOME/.local/bin 118 | curl -sSLo $HOME/.local/bin/skaffold "https://storage.googleapis.com/skaffold/releases/${SKAFFOLD_VERSION}/skaffold-$(go env GOOS)-$(go env GOARCH)" 119 | chmod +x $HOME/.local/bin/skaffold 120 | 121 | - name: Build and push container images 122 | run: | 123 | skaffold build \ 124 | --default-repo ${{ env.REGISTRY }}/${{ github.repository_owner }} \ 125 | --file-output ${{ runner.temp }}/skaffold-artifacts.json \ 126 | --profile release \ 127 | --push \ 128 | --tag $VERSION 129 | env: 130 | GITHUB_TOKEN: ${{ github.token }} 131 | 132 | - name: Render release manifest 133 | run: | 134 | skaffold render \ 135 | --build-artifacts ${{ runner.temp }}/skaffold-artifacts.json \ 136 | --digest-source none \ 137 | --output digester_manifest.yaml \ 138 | --profile release 139 | 140 | - name: Create checksums file 141 | run: shasum -a 256 digester_* > SHA256SUMS 142 | 143 | - name: Get image name with digest 144 | run: | 145 | echo -n IMAGE_NAME= >> $GITHUB_ENV 146 | echo $(jq -r '.builds[] | select(.imageName=="k8s-digester") | .tag' ${{ runner.temp }}/skaffold-artifacts.json) >> $GITHUB_ENV 147 | 148 | - name: Install krane 149 | run: | 150 | mkdir -p $HOME/.local/bin 151 | curl -sSL "https://github.com/google/go-containerregistry/releases/download/${GGCR_VERSION}/go-containerregistry_$(uname -s)_$(uname -m).tar.gz" | tar -xzC $HOME/.local/bin krane 152 | 153 | - name: Update latest tag 154 | run: krane tag $IMAGE_NAME latest 155 | env: 156 | GITHUB_TOKEN: ${{ github.token }} 157 | 158 | - name: Install Cosign 159 | uses: sigstore/cosign-installer@main 160 | with: 161 | cosign-release: ${{ env.COSIGN_VERSION }} 162 | 163 | - name: Sign the images with GitHub OIDC Token 164 | run: cosign sign --k8s-keychain --recursive --upload --yes $IMAGE_NAME 165 | env: 166 | GITHUB_TOKEN: ${{ github.token }} 167 | 168 | - name: Create release body file 169 | run: | 170 | cat << EOF > ${{ runner.temp }}/body.md 171 | ## Images 172 | 173 | GitHub Container Registry: 174 | 175 | $IMAGE_NAME 176 | EOF 177 | 178 | - name: Install kubectl 179 | uses: azure/setup-kubectl@v3 180 | with: 181 | version: ${{ env.KUBECTL_VERSION }} 182 | 183 | - name: Set image name in deployment manifest 184 | run: | 185 | tmpfile=$(mktemp) 186 | kubectl patch \ 187 | --dry-run=client \ 188 | --filename deployment.yaml \ 189 | --local \ 190 | --output yaml \ 191 | --patch '{"spec":{"template":{"spec":{"containers":[{"name":"manager","image":"${{ env.IMAGE_NAME }}"}]}}}}' \ 192 | > $tmpfile 193 | mv $tmpfile deployment.yaml 194 | working-directory: manifests 195 | 196 | - name: Update version in readme 197 | run: | 198 | sed -i "s/VERSION=.*/VERSION=$VERSION/" README.md manifests/README.md docs/authentication.md 199 | 200 | - name: Commit and push to release branch with new version and image ref 201 | run: | 202 | git add README.md manifests docs/authentication.md 203 | git config user.name github-actions 204 | git config user.email github-actions@github.com 205 | git commit -m "Update version in readme and manifest to $VERSION" 206 | git push --force origin release-$VERSION 207 | 208 | - name: Set release env vars 209 | run: | 210 | echo COMMITISH=$(git rev-parse HEAD) >> $GITHUB_ENV 211 | 212 | - name: Create release 213 | id: create_release 214 | uses: actions/create-release@v1 215 | env: 216 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 217 | with: 218 | tag_name: ${{ env.VERSION }} 219 | release_name: ${{ env.VERSION }} 220 | body_path: ${{ runner.temp }}/body.md 221 | commitish: ${{ env.COMMITISH }} 222 | 223 | - name: Upload binary darwin amd64 224 | uses: actions/upload-release-asset@v1 225 | env: 226 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 227 | with: 228 | upload_url: ${{ steps.create_release.outputs.upload_url }} 229 | asset_path: digester_Darwin_x86_64 230 | asset_name: digester_Darwin_x86_64 231 | asset_content_type: application/octet-stream 232 | 233 | - name: Upload binary darwin arm64 234 | uses: actions/upload-release-asset@v1 235 | env: 236 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 237 | with: 238 | upload_url: ${{ steps.create_release.outputs.upload_url }} 239 | asset_path: digester_Darwin_arm64 240 | asset_name: digester_Darwin_arm64 241 | asset_content_type: application/octet-stream 242 | 243 | - name: Upload binary linux amd64 244 | uses: actions/upload-release-asset@v1 245 | env: 246 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 247 | with: 248 | upload_url: ${{ steps.create_release.outputs.upload_url }} 249 | asset_path: digester_Linux_x86_64 250 | asset_name: digester_Linux_x86_64 251 | asset_content_type: application/octet-stream 252 | 253 | - name: Upload binary linux arm64 254 | uses: actions/upload-release-asset@v1 255 | env: 256 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 257 | with: 258 | upload_url: ${{ steps.create_release.outputs.upload_url }} 259 | asset_path: digester_Linux_aarch64 260 | asset_name: digester_Linux_aarch64 261 | asset_content_type: application/octet-stream 262 | 263 | - name: Upload manifest file 264 | uses: actions/upload-release-asset@v1 265 | env: 266 | GITHUB_TOKEN: ${{ github.token }} 267 | with: 268 | upload_url: ${{ steps.create_release.outputs.upload_url }} 269 | asset_path: digester_manifest.yaml 270 | asset_name: digester_manifest.yaml 271 | asset_content_type: application/x-yaml 272 | 273 | - name: Upload checksum file 274 | uses: actions/upload-release-asset@v1 275 | env: 276 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 277 | with: 278 | upload_url: ${{ steps.create_release.outputs.upload_url }} 279 | asset_path: SHA256SUMS 280 | asset_name: SHA256SUMS 281 | asset_content_type: text/plain 282 | 283 | - name: Update version in readme on main branch 284 | run: | 285 | git config user.name github-actions 286 | git config user.email github-actions@github.com 287 | git reset --hard 288 | git remote update 289 | git checkout main 290 | git pull --no-edit --no-rebase --strategy-option=theirs origin 291 | sed -i "s/VERSION=.*/VERSION=$VERSION/" README.md manifests/README.md docs/authentication.md 292 | git add README.md manifests/README.md docs/authentication.md 293 | git commit -m "Update version in docs to $VERSION" 294 | git pull --no-edit --no-rebase --strategy-option=ours origin 295 | git push origin main 296 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 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 | # Adapted from sethvargo/gcs-cacher 16 | 17 | name: Close stale 18 | 19 | on: 20 | schedule: 21 | - cron: "0 0 * * *" 22 | 23 | jobs: 24 | stale: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/stale@v3 28 | with: 29 | repo-token: "${{ github.token }}" 30 | 31 | stale-issue-message: |- 32 | This issue is stale because it has been open for 28 days with no 33 | activity. It will automatically close after 14 more days of inactivity. 34 | stale-issue-label: "kind/stale" 35 | exempt-issue-labels: "bug,enhancement" 36 | 37 | stale-pr-message: |- 38 | This Pull Request is stale because it has been open for 28 days with 39 | no activity. It will automatically close after 14 more days of 40 | inactivity. 41 | stale-pr-label: "kind/stale" 42 | exempt-pr-labels: "bug,enhancement" 43 | 44 | days-before-stale: 28 45 | days-before-close: 14 46 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 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 | # Adapted from sethvargo/gcs-cacher 16 | 17 | name: Test 18 | 19 | on: 20 | push: 21 | paths-ignore: 22 | - "**.md" 23 | - "docs/**" 24 | pull_request: 25 | paths-ignore: 26 | - "**.md" 27 | - "docs/**" 28 | 29 | jobs: 30 | test: 31 | runs-on: ubuntu-latest 32 | 33 | steps: 34 | - name: Checkout code 35 | uses: actions/checkout@v3 36 | 37 | - name: Setup Go 38 | uses: actions/setup-go@v4 39 | with: 40 | go-version-file: go.mod 41 | 42 | - name: Run unit test 43 | run: go test -v -count=1 -race -short -timeout=5m -vet="asmdecl,assign,atomic,bools,buildtag,cgocall,composites,copylocks,errorsas,httpresponse,loopclosure,lostcancel,nilfunc,printf,shift,stdmethods,structtag,tests,unmarshal,unreachable,unsafeptr,unusedresult" ./... 44 | 45 | - name: Install kube-apiserver and etcd 46 | run: | 47 | mkdir -p ${HOME}/.local/bin 48 | curl -sSL "https://github.com/kubernetes-sigs/kubebuilder/releases/download/v${KUBEBUILDER_VERSION}/kubebuilder_${KUBEBUILDER_VERSION}_linux_amd64.tar.gz" | tar --strip-components 2 -xzC ${HOME}/.local/bin kubebuilder_${KUBEBUILDER_VERSION}_linux_amd64/bin/etcd kubebuilder_${KUBEBUILDER_VERSION}_linux_amd64/bin/kube-apiserver kubebuilder_${KUBEBUILDER_VERSION}_linux_amd64/bin/kubebuilder 49 | echo "KUBEBUILDER_ASSETS=${HOME}/.local/bin" >> $GITHUB_ENV 50 | env: 51 | KUBEBUILDER_VERSION: "2.3.2" 52 | 53 | - name: Run integration tests 54 | run: | 55 | go test -v -count=1 -timeout=10m ./cmd/webhook/... 56 | env: 57 | KUBEBUILDER_ATTACH_CONTROL_PLANE_OUTPUT: "true" 58 | KUBEBUILDER_CONTROLPLANE_START_TIMEOUT: 180s 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 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 | *-patch.json 16 | .idea/ 17 | .kpt-pipeline/ 18 | .vscode/ 19 | bin/ 20 | build/cert 21 | config.json 22 | digester 23 | docker-config.json 24 | k8s-digester 25 | -------------------------------------------------------------------------------- /.ko.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 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 | defaultBaseImage: gcr.io/distroless/static-debian12:nonroot 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google.com/conduct/). 29 | -------------------------------------------------------------------------------- /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. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Digester 2 | 3 | Digester resolves tags to 4 | [digests](https://cloud.google.com/solutions/using-container-images) for 5 | container and init container images in Kubernetes 6 | [Pod](https://kubernetes.io/docs/concepts/workloads/pods/) and 7 | [Pod template](https://kubernetes.io/docs/concepts/workloads/pods/#pod-templates) 8 | specs. 9 | 10 | It replaces container image references that use tags: 11 | 12 | ```yaml 13 | spec: 14 | containers: 15 | - image: gcr.io/google-containers/echoserver:1.10 16 | ``` 17 | 18 | With references that use the image digest: 19 | 20 | ```yaml 21 | spec: 22 | containers: 23 | - image: gcr.io/google-containers/echoserver:1.10@sha256:cb5c1bddd1b5665e1867a7fa1b5fa843a47ee433bbb75d4293888b71def53229 24 | ``` 25 | 26 | Digester can run either as a 27 | [mutating admission webhook](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/) 28 | in a Kubernetes cluster, or as a client-side 29 | [Kubernetes Resource Model (KRM) function](https://kpt.dev/book/02-concepts/03-functions) 30 | with the [kpt](https://kpt.dev/) or 31 | [kustomize](https://kubectl.docs.kubernetes.io/guides/introduction/kustomize/) 32 | command-line tools. 33 | 34 | If a tag points to an 35 | [image index](https://github.com/opencontainers/image-spec/blob/master/image-index.md#oci-image-index-specification) 36 | or 37 | [manifest list](https://docs.docker.com/registry/spec/manifest-v2-2/#manifest-list), 38 | digester resolves the tag to the digest of the image index or manifest list. 39 | 40 | The webhook is opt-in at the namespace level by label, see 41 | [Deploying the webhook](#deploying-the-webhook). 42 | 43 | If you use 44 | [Binary Authorization](https://cloud.google.com/binary-authorization/docs), 45 | digester can help to ensure that only verified container images can be deployed 46 | to your clusters. A Binary Authorization 47 | [attestation](https://cloud.google.com/binary-authorization/docs/key-concepts#attestations) 48 | is valid for a particular container image digest. You must deploy container 49 | images by digest so that Binary Authorization can verify the attestations for 50 | the container image. You can use digester to deploy container images by digest. 51 | 52 | ## Running the KRM function 53 | 54 | 1. Download the digester binary for your platform from the 55 | [Releases page](../../releases). 56 | 57 | Alternatively, you can download the latest version using these commands: 58 | 59 | ```sh 60 | VERSION=v0.1.16 61 | curl -Lo digester "https://github.com/google/k8s-digester/releases/download/${VERSION}/digester_$(uname -s)_$(uname -m)" 62 | chmod +x digester 63 | ``` 64 | 65 | 2. [Install kpt](https://kpt.dev/installation/) v1.0.0-beta.1 or later, and/or 66 | [install kustomize](https://kubectl.docs.kubernetes.io/installation/kustomize/) 67 | v3.7.0 or later. 68 | 69 | 3. Run the digester KRM function using either kpt or kustomize: 70 | 71 | - Using kpt: 72 | 73 | ```sh 74 | kpt fn eval [manifest directory] --exec ./digester 75 | ``` 76 | 77 | - Using kustomize: 78 | 79 | ```sh 80 | kustomize fn run [manifest directory] --enable-exec --exec-path ./digester 81 | ``` 82 | 83 | By running as an executable, the digester KRM function has access to 84 | container image registry credentials in the current environment, such as 85 | the current user's 86 | [Docker config file](https://github.com/google/go-containerregistry/blob/main/pkg/authn/README.md#the-config-file) 87 | and 88 | [credential helpers](https://docs.docker.com/engine/reference/commandline/login/#credential-helper-protocol). 89 | For more information, see the digester documentation on 90 | [Authenticating to container image registries](docs/authentication.md). 91 | 92 | ## Deploying the webhook 93 | 94 | The digester webhook requires Kubernetes v1.16 or later. 95 | 96 | 1. If you use Google Kubernetes Engine (GKE), grant yourself the 97 | `cluster-admin` Kubernetes 98 | [cluster role](https://kubernetes.io/docs/reference/access-authn-authz/rbac/): 99 | 100 | ```sh 101 | kubectl create clusterrolebinding cluster-admin-binding \ 102 | --clusterrole cluster-admin \ 103 | --user "$(gcloud config get core/account)" 104 | ``` 105 | 106 | 2. Install the digester webhook in your Kubernetes cluster: 107 | 108 | ```sh 109 | VERSION=v0.1.16 110 | kubectl apply -k "https://github.com/google/k8s-digester.git/manifests/?ref=${VERSION}" 111 | ``` 112 | 113 | 3. Add the `digest-resolution: enabled` label to namespaces where you want the 114 | webhook to resolve tags to digests: 115 | 116 | ```sh 117 | kubectl label namespace [NAMESPACE] digest-resolution=enabled 118 | ``` 119 | 120 | To configure how the webhook authenticates to your container image registries, 121 | see the documentation on 122 | [Authenticating to container image registries](https://github.com/google/k8s-digester/blob/main/docs/authentication.md#authenticating-to-container-image-registries). 123 | 124 | If you want to install the webhook using kustomize or kpt, follow the steps in 125 | the [package documentation](manifests/README.md). 126 | 127 | If you want to apply a pre-rendered manifest, you can download an all-in-one 128 | manifest file for a released version from the [Releases page](../../releases). 129 | 130 | ### Private clusters 131 | 132 | If you install the webhook in a 133 | [private Google Kubernetes Engine (GKE) cluster](https://cloud.google.com/kubernetes-engine/docs/how-to/private-clusters), 134 | you must add a firewall rule. In a private cluster, the nodes only have 135 | [internal IP addresses](https://cloud.google.com/vpc/docs/ip-addresses). 136 | The firewall rule allows the API server to access the webhook running on port 137 | 8443 on the cluster nodes. 138 | 139 | 1. Create an environment variable called `CLUSTER`. The value is the name of 140 | your cluster that you see when you run `gcloud container clusters list`: 141 | 142 | ```sh 143 | CLUSTER=[your private GKE cluster name] 144 | ``` 145 | 146 | 2. Look up the IP address range for the cluster API server and store it in an 147 | environment variable: 148 | 149 | ```sh 150 | API_SERVER_CIDR=$(gcloud container clusters describe $CLUSTER \ 151 | --format 'value(privateClusterConfig.masterIpv4CidrBlock)') 152 | ``` 153 | 154 | 3. Look up the 155 | [network tags](https://cloud.google.com/vpc/docs/add-remove-network-tags) 156 | for your cluster nodes and store them comma-separated in an environment 157 | variable: 158 | 159 | ```sh 160 | TARGET_TAGS=$(gcloud compute firewall-rules list \ 161 | --filter "name~^gke-$CLUSTER" \ 162 | --format 'value(targetTags)' | uniq | paste -d, -s -) 163 | ``` 164 | 165 | 4. Create a firewall rule that allow traffic from the API server to the 166 | cluster nodes on TCP port 8443: 167 | 168 | ```sh 169 | gcloud compute firewall-rules create allow-api-server-to-digester-webhook \ 170 | --action ALLOW \ 171 | --direction INGRESS \ 172 | --source-ranges "$API_SERVER_CIDR" \ 173 | --rules tcp:8443 \ 174 | --target-tags "$TARGET_TAGS" 175 | ``` 176 | 177 | You can read more about private cluster firewall rules in the 178 | [GKE private cluster documentation](https://cloud.google.com/kubernetes-engine/docs/how-to/private-clusters#add_firewall_rules). 179 | 180 | ## Documentation 181 | 182 | - [Tutorial](https://cloud.google.com/architecture/using-container-image-digests-in-kubernetes-manifests#using_digester) 183 | 184 | - [Motivation](docs/motivation.md) 185 | 186 | - [Recommendations](docs/recommendations.md) 187 | 188 | - [Authenticating to container image registries](docs/authentication.md) 189 | 190 | - [Configuring GKE Workload Identity for authenticating to Container Registry and Artifact Registry](docs/workload-identity.md) 191 | 192 | - [Resolving common issues](docs/common-issues.md) 193 | 194 | - [Troubleshooting](docs/troubleshooting.md) 195 | 196 | - [Building digester](docs/build.md) 197 | 198 | - [Developing digester](docs/development.md) 199 | 200 | - [Releasing digester](docs/release.md) 201 | 202 | ## Disclaimer 203 | 204 | This is not an officially supported Google product. 205 | -------------------------------------------------------------------------------- /build/examples/cronjob.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 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 | apiVersion: batch/v1 16 | kind: CronJob 17 | metadata: 18 | name: hello-cronjob 19 | spec: 20 | schedule: "*/15 * * * *" 21 | jobTemplate: 22 | spec: 23 | template: 24 | spec: 25 | restartPolicy: OnFailure 26 | containers: 27 | - name: hello-cronjob 28 | image: gcr.io/google-samples/hello-app:1.0 29 | -------------------------------------------------------------------------------- /build/examples/deployment.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 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 | apiVersion: apps/v1 16 | kind: Deployment 17 | metadata: 18 | name: hello-deployment 19 | spec: 20 | replicas: 1 21 | selector: 22 | matchLabels: 23 | app: hello 24 | template: 25 | metadata: 26 | labels: 27 | app: hello 28 | spec: 29 | containers: 30 | - name: hello 31 | image: gcr.io/google-samples/hello-app:2.0 32 | -------------------------------------------------------------------------------- /build/examples/pod-diff-registry.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 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 | apiVersion: v1 16 | kind: Pod 17 | metadata: 18 | name: hello-pod 19 | spec: 20 | containers: 21 | - name: busybox 22 | image: busybox:latest 23 | initContainers: 24 | - name: init 25 | image: gcr.io/google-samples/hello-app:1.0 26 | -------------------------------------------------------------------------------- /build/examples/pod.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 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 | apiVersion: v1 16 | kind: Pod 17 | metadata: 18 | name: hello-pod 19 | spec: 20 | containers: 21 | - name: hello 22 | image: gcr.io/google-containers/pause:3.2 23 | initContainers: 24 | - name: init 25 | image: gcr.io/google-samples/hello-app:1.0 26 | -------------------------------------------------------------------------------- /build/test/request-authn.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "admission.k8s.io/v1", 3 | "kind": "AdmissionReview", 4 | "request": { 5 | "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", 6 | "kind": { 7 | "group": "apps", 8 | "version": "v1", 9 | "kind": "Deployment" 10 | }, 11 | "resource": { 12 | "group": "apps", 13 | "version": "v1", 14 | "resource": "deployments" 15 | }, 16 | "subResource": "scale", 17 | "requestKind": { 18 | "group": "apps", 19 | "version": "v1", 20 | "kind": "Deployment" 21 | }, 22 | "requestResource": { 23 | "group": "apps", 24 | "version": "v1", 25 | "resource": "deployments" 26 | }, 27 | "requestSubResource": "deployment", 28 | "name": "my-deployment", 29 | "namespace": "my-namespace", 30 | "operation": "CREATE", 31 | "userInfo": { 32 | "username": "admin", 33 | "uid": "014fbff9a07c", 34 | "groups": [ 35 | "system:authenticated", 36 | "my-admin-group" 37 | ] 38 | }, 39 | "object": { 40 | "apiVersion": "apps/v1", 41 | "kind": "Deployment", 42 | "metadata": { 43 | "name": "hello" 44 | }, 45 | "spec": { 46 | "replicas": 3, 47 | "selector": { 48 | "matchLabels": { 49 | "app": "hello" 50 | } 51 | }, 52 | "template": { 53 | "metadata": { 54 | "labels": { 55 | "app": "hello" 56 | } 57 | }, 58 | "spec": { 59 | "containers": [ 60 | { 61 | "name": "hello-app", 62 | "image": "gcr.io/$PROJECT_ID/hello-app:1.0", 63 | "ports": [ 64 | { 65 | "containerPort": 8080 66 | } 67 | ] 68 | } 69 | ] 70 | } 71 | } 72 | } 73 | }, 74 | "options": {}, 75 | "dryRun": false 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /build/test/request.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "admission.k8s.io/v1", 3 | "kind": "AdmissionReview", 4 | "request": { 5 | "uid": "705ab4f5-6393-11e8-b7cc-42010a800001", 6 | "kind": { 7 | "group": "apps", 8 | "version": "v1", 9 | "kind": "Deployment" 10 | }, 11 | "resource": { 12 | "group": "apps", 13 | "version": "v1", 14 | "resource": "deployments" 15 | }, 16 | "subResource": "scale", 17 | "requestKind": { 18 | "group": "apps", 19 | "version": "v1", 20 | "kind": "Deployment" 21 | }, 22 | "requestResource": { 23 | "group": "apps", 24 | "version": "v1", 25 | "resource": "deployments" 26 | }, 27 | "requestSubResource": "deployment", 28 | "name": "my-deployment", 29 | "namespace": "my-namespace", 30 | "operation": "CREATE", 31 | "userInfo": { 32 | "username": "admin", 33 | "uid": "014fbff9a07c", 34 | "groups": [ 35 | "system:authenticated", 36 | "my-admin-group" 37 | ], 38 | "extra": { 39 | "some-key": [ 40 | "some-value1", 41 | "some-value2" 42 | ] 43 | } 44 | }, 45 | "object": { 46 | "apiVersion": "apps/v1", 47 | "kind": "Deployment", 48 | "metadata": { 49 | "name": "echo-deployment" 50 | }, 51 | "spec": { 52 | "replicas": 3, 53 | "selector": { 54 | "matchLabels": { 55 | "app": "echo" 56 | } 57 | }, 58 | "template": { 59 | "metadata": { 60 | "labels": { 61 | "app": "echo" 62 | } 63 | }, 64 | "spec": { 65 | "containers": [ 66 | { 67 | "name": "echoserver", 68 | "image": "gcr.io/google-containers/echoserver:1.10", 69 | "ports": [ 70 | { 71 | "containerPort": 8080 72 | } 73 | ] 74 | } 75 | ] 76 | } 77 | } 78 | } 79 | }, 80 | "options": {}, 81 | "dryRun": false 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /cmd/function/function.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 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 function provides the command to run the KRM function. 16 | package function 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "os" 22 | "path/filepath" 23 | 24 | "github.com/go-logr/logr" 25 | "github.com/spf13/cobra" 26 | "github.com/spf13/viper" 27 | "k8s.io/client-go/rest" 28 | "k8s.io/client-go/tools/clientcmd" 29 | "k8s.io/client-go/util/homedir" 30 | "sigs.k8s.io/kustomize/kyaml/fn/framework" 31 | "sigs.k8s.io/kustomize/kyaml/fn/framework/command" 32 | 33 | "github.com/google/k8s-digester/pkg/logging" 34 | "github.com/google/k8s-digester/pkg/resolve" 35 | "github.com/google/k8s-digester/pkg/util" 36 | ) 37 | 38 | // Cmd creates the KRM function command. This is the root command. 39 | func Cmd(ctx context.Context) *cobra.Command { 40 | log := logging.CreateStdLogger("digester") 41 | resourceFn := createResourceFn(ctx, log) 42 | cmd := command.Build(framework.ResourceListProcessorFunc(resourceFn), command.StandaloneDisabled, false) 43 | customizeCmd(cmd) 44 | return cmd 45 | } 46 | 47 | // createResourceFn returns a function that iterates over the items in the 48 | // resource list. 49 | func createResourceFn(ctx context.Context, log logr.Logger) framework.ResourceListProcessorFunc { 50 | return func(resourceList *framework.ResourceList) error { 51 | log.V(2).Info("kubeconfig", "kubeconfig", viper.GetString("kubeconfig")) 52 | log.V(2).Info("offline", "offline", viper.GetBool("offline")) 53 | log.V(2).Info("skip-prefixes", "skip-prefixes", util.StringArray(viper.GetString("skip-prefixes"))) 54 | var config *rest.Config 55 | if !viper.GetBool("offline") { 56 | var kubeconfig string 57 | var err error 58 | kubeconfigs := util.StringArray(viper.GetString("kubeconfig")) 59 | if len(kubeconfigs) > 0 { 60 | kubeconfig = kubeconfigs[0] 61 | } 62 | 63 | config, err = createConfig(log, kubeconfig) 64 | if err != nil { 65 | return fmt.Errorf("could not create k8s client config: %w", err) 66 | } 67 | } 68 | for _, r := range resourceList.Items { 69 | if err := resolve.ImageTags(ctx, log, config, r, util.StringArray(viper.GetString("skip-prefixes"))); err != nil { 70 | return err 71 | } 72 | } 73 | return nil 74 | } 75 | } 76 | 77 | // customizeCmd modifies the kyaml function framework command by adding flags 78 | // that this KRM function needs, and to make it more user-friendly. 79 | func customizeCmd(cmd *cobra.Command) { 80 | cmd.Use = "digester" 81 | cmd.Short = "Resolve container image tags to digests" 82 | cmd.Long = "Digester adds digests to container and " + 83 | "init container images in Kubernetes pod and pod template " + 84 | "specs.\n\nUse either as a mutating admission webhook, " + 85 | "or as a client-side KRM function with kpt or kustomize." 86 | cmd.Flags().String("kubeconfig", getKubeconfigDefault(), 87 | "(optional) absolute path to the kubeconfig file. Requires offline=false.") 88 | viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) 89 | viper.BindEnv("kubeconfig") 90 | cmd.Flags().Bool("offline", true, 91 | "do not connect to Kubernetes API server to retrieve imagePullSecrets") 92 | viper.BindPFlag("offline", cmd.Flags().Lookup("offline")) 93 | viper.BindEnv("offline") 94 | cmd.Flags().String("skip-prefixes", "", "(optional) image prefixes that should not be resolved to digests, colon separated") 95 | viper.BindPFlag("skip-prefixes", cmd.Flags().Lookup("skip-prefixes")) 96 | viper.BindEnv("skip-prefixes", "SKIP_PREFIXES") 97 | } 98 | 99 | // getKubeconfigDefault determines the default value of the --kubeconfig flag. 100 | func getKubeconfigDefault() string { 101 | var kubeconfigDefault string 102 | home := homedir.HomeDir() 103 | if home != "" { 104 | kubeconfigHomePath := filepath.Join(home, ".kube", "config") 105 | if _, err := os.Stat(kubeconfigHomePath); err == nil { 106 | kubeconfigDefault = kubeconfigHomePath 107 | } 108 | } 109 | return kubeconfigDefault 110 | } 111 | 112 | // createConfig creates a k8s client config using either in-cluster config 113 | // or the provided kubeconfig file. 114 | func createConfig(log logr.Logger, kubeconfig string) (*rest.Config, error) { 115 | if kubeconfig == "" { 116 | log.V(1).Info("using in-cluster config") 117 | return rest.InClusterConfig() 118 | } 119 | log.V(1).Info("using kubeconfig file", "kubeconfig", kubeconfig) 120 | return clientcmd.BuildConfigFromFlags("", kubeconfig) 121 | } 122 | -------------------------------------------------------------------------------- /cmd/version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 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 version prints the version of this tool, as provided at compile time. 16 | package version 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | 22 | "github.com/spf13/cobra" 23 | 24 | "github.com/google/k8s-digester/pkg/version" 25 | ) 26 | 27 | var ( 28 | writer = os.Stdout 29 | 30 | // Cmd is the version sub-command 31 | Cmd = &cobra.Command{ 32 | Use: "version", 33 | Short: "Print the version information", 34 | RunE: func(_ *cobra.Command, _ []string) error { 35 | return printVersion() 36 | }, 37 | } 38 | ) 39 | 40 | func printVersion() error { 41 | _, err := fmt.Fprintln(writer, version.Version) 42 | return err 43 | } 44 | -------------------------------------------------------------------------------- /cmd/webhook/webhook.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 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 webhook provides the command to run the Kubernetes mutating admission webhook. 16 | package webhook 17 | 18 | import ( 19 | "context" 20 | "flag" 21 | "fmt" 22 | 23 | "github.com/go-logr/logr" 24 | "github.com/open-policy-agent/cert-controller/pkg/rotator" 25 | "github.com/spf13/cobra" 26 | corev1 "k8s.io/api/core/v1" 27 | "k8s.io/apimachinery/pkg/runtime" 28 | "k8s.io/apimachinery/pkg/types" 29 | "k8s.io/client-go/rest" 30 | "sigs.k8s.io/controller-runtime/pkg/client/config" 31 | "sigs.k8s.io/controller-runtime/pkg/healthz" 32 | "sigs.k8s.io/controller-runtime/pkg/manager" 33 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 34 | "sigs.k8s.io/controller-runtime/pkg/webhook" 35 | "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 36 | 37 | "github.com/google/k8s-digester/pkg/handler" 38 | "github.com/google/k8s-digester/pkg/logging" 39 | "github.com/google/k8s-digester/pkg/util" 40 | ) 41 | 42 | const ( 43 | caName = "digester-ca" 44 | caOrganization = "digester" 45 | defaultCertDir = "/certs" 46 | defaultMetricsAddr = ":8888" 47 | defaultHealthAddr = ":9090" 48 | defaultPort = 8443 49 | secretName = "digester-webhook-server-cert" // matches the Secret name 50 | serviceName = "digester-webhook-service" // matches the Service name 51 | webhookName = "digester-mutating-webhook-configuration" // matches the MutatingWebhookConfiguration name 52 | webhookPath = "/v1/mutate" // matches the MutatingWebhookConfiguration clientConfig path 53 | ) 54 | 55 | // Cmd is the webhook controller manager sub-command 56 | var Cmd = &cobra.Command{ 57 | Use: "webhook", 58 | Short: "Start a Kubernetes mutating admission webhook controller manager", 59 | RunE: func(cmd *cobra.Command, _ []string) error { 60 | return run(cmd.Context()) 61 | }, 62 | } 63 | 64 | var ( 65 | dnsName = fmt.Sprintf("%s.%s.svc", serviceName, util.GetNamespace()) 66 | webhooks = []rotator.WebhookInfo{ 67 | { 68 | Name: webhookName, 69 | Type: rotator.Mutating, 70 | }, 71 | } 72 | ) 73 | 74 | var ( 75 | certDir string 76 | disableCertRotation bool 77 | dryRun bool 78 | healthAddr string 79 | metricsAddr string 80 | offline bool 81 | port int 82 | ignoreErrors bool 83 | skipPrefixes string 84 | ) 85 | 86 | func init() { 87 | Cmd.Flags().StringVar(&certDir, "cert-dir", defaultCertDir, "directory where TLS certificates and keys are stored") 88 | Cmd.Flags().BoolVar(&disableCertRotation, "disable-cert-rotation", false, "disable automatic generation and rotation of webhook TLS certificates/keys") 89 | Cmd.Flags().BoolVar(&dryRun, "dry-run", false, "if true, do not mutate any resources") 90 | Cmd.Flags().StringVar(&healthAddr, "health-addr", defaultHealthAddr, "health endpoint address") 91 | Cmd.Flags().AddGoFlag(flag.Lookup("kubeconfig")) 92 | Cmd.Flags().StringVar(&metricsAddr, "metrics-addr", defaultMetricsAddr, "metrics endpoint address") 93 | Cmd.Flags().BoolVar(&offline, "offline", false, "do not connect to API server to retrieve imagePullSecrets") 94 | Cmd.Flags().IntVar(&port, "port", defaultPort, "webhook server port") 95 | Cmd.Flags().BoolVar(&ignoreErrors, "ignore-errors", false, "do not fail on webhook admission errors, just log them") 96 | Cmd.Flags().StringVar(&skipPrefixes, "skip-prefixes", "", "(optional) image prefixes that should not be resolved to digests, colon separated") 97 | } 98 | 99 | func run(ctx context.Context) error { 100 | syncLogger, err := logging.CreateZapLogger("manager") 101 | if err != nil { 102 | return fmt.Errorf("could not create zap logger %w", err) 103 | } 104 | defer syncLogger.Sync() 105 | log := syncLogger.Log 106 | 107 | cfg, err := config.GetConfig() 108 | if err != nil { 109 | return fmt.Errorf("unable to get kubeconfig: %w", err) 110 | } 111 | scheme := runtime.NewScheme() 112 | if err := corev1.AddToScheme(scheme); err != nil { 113 | return fmt.Errorf("could not add core/v1 Kubernetes resources to scheme: %w", err) 114 | } 115 | mgr, err := manager.New(cfg, manager.Options{ 116 | Scheme: scheme, 117 | Logger: log, 118 | LeaderElection: false, 119 | Metrics: metricsserver.Options{ 120 | BindAddress: metricsAddr, 121 | }, 122 | HealthProbeBindAddress: healthAddr, 123 | WebhookServer: webhook.NewServer(webhook.Options{ 124 | Port: port, 125 | CertDir: certDir, 126 | }), 127 | }) 128 | if err != nil { 129 | return fmt.Errorf("unable to set up manager: %w", err) 130 | } 131 | if err := mgr.AddReadyzCheck("default", healthz.Ping); err != nil { 132 | return fmt.Errorf("unable to create readyz check: %w", err) 133 | } 134 | if err := mgr.AddHealthzCheck("default", healthz.Ping); err != nil { 135 | return fmt.Errorf("unable to create healthz check: %w", err) 136 | } 137 | certSetupFinished := make(chan struct{}) 138 | if !disableCertRotation { 139 | log.Info("setting up cert rotation") 140 | if err := rotator.AddRotator(mgr, &rotator.CertRotator{ 141 | SecretKey: types.NamespacedName{ 142 | Namespace: util.GetNamespace(), 143 | Name: secretName, 144 | }, 145 | CertDir: certDir, 146 | CAName: caName, 147 | CAOrganization: caOrganization, 148 | DNSName: dnsName, 149 | IsReady: certSetupFinished, 150 | Webhooks: webhooks, 151 | }); err != nil { 152 | return fmt.Errorf("unable to set up cert rotation: %w", err) 153 | } 154 | } else { 155 | log.Info("skipping certificate provisioning setup") 156 | close(certSetupFinished) 157 | } 158 | 159 | go setupControllers(mgr, log, dryRun, ignoreErrors, certSetupFinished, util.StringArray(skipPrefixes)) 160 | 161 | log.Info("starting manager") 162 | if err := mgr.Start(ctx); err != nil { 163 | return fmt.Errorf("problem running manager: %w", err) 164 | } 165 | return nil 166 | } 167 | 168 | func setupControllers(mgr manager.Manager, log logr.Logger, dryRun bool, ignoreErrors bool, certSetupFinished chan struct{}, skipPrefixes []string) { 169 | log.Info("waiting for cert rotation setup") 170 | <-certSetupFinished 171 | log.Info("done waiting for cert rotation setup") 172 | var k8sClientConfig *rest.Config 173 | if !offline { 174 | k8sClientConfig = mgr.GetConfig() 175 | } 176 | whh := &handler.Handler{ 177 | Log: log.WithName("webhook"), 178 | DryRun: dryRun, 179 | IgnoreErrors: ignoreErrors, 180 | Config: k8sClientConfig, 181 | SkipPrefixes: skipPrefixes, 182 | } 183 | mwh := &admission.Webhook{Handler: whh} 184 | log.Info("starting webhook server", "path", webhookPath) 185 | mgr.GetWebhookServer().Register(webhookPath, mwh) 186 | } 187 | -------------------------------------------------------------------------------- /cmd/webhook/webhook_suite_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 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 webhook 16 | 17 | import ( 18 | "flag" 19 | "os" 20 | "testing" 21 | 22 | admissionregistrationv1 "k8s.io/api/admissionregistration/v1" 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | "sigs.k8s.io/controller-runtime/pkg/envtest" 25 | 26 | "github.com/google/k8s-digester/pkg/logging" 27 | "github.com/google/k8s-digester/pkg/util" 28 | ) 29 | 30 | var testEnv *envtest.Environment 31 | 32 | // TestMain starts the API server 33 | func TestMain(m *testing.M) { 34 | flag.Parse() 35 | if testing.Short() { 36 | stdlog := logging.CreateStdLogger("webhook_suite_test") 37 | stdlog.Info("skipping integration test suite in short mode") 38 | return 39 | } 40 | admissionRegistrationNamespacedScope := admissionregistrationv1.NamespacedScope 41 | admissionRegistrationFailurePolicyFail := admissionregistrationv1.Fail 42 | admissionregistrationIfNeededReinvocationPolicy := admissionregistrationv1.IfNeededReinvocationPolicy 43 | admissionregistrationSideEffectClassNone := admissionregistrationv1.SideEffectClassNone 44 | clientConfigWebhookPath := webhookPath 45 | 46 | testEnv = &envtest.Environment{ 47 | AttachControlPlaneOutput: true, 48 | WebhookInstallOptions: envtest.WebhookInstallOptions{ 49 | MutatingWebhooks: []*admissionregistrationv1.MutatingWebhookConfiguration{ 50 | { 51 | TypeMeta: metav1.TypeMeta{ 52 | APIVersion: "admissionregistration.k8s.io/v1", 53 | Kind: "MutatingWebhookConfiguration", 54 | }, 55 | ObjectMeta: metav1.ObjectMeta{ 56 | Name: webhookName, 57 | }, 58 | Webhooks: []admissionregistrationv1.MutatingWebhook{ 59 | { 60 | Name: dnsName, 61 | AdmissionReviewVersions: []string{"v1", "v1beta1"}, 62 | ClientConfig: admissionregistrationv1.WebhookClientConfig{ 63 | Service: &admissionregistrationv1.ServiceReference{ 64 | Name: serviceName, 65 | Namespace: util.GetNamespace(), 66 | Path: &clientConfigWebhookPath, 67 | }, 68 | }, 69 | FailurePolicy: &admissionRegistrationFailurePolicyFail, 70 | NamespaceSelector: &metav1.LabelSelector{ 71 | MatchLabels: map[string]string{ 72 | "digest-resolution": "enabled", 73 | }, 74 | }, 75 | ReinvocationPolicy: &admissionregistrationIfNeededReinvocationPolicy, 76 | Rules: []admissionregistrationv1.RuleWithOperations{ 77 | { 78 | Operations: []admissionregistrationv1.OperationType{ 79 | admissionregistrationv1.Create, 80 | admissionregistrationv1.Update, 81 | }, 82 | Rule: admissionregistrationv1.Rule{ 83 | APIGroups: []string{""}, 84 | APIVersions: []string{"v1"}, 85 | Resources: []string{"pods"}, 86 | Scope: &admissionRegistrationNamespacedScope, 87 | }, 88 | }, 89 | }, 90 | SideEffects: &admissionregistrationSideEffectClassNone, 91 | }, 92 | }, 93 | }, 94 | }, 95 | }, 96 | } 97 | 98 | testEnv.ControlPlane.GetAPIServer().Configure().Append( 99 | "enable-admission-plugins", "ValidatingAdmissionWebhook,MutatingAdmissionWebhook", 100 | ) 101 | 102 | log := logging.CreateKlogLogger() 103 | if _, err := testEnv.Start(); err != nil { 104 | log.Error(err, "problem starting API server") 105 | os.Exit(1) 106 | } 107 | 108 | exitCode := m.Run() 109 | defer os.Exit(exitCode) 110 | 111 | if err := testEnv.Stop(); err != nil { 112 | log.Error(err, "problem stopping API server") 113 | os.Exit(1) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /cmd/webhook/webhook_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 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 webhook 16 | 17 | import ( 18 | "testing" 19 | 20 | corev1 "k8s.io/api/core/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | "k8s.io/apimachinery/pkg/runtime" 23 | "k8s.io/apimachinery/pkg/types" 24 | ctrl "sigs.k8s.io/controller-runtime" 25 | "sigs.k8s.io/controller-runtime/pkg/webhook" 26 | 27 | "github.com/google/k8s-digester/pkg/handler" 28 | "github.com/google/k8s-digester/pkg/logging" 29 | ) 30 | 31 | func Test_ResolvePublicGCRImage(t *testing.T) { 32 | ctx := ctrl.SetupSignalHandler() 33 | scheme := runtime.NewScheme() 34 | if err := corev1.AddToScheme(scheme); err != nil { 35 | t.Fatalf("could not add core v1 Kubernetes resources to scheme") 36 | } 37 | mgr, err := ctrl.NewManager(testEnv.Config, ctrl.Options{ 38 | Scheme: scheme, 39 | WebhookServer: webhook.NewServer(webhook.Options{ 40 | Host: testEnv.WebhookInstallOptions.LocalServingHost, 41 | Port: testEnv.WebhookInstallOptions.LocalServingPort, 42 | CertDir: testEnv.WebhookInstallOptions.LocalServingCertDir, 43 | }), 44 | }) 45 | if err != nil { 46 | t.Fatalf("could not create test controller manager: %+v", err) 47 | } 48 | t.Logf("webhook address: https://%v:%v%v", 49 | testEnv.WebhookInstallOptions.LocalServingHost, 50 | testEnv.WebhookInstallOptions.LocalServingPort, 51 | webhookPath) 52 | 53 | webhookServer := mgr.GetWebhookServer() 54 | webhookServer.Register(webhookPath, &webhook.Admission{ 55 | Handler: &handler.Handler{ 56 | Log: logging.CreateKlogLogger(), 57 | }, 58 | }) 59 | go func() { 60 | if err := mgr.Start(ctx); err != nil { 61 | t.Errorf("could not start test controller manager: %+v", err) 62 | } 63 | }() 64 | 65 | client := mgr.GetClient() 66 | 67 | namespace := &corev1.Namespace{ 68 | TypeMeta: metav1.TypeMeta{ 69 | APIVersion: "v1", 70 | Kind: "Namespace", 71 | }, 72 | ObjectMeta: metav1.ObjectMeta{ 73 | Name: "test-ns", 74 | Labels: map[string]string{ 75 | "digest-resolution": "enabled", 76 | }, 77 | }, 78 | } 79 | if err := client.Create(ctx, namespace); err != nil { 80 | t.Fatalf("could not create test namespace: %+v", err) 81 | } 82 | 83 | podIn := &corev1.Pod{ 84 | TypeMeta: metav1.TypeMeta{ 85 | APIVersion: "v1", 86 | Kind: "Pod", 87 | }, 88 | ObjectMeta: metav1.ObjectMeta{ 89 | Name: "test-pod", 90 | Namespace: "test-ns", 91 | }, 92 | Spec: corev1.PodSpec{ 93 | Containers: []corev1.Container{ 94 | { 95 | Name: "test-container", 96 | Image: "index.docker.io/docker/whalesay:latest", 97 | }, 98 | }, 99 | }, 100 | } 101 | if err := client.Create(ctx, podIn); err != nil { 102 | t.Fatalf("could not create test pod: %+v", err) 103 | } 104 | 105 | podOut := &corev1.Pod{} 106 | if err := client.Get(ctx, 107 | types.NamespacedName{ 108 | Name: "test-pod", 109 | Namespace: "test-ns", 110 | }, podOut); err != nil { 111 | t.Fatalf("could not get test pod: %+v", err) 112 | } 113 | 114 | wantImage := "index.docker.io/docker/whalesay:latest@sha256:178598e51a26abbc958b8a2e48825c90bc22e641de3d31e18aaf55f3258ba93b" 115 | gotImage := podOut.Spec.Containers[0].Image 116 | if wantImage != gotImage { 117 | t.Errorf("wanted %s, got %s", wantImage, gotImage) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /docs/authentication.md: -------------------------------------------------------------------------------- 1 | # Authenticating to container image registries 2 | 3 | To resolve digests for private images, digester requires credentials to 4 | authenticate to container image registries. 5 | 6 | ## Authentication modes 7 | 8 | Digester supports two modes of authentication: offline and online. 9 | 10 | ### Offline authentication 11 | 12 | When using offline authentication, digester uses credentials available on the 13 | node or machine where it runs. This includes the following credentials: 14 | 15 | 1. Google service account credentials available via 16 | [Application Default Credentials](https://cloud.google.com/docs/entication/production#auth-cloud-implicit-go) 17 | for authenticating to 18 | [Container Registry](https://cloud.google.com/container-registry/docs) and 19 | [Artifact Registry](https://cloud.google.com/artifact-registry/docs). 20 | 21 | For implementation details, see the 22 | [github.com/google/go-containerregistry/pkg/v1/google](https://pkg.go.dev/github.com/google/go-containerregistry/pkg/v1/google) 23 | and 24 | [golang.org/x/oauth2/google](https://pkg.go.dev/golang.org/x/oauth2/google) 25 | Go packages. 26 | 27 | 2. Credentials and credential helpers specified in the 28 | [Docker config file](https://github.com/google/go-containerregistry/tree/main/pkg/authn#docker-config-auth), 29 | for authenticating to any container image registry. The file name is 30 | `config.json`, and the default file location is the directory 31 | `$HOME/.docker`. You can override the default location of the config file 32 | using the `DOCKER_CONFIG` environment variable. 33 | 34 | For implementation details, see the 35 | [github.com/google/go-containerregistry/pkg/authn](https://pkg.go.dev/ub.com/google/go-containerregistry/pkg/authn) 36 | and 37 | [github.com/docker/cli/cli/config](https://pkg.go.dev/github.com/docker/cli/cli/config) 38 | Go packages. 39 | 40 | 3. Ambient credentials for GitHub Container Registry (`ghcr.io`), 41 | Amazon Elastic Container Registry, and Azure Container Registry. 42 | 43 | ### Online authentication 44 | 45 | When using online authentication, digester authenticates using the following 46 | credentials: 47 | 48 | 1. The `imagePullSecrets` listed in the 49 | [pod specification](https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod) 50 | and the 51 | [service account](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account) 52 | used by the pod. Digester retrieves these secrets from the Kubernetes 53 | cluster API server. 54 | 55 | For implementation details, see the 56 | [github.com/google/go-containerregistry/pkg/authn/kubernetes](https://pkg.go.dev/github.com/google/go-containerregistry/pkg/authn/kubernetes) 57 | Go package. 58 | 59 | 2. Credentials specified in the 60 | [Docker config file](https://github.com/google/go-containerregistry/tree/main/pkg/authn#docker-config-auth), 61 | for authenticating to any container image registry. The file name is 62 | `config.json`, and the location can be the container working directory 63 | (`$PWD/config.json`), or a directory called `.docker` under the user 64 | home directory (`$HOME/.docker/config.json`) or the file system root 65 | directory (`/.docker/config.json`). 66 | 67 | For implementation details, see the 68 | [github.com/google/go-containerregistry/pkg/authn](https://pkg.go.dev/ub.com/google/go-containerregistry/pkg/authn) 69 | and 70 | [github.com/docker/cli/cli/config](https://pkg.go.dev/github.com/docker/cli/cli/config) 71 | Go packages. 72 | 73 | 3. Cloud provider-specific instances of the 74 | [`Keychain` interface](https://pkg.go.dev/github.com/google/go-containerregistry/pkg/authn#Keychain). 75 | For instance, the 76 | [implementation for Google](https://pkg.go.dev/github.com/google/go-containerregistry/pkg/v1/google#Keychain) 77 | uses Google service account credentials from the file referenced by the 78 | [`GOOGLE_APPLICATION_CREDENTIALS` environment variable](https://cloud.google.com/docs/authentication/getting-started#setting_the_environment_variable), 79 | or retrieves credentials from the node or Workload Identity 80 | [metadata server](https://cloud.google.com/compute/docs/metadata/overview). 81 | 82 | For implementation details, see the 83 | [github.com/google/go-containerregistry/pkg/authn/k8schain](https://pkg.go.dev/github.com/google/go-containerregistry/pkg/authn/k8schain) 84 | Go package. 85 | 86 | The client-side KRM function defaults to offline authentication, whereas the 87 | webhook defaults to online authentication. You can override the default 88 | authentication mode using the `--offline` command-line flag or the `OFFLINE` 89 | environment variable. 90 | 91 | ## KRM function offline authentication 92 | 93 | The KRM function uses offline authentication by default. By running digester 94 | as a local binary using the kpt `--exec` flag, or the kustomize `--exec-path` 95 | flag, the KRM function has access to container image registry credentials in 96 | the current environment, such as the current user's 97 | [Docker config file](https://github.com/google/go-containerregistry/blob/main/pkg/authn/README.md#the-config-file) 98 | and 99 | [credential helpers](https://docs.docker.com/engine/reference/commandline/login/#credential-helper-protocol). 100 | 101 | Run digester as a local binary using the kpt `--exec` flag: 102 | 103 | ```sh 104 | kpt fn eval [manifest directory] --exec [path/to/digester] 105 | ``` 106 | 107 | If your Docker config file contains your container image registry credentials 108 | and you do not need a credential helper, you can run digester in a container. 109 | Mount your Docker config file in the container using the `--mount` flag: 110 | 111 | ```sh 112 | VERSION=v0.1.16 113 | kpt fn eval [manifest directory] \ 114 | --as-current-user \ 115 | --env DOCKER_CONFIG=/.docker \ 116 | --image ghcr.io/google/k8s-digester:$VERSION \ 117 | --mount type=bind,src="$HOME/.docker/config.json",dst=/.docker/config.json \ 118 | --network 119 | ``` 120 | 121 | The `--network` flag provides external network access to digester running in 122 | the container. Digester requires this to connect to the container image 123 | registry. 124 | 125 | ## KRM function online authentication 126 | 127 | To use online authentication with the digester KRM function, set the 128 | `OFFLINE=false` environment variable. Use this command to run the digester KRM 129 | function as a local binary:: 130 | 131 | ```sh 132 | OFFLINE=false kpt fn eval [manifest directory] --exec ./digester 133 | ``` 134 | 135 | If you want to run the KRM function in a container, mount your kubeconfig file: 136 | 137 | ```sh 138 | VERSION=v0.1.16 139 | kpt fn eval [manifest directory] \ 140 | --as-current-user \ 141 | --env KUBECONFIG=/.kube/config \ 142 | --env OFFLINE=false \ 143 | --image ghcr.io/google/k8s-digester:$VERSION \ 144 | --mount type=bind,src="$HOME/.kube/config",dst=/.kube/config \ 145 | --network 146 | ``` 147 | 148 | When using online authentication, digester connects to the Kubernetes cluster 149 | defined by your current 150 | [kubeconfig context](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/). 151 | 152 | The user defined by the current context must have permissions to read the 153 | `imagePullSecrets` and service accounts listed in the Pod specifications. 154 | 155 | You can provide an alternative kubeconfig file by setting the value of the 156 | `--kubeconfig` command-line flag or the `KUBECONFIG` environment variable to 157 | the full path of an alternative kubeconfig file. 158 | 159 | ## Webhook online authentication 160 | 161 | The webhook uses online authentication by default, and it uses the 162 | `digester-admin` Kubernetes service account to authenticate to the API server. 163 | 164 | The `digester-manager-role` ClusterRole provides read access to all 165 | Secrets and ServiceAccounts in the cluster, and the 166 | `digester-manager-rolebinding` ClusterRoleBinding binds this role to the 167 | `digester-admin` Kubernetes service account in the `digester-system` namespace. 168 | 169 | ## Webhook offline authentication 170 | 171 | If you don't want to give the digester webhook read access to Secrets and 172 | ServiceAccounts in the cluster, you can enable offline authentication 173 | (`--offline=true`). With offline authentication, you can provide credentials to 174 | the webhook using a 175 | [Docker config file](https://github.com/google/go-containerregistry/blob/main/pkg/authn/README.md#the-config-file): 176 | 177 | 1. Set the `offline` flag value to `true` in the webhook Deployment manifest: 178 | 179 | ```sh 180 | kpt fn eval manifests --image gcr.io/kpt-fn/apply-setters:v0.2 -- offline=true 181 | ``` 182 | 183 | 2. Create a Docker config file containing map entries with usernames and 184 | passwords for your registries: 185 | 186 | ```sh 187 | REGISTRY_HOST=[your container image registry authority, e.g., registry.gitlab.com] 188 | REGISTRY_USERNAME=[your container image registry user name] 189 | REGISTRY_PASSWORD=[your container image registry password or token] 190 | 191 | cat << EOF > docker-config.json 192 | { 193 | "auths": { 194 | "$REGISTRY_HOST": { 195 | "username": "$REGISTRY_USERNAME", 196 | "password": "$REGISTRY_PASSWORD" 197 | } 198 | } 199 | } 200 | EOF 201 | ``` 202 | 203 | 3. Create a Secret in the `digester-system` namespace containing the config 204 | file: 205 | 206 | ```sh 207 | kubectl create secret generic docker-config --namespace digester-system \ 208 | --from-file config.json=$(pwd)/docker-config.json 209 | ``` 210 | 211 | 4. Create a patch file for the `webhook-controller-manager` Deployment. The 212 | patch adds the Docker config file Secret as a volume, and mounts the volume 213 | on the Pods: 214 | 215 | ```sh 216 | cat << EOF > manifests/docker-config-patch.json 217 | [ 218 | { 219 | "op": "add", 220 | "path": "/spec/template/spec/containers/0/volumeMounts/-", 221 | "value":{ 222 | "mountPath": ".docker", 223 | "name": "docker", 224 | "readOnly": true 225 | } 226 | }, 227 | { 228 | "op": "add", 229 | "path": "/spec/template/spec/volumes/-", 230 | "value": { 231 | "name": "docker", 232 | "secret": { 233 | "defaultMode": 420, 234 | "secretName": "docker-config" 235 | } 236 | } 237 | } 238 | ] 239 | EOF 240 | ``` 241 | 242 | 4. Add the patch to the kustomize manifest: 243 | 244 | ```sh 245 | cat << EOF >> manifests/Kustomization 246 | patches: 247 | - path: docker-config-patch.json 248 | target: 249 | group: apps 250 | version: v1 251 | kind: Deployment 252 | name: digester-controller-manager 253 | EOF 254 | ``` 255 | 256 | 4. Deploy the webhook with the patch: 257 | 258 | ```sh 259 | kubectl apply --kustomize manifests 260 | ``` 261 | 262 | If you use offline authentication, you can remove the rule in the 263 | `digester-manager-role` ClusterRole that grants access to `secrets` and 264 | `serviceaccounts`, see 265 | [`manifests/cluster-role.yaml`](../manifests/cluster-role.yaml). 266 | -------------------------------------------------------------------------------- /docs/build.md: -------------------------------------------------------------------------------- 1 | # Build 2 | 3 | This document explains how to build your own binaries or container images for 4 | digester. 5 | 6 | Before you proceed, clone the Git repository and install the following tools: 7 | 8 | - [Go distribution](https://golang.org/doc/install) v1.17 or later 9 | - [kustomize](https://kubectl.docs.kubernetes.io/installation/kustomize/) v3.7.0 or later 10 | - [Skaffold](https://skaffold.dev/docs/install/#standalone-binary) v1.37.2 or later 11 | 12 | ## Building binaries and container images 13 | 14 | - Build the binary: 15 | 16 | ```sh 17 | go build -o digester . 18 | ``` 19 | 20 | - Build a container image and load it into your local Docker daemon: 21 | 22 | ```sh 23 | skaffold build --cache-artifacts=false --push=false 24 | ``` 25 | 26 | - Build a container image and push it to Container Registry: 27 | 28 | ```sh 29 | skaffold build --push --default-repo gcr.io/$(gcloud config get core/project) 30 | ``` 31 | 32 | The base image is `gcr.io/distroless/static:nonroot`. If you want to use a 33 | different base image, change the value of the `defaultBaseImage` field in the 34 | file [`.ko.yaml`](ko.yaml). For instance, if you want to use a base image that 35 | contains credential helpers for a number of container registries, you can use a 36 | base image from the `gcr.io/kaniko-project/executor` repository. 37 | 38 | ## Building and deploying the webhook 39 | 40 | 1. (optional) If you use a Google Kubernetes Engine (GKE) cluster with 41 | [Workload Identity](workload-identity.md), and either Container Registry or 42 | Artifact Registry, annotate the digester Kubernetes service account: 43 | 44 | ```sh 45 | kustomize cfg annotate manifests \ 46 | --kind ServiceAccount \ 47 | --name digester-admin \ 48 | --namespace digester-system \ 49 | --kv "iam.gke.io/gcp-service-account=$GSA" 50 | ``` 51 | 52 | This annotation informs GKE that the Kubernetes service account 53 | `digester-admin` in the namespace `digester-system` can impersonate the 54 | Google service account `$GSA`. 55 | 56 | 2. Build and push the webhook container image, and deploy to your Kubernetes cluster: 57 | 58 | ```sh 59 | skaffold run --push --default-repo gcr.io/$(gcloud config get core/project) 60 | ``` 61 | -------------------------------------------------------------------------------- /docs/common-issues.md: -------------------------------------------------------------------------------- 1 | # Resolving common issues 2 | 3 | This section provides solutions to common issues encountered when using 4 | digester. 5 | 6 | ## Self-signed and untrusted certificates 7 | 8 | If your container image registry uses a self-signed certificate, or a 9 | certificate issued by a certificate authority (CA) that is not trusted by the 10 | CA bundle used by digester 11 | ([`ca-certificates`](https://packages.debian.org/stable/ca-certificates)), you 12 | can configure digester with your own CA bundle. 13 | 14 | To do so, set the 15 | [`SSL_CERT_FILE` or `SSL_CERT_DIR` environment variables](https://golang.org/pkg/crypto/x509/#SystemCertPool) 16 | on the `manager` container in the webhook 17 | [deployment resource](../manifests/deployment.yaml). 18 | The steps below use the `SSL_CERT_DIR` environment variable. 19 | 20 | 1. Create a Kubernetes generic Secret containing you CA bundle certificates, 21 | called `my-ca-bundle`, in the `digester-system` namespace: 22 | 23 | ```sh 24 | kubectl create secret generic my-ca-bundle --namespace digester-system \ 25 | --from-file=cert1=/path/to/cert1 --from-file=cert2=/path/to/cert2 26 | ``` 27 | 28 | 2. Create a JSON patch file called `ca-bundle-patch.json` that adds the 29 | `SSL_CERT_DIR` environment variable, a volume, and a volume mount to the 30 | webhook deployment: 31 | 32 | ```json 33 | [ 34 | { 35 | "op": "add", 36 | "path": "/spec/template/spec/containers/0/env/-", 37 | "value":{ 38 | "name": "SSL_CERT_DIR", 39 | "value": "/my-ca-certs" 40 | } 41 | }, 42 | { 43 | "op": "add", 44 | "path": "/spec/template/spec/containers/0/volumeMounts/-", 45 | "value":{ 46 | "mountPath": "/my-ca-certs", 47 | "name": "my-ca-bundle-volume", 48 | "readOnly": true 49 | } 50 | }, 51 | { 52 | "op": "add", 53 | "path": "/spec/template/spec/volumes/-", 54 | "value": { 55 | "name": "my-ca-bundle-volume", 56 | "secret": { 57 | "defaultMode": 420, 58 | "secretName": "my-ca-bundle" 59 | } 60 | } 61 | } 62 | ] 63 | ``` 64 | 65 | 3. Apply the patch: 66 | 67 | ```sh 68 | kubectl patch deployment/digester-controller-manager -n digester-system \ 69 | --type json --patch-file ca-bundle-patch.json 70 | ``` 71 | 72 | Ref: https://knative.dev/docs/serving/tag-resolution/#custom-certificates 73 | 74 | ## Corporate proxies 75 | 76 | If digester needs to traverse a corporate HTTP proxy to reach the container 77 | registry, you can configure digester to use the proxy. 78 | 79 | To do so, set the 80 | [`HTTP_PROXY` or `HTTPS_PROXY` environment variables](https://golang.org/pkg/net/http/#ProxyFromEnvironment) 81 | on the `manager` container in the webhook 82 | [deployment resource](../manifests/deployment.yaml). 83 | The steps below use the `HTTPS_PROXY` environment variable. 84 | 85 | 1. Create a JSON patch file called `http-proxy-patch.json` that adds the 86 | `HTTPS_PROXY` environment variable to the webhook deployment: 87 | 88 | ```json 89 | [ 90 | { 91 | "op": "add", 92 | "path": "/spec/template/spec/containers/0/env/-", 93 | "value":{ 94 | "name": "HTTPS_PROXY", 95 | "value": "http://myproxy.example.com:3128" 96 | } 97 | } 98 | ] 99 | ``` 100 | 101 | 2. Apply the patch: 102 | 103 | ```sh 104 | kubectl patch deployment/digester-controller-manager -n digester-system \ 105 | --type json --patch-file http-proxy-patch.json 106 | ``` 107 | 108 | Note that this will not work for proxies that require NTLM authentication. 109 | 110 | Ref: https://knative.dev/docs/serving/tag-resolution/#corporate-proxy 111 | 112 | ## Interaction with systems expecting tags, particularly cloud managed services 113 | 114 | If digester updates an image tag that is being actively managed by a cloud controller then 115 | it may cause the cloud controller to behave unexpectedly. 116 | 117 | One example of this is the Anthos Service Mesh Managed Dataplane Controller which looks 118 | for specific tagged versions of the istio-proxy sidecar injected by the mutating webhook. 119 | 120 | Replacement of the tagged names with digest values can, under these circumstances, create 121 | an edge case for the cloud managed services handling unepected values in unforseen ways such 122 | as updating pods and terminating them once they have already been updated (since the image 123 | does not match the value set by the controller with only the tag). 124 | 125 | In these circumstances and if you are using digester to provide a tag feature when using 126 | Binary Authorization it is worth noting that there is a capability to whitelist certain 127 | image registries and repo locations within Binary Authorization. ASM images are by default 128 | whitelisted by the policy. 129 | 130 | To avoid digester replacing the tagged version expected by mdp-controller in these instances 131 | one can utilise the --skip-prefixes option to the webhook which takes a set of prefixes 132 | separated by a colon (if multiple prefixes are needed). 133 | 134 | The parameter can be added to the webhook args in the deployment, the following is an 135 | example 136 | ``` 137 | args: 138 | - webhook 139 | - --cert-dir=/certs # kpt-set: --cert-dir=${cert-dir} 140 | - --disable-cert-rotation=false # kpt-set: --disable-cert-rotation=${disable-cert-rotation} 141 | - --dry-run=false # kpt-set: --dry-run=${dry-run} 142 | - --health-addr=:9090 # kpt-set: --health-addr=:${health-port} 143 | - --metrics-addr=:8888 # kpt-set: --metrics-addr=:${metrics-port} 144 | - --offline=false # kpt-set: --offline=${offline} 145 | - --port=8443 # kpt-set: --port=${port} 146 | - --skip-prefixes=gcr.io/gke-release/asm/mdp:gcr.io/gke-release/asm/proxyv2 147 | ``` -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | During development, you can run the KRM function and the webhook locally. 4 | You can also use Skaffold to set up a watch loop that automatically deploys 5 | the webhook to a Kubernetes cluster on source code changes. 6 | 7 | ## Running the KRM function during development 8 | 9 | - Apply the function to a Pod manifest: 10 | 11 | ```sh 12 | DEBUG=true go run . < build/examples/pod.yaml 13 | ``` 14 | 15 | ## Running the webhook locally during development 16 | 17 | 1. Create a self-signed certificate: 18 | 19 | ```sh 20 | mkdir -p build/cert 21 | 22 | openssl req -x509 -newkey rsa:4096 -nodes -sha256 -days 3650 \ 23 | -keyout build/cert/tls.key -out build/cert/tls.crt -extensions san \ 24 | -config \ 25 | <(echo "[req]"; 26 | echo distinguished_name=req; 27 | echo "[san]"; 28 | echo subjectAltName=DNS:localhost,IP:127.0.0.1 29 | ) \ 30 | -subj '/CN=localhost' 31 | ``` 32 | 33 | 2. Run the webhook locally: 34 | 35 | ```sh 36 | DEBUG=true go run . webhook --cert-dir=build/cert --disable-cert-rotation=true --offline=true 37 | ``` 38 | 39 | Setting the `DEBUG=true` environment variable enabled development mode 40 | logging. 41 | 42 | The `--cert-dir` and `--disable-cert-rotation=true` flags means that the 43 | webhook uses the certificate you created in the previous step, instead of 44 | retrieving a certificate from the API server. 45 | 46 | The `--offline=true` flag means that the webhook will not retrieve 47 | `imagePullSecrets` from a Kubernetes API server. 48 | 49 | 3. In another terminal window, send an admission review request for a 50 | Deployment that uses a public image: 51 | 52 | ```sh 53 | curl -sk -X POST -H "Content-Type: application/json" \ 54 | --data @build/test/request.json \ 55 | https://localhost:8443/v1/mutate \ 56 | | jq -r '.response.patch' | base64 --decode | jq 57 | ``` 58 | 59 | The output is the list of JSON patches that the API server admission 60 | process applies to the request object. 61 | 62 | 4. Publish a private image by using `crane` to copy a public image: 63 | 64 | ```sh 65 | export PROJECT_ID=$(gcloud config get core/project) 66 | 67 | curl -sL "https://github.com/google/go-containerregistry/releases/download/v0.5.1/go-containerregistry_$(uname -s)_$(uname -m).tar.gz" \ 68 | | tar -zxf - crane gcrane 69 | 70 | ./crane cp gcr.io/google-samples/hello-app:1.0 gcr.io/$PROJECT_ID/hello-app:1.0 71 | ``` 72 | 73 | 5. Send an admission review request for a Deployment that uses the private 74 | image: 75 | 76 | ```sh 77 | curl -sk -X POST -H "Content-Type: application/json" \ 78 | --data @<(envsubst < build/test/request-authn.json) \ 79 | https://localhost:8443/v1/mutate \ 80 | | jq -r '.response.patch' | base64 --decode | jq 81 | ``` 82 | 83 | ## Redeploying the webhook to a Kubernetes cluster on source code changes 84 | 85 | 1. Create a development Kubernetes cluster, for instance using 86 | [Google Kubernetes Engine (GKE)](https://cloud.google.com/kubernetes-engine/docs), 87 | [Minikube](https://minikube.sigs.k8s.io/), or 88 | [kind](https://kind.sigs.k8s.io/). 89 | 90 | 2. Install these tools: 91 | 92 | - [crane](https://github.com/google/go-containerregistry/tree/main/cmd/crane#installation) 93 | - [ko](https://github.com/google/ko#installation) 94 | - [kpt](https://kpt.dev/installation/) 95 | - [kustomize](https://kubectl.docs.kubernetes.io/installation/kustomize/) 96 | - [Skaffold](https://skaffold.dev/docs/install/) 97 | 98 | 3. Set the Skaffold default container image registry: 99 | 100 | ```sh 101 | export SKAFFOLD_DEFAULT_REPO=gcr.io/$(gcloud config get core/project) 102 | ``` 103 | 104 | 4. (optional) Enable debug mode for more verbose logging: 105 | 106 | ```sh 107 | kpt fn eval manifests --image gcr.io/kpt-fn/apply-setters:v0.2 -- debug=true 108 | ``` 109 | 110 | 5. (optional) Set `replicas` to 1: 111 | 112 | ```sh 113 | kpt fn eval manifests --image gcr.io/kpt-fn/apply-setters:v0.2 -- replicas=1 114 | ``` 115 | 116 | 6. Deploy the webhook and start the watch loop: 117 | 118 | ```sh 119 | skaffold dev 120 | ``` 121 | -------------------------------------------------------------------------------- /docs/motivation.md: -------------------------------------------------------------------------------- 1 | # Motivation 2 | 3 | We created digester to make it easier for Kubernetes users to deploy container 4 | images by digest, and to assist users of 5 | [Binary Authorization](https://cloud.google.com/binary-authorization/docs). 6 | 7 | ## What are container image digests? 8 | 9 | A 10 | [container image digest](https://github.com/opencontainers/image-spec/blob/master/descriptor.md#digests) 11 | uniquely and immutably identifies a container image. 12 | 13 | The digest value is the result of applying a 14 | [collision-resistant hash function](https://wikipedia.org/wiki/Collision_resistance), 15 | typically [SHA-256](https://wikipedia.org/wiki/SHA-2), 16 | to the image index, manifest list, or image manifest. 17 | 18 | If you are not familiar with image digests, read the document 19 | [Using container image digests](https://cloud.google.com/architecture/using-container-images). 20 | 21 | ## Why deploy with digests instead of tags? 22 | 23 | When you deploy images by digest, you avoid the downsides of deploying by 24 | [image tags](https://github.com/opencontainers/distribution-spec/blob/master/spec.md). 25 | 26 | Tags are commonly used to refer to different revisions of a container image, 27 | for example, `v1.0.1`, to refer to a version that you call 1.0.1. Tags make 28 | image revisions easy to look up by human-readable strings. However, tags are 29 | mutable references, which means the image referenced by a tag can change. 30 | 31 | If you publish a new image using the same tag as an existing image, the tag 32 | stops pointing to the existing image and starts pointing to your new image. 33 | 34 | Because tags are mutable, they have the following disadvantages when you use 35 | them to deploy an image: 36 | 37 | - In Kubernetes, deploying by tag can result in unexpected results. For 38 | example, assume that you have an existing Deployment resource that 39 | references a container image by tag `v1.0.1`. To fix a bug or make a small 40 | change, your build process creates a new image with the same tag `v1.0.1`. 41 | New Pods that are created from your Deployment resource can end up using 42 | either the old or the new image, even if you don't change your Deployment 43 | resource specification. This problem also applies to other Kubernetes 44 | resources such as StatefulSets, DaemonSets, ReplicaSets, and Jobs. 45 | 46 | - If you use tools to scan or analyze images, results from these tools are 47 | only valid for the image that was scanned. To ensure that you deploy the 48 | image that was scanned, you cannot rely on the tag because the image 49 | referred to by the tag might have changed. 50 | 51 | - If you use 52 | [Binary Authorization](https://cloud.google.com/binary-authorization/docs) 53 | with 54 | [Google Kubernetes Engine (GKE)](https://cloud.google.com/kubernetes-engine/docs), 55 | tag-based deployment is disallowed because it's impossible to determine 56 | the exact image that is used when a Pod is created. 57 | 58 | - You must decide on which 59 | [imagePullPolicy](https://kubernetes.io/docs/concepts/configuration/overview/#container-images) 60 | to use for the containers in your Pods. 61 | 62 | When you deploy your images, you can use an image digest to avoid these 63 | disadvantages of using tags. You can still add tags to your images if you like, 64 | but you don't have to do so. 65 | 66 | ## Software supply chain security benefits 67 | 68 | Because digests are immutable and unique, using them to deploy images means 69 | that you can cryptographically verify that the image that's running in your 70 | production environment is the exact same image that you produced in your 71 | build process by comparing the digest value. 72 | 73 | In addition, if you want to ensure you only deploy approved images to your 74 | Google Kubernetes Engine (GKE) clusters, you can use 75 | [Binary Authorization](https://cloud.google.com/binary-authorization/docs). 76 | 77 | ## Other solutions 78 | 79 | There are many ways to add image digests to Kubernetes manifests. Some of them 80 | are documented in the tutorial 81 | [Using container image digests in Kubernetes manifests](https://cloud.google.com/architecture/using-container-image-digests-in-kubernetes-manifests). 82 | 83 | [Cloud Run](https://cloud.google.com/run/docs/deploying#service), 84 | [Cloud Run for Anthos](https://cloud.google.com/anthos/run/docs/deploying#service), 85 | and 86 | [Knative Serving](https://knative.dev/docs/serving/tag-resolution/) 87 | resolve image tags to digests on deployment. The digest is stored in a service 88 | revision, and all instances of that service revision use the digest. 89 | 90 | ## References 91 | 92 | - [Using container image digests](https://cloud.google.com/architecture/using-container-images) 93 | - [Using container image digests in Kubernetes manifests](https://cloud.google.com/architecture/using-container-image-digests-in-kubernetes-manifests) 94 | - [k/k#1697: Image name/tag resolution preprocessing pass](https://github.com/kubernetes/kubernetes/issues/1697) 95 | - [Why we resolve tags in Knative](https://docs.google.com/presentation/d/e/2PACX-1vTgyp2lGDsLr_bohx3Ym_2mrTcMoFfzzd6jocUXdmWQFdXydltnraDMoLxvEe6WY9pNPpUUvM-geJ-g/pub?resourcekey=0-FH5lN4C2sbURc_ds8XRHeA) 96 | -------------------------------------------------------------------------------- /docs/recommendations.md: -------------------------------------------------------------------------------- 1 | # Recommendations 2 | 3 | Digester can run either as a mutating admission webhook in a Kubernetes 4 | cluster, or as a client-side KRM function. 5 | 6 | We recommend that you use both the KRM function and the webhook in your 7 | environment. 8 | 9 | The reason for this recommendation is that the KRM function and webhook 10 | complement each other. There are some situations that only the KRM function 11 | or the webhook can handle. By using both the KRM function and the webhook, 12 | you get better coverage of different situations. 13 | 14 | The following sections describe drawbacks of the KRM function and webhook 15 | components, and how you can overcome these drawbacks by using the other 16 | component. 17 | 18 | ## Injected containers 19 | 20 | A drawback of the digester KRM function is that it will not resolve digests 21 | for container and init container images injected by other mutating admission 22 | webhooks, such as the 23 | [Istio sidecar injector](https://istio.io/latest/docs/setup/additional-setup/sidecar-injection/#automatic-sidecar-injection). 24 | 25 | The digester webhook resolves digests for container and init container images 26 | injected by other mutating webhooks because its 27 | [`reinvocationPolicy` is set to `IfNeeded`](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy). 28 | This policy means that the API server executes the digester webhook again if 29 | another webhook mutates the resource after the digester webhook first executed. 30 | 31 | ## Race condition 32 | 33 | The digester webhook resolves the image digest at the time of deployment. This 34 | means that you might encounter a race condition where the image associated with 35 | the tag has changed in the time between when you decided to deploy the image, 36 | and when you actually deployed the image to your cluster. This race condition 37 | might result in deploying an unexpected image. 38 | 39 | Additionally, if you want to deploy the same image to multiple clusters, the 40 | deployments to each cluster might not happen at the same time, and the image 41 | tag might change in between the deployments. The result might be that you 42 | deploy different images to your clusters. 43 | 44 | You can run the digester KRM function at the time you decide to deploy and 45 | store the resulting manifest with digest in source control. You can then use 46 | the manifest with digest to deploy, and this avoids the race condition. 47 | 48 | ## Rolling back 49 | 50 | With the digester webhook, a situation similar to the race condition described 51 | above might occur if you want to roll back an image to a previous version. As 52 | an example, let's say that you are currently running an image with the tag `v1` 53 | in your cluster, and the webhook resolved this tag to a digest value. You then 54 | deploy a new version `v2` of your image, but there is a problem and you decide 55 | that you want to roll back to version `v1`. 56 | 57 | If you did not record the digest of the `v1` image from the first deployment, 58 | the digester webhook will resolve the `v1` tag to a digest again when you roll 59 | back. The tag `v1` might have changed to point to a new image in the time 60 | between when you first deployed `v1` and when you rolled back. The result might 61 | be that your rollback results in deploying a different image to the first time 62 | you deployed `v1`. 63 | 64 | You can run the digester KRM function the first time you deploy the `v1` 65 | image and store the resulting manifest with digest in source control. Then, 66 | when you want to roll back from `v2` to `v1`, you can apply the manifest you 67 | stored. This manifest contains the image digest, so after you roll back, you 68 | will be running the same image as before you deployed `v2`. 69 | 70 | ## Resource coverage 71 | 72 | Another drawback of the digester webhook is that it only mutates the resource 73 | types listed in the rules of the `MutatingWebhookConfiguration`. This includes 74 | pods, podtemplates, replicationcontrollers, daemonsets, deployments, 75 | replicasets, statefulsets, cronjobs, jobs, and Knative Eventing 76 | containersources. 77 | 78 | The digester KRM function does not inspect the resource type or Kind, 79 | and it resolves digests for any resource that contains the fields 80 | `spec.containers`, `spec.initContainers`, `spec.template.spec.containers`, and 81 | `spec.template.spec.initContainers`. 82 | -------------------------------------------------------------------------------- /docs/release.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | 1. Commit your changes: 4 | 5 | ```sh 6 | git commit -m "Stuff" 7 | ``` 8 | 9 | 2. Create an annotated Git tag of the format `v*-rc*`, e.g., `v0.0.1-rc0`: 10 | 11 | ```sh 12 | git tag -a v0.0.1-rc0 -m "v0.0.1-rc0" 13 | ``` 14 | 15 | 3. Push your commits and the tag: 16 | 17 | ```sh 18 | git push --follow-tags 19 | ``` 20 | 21 | If the release job fails and the release tag isn't created, you can fix the 22 | problem and create a new tag with the same version number, bumping the release 23 | candidate (`rc`) number, e.g. `v0.0.1-rc1`. 24 | -------------------------------------------------------------------------------- /docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | Be sure to check out solutions to [common issues](common-issues.md). 4 | 5 | ## KRM function troubleshooting 6 | 7 | If the KRM function fails to look up the image digest, you can increase the 8 | logging verbosity by using the `DEBUG` environment variable: 9 | 10 | ```sh 11 | export DEBUG=true 12 | kpt fn [...] 13 | ``` 14 | 15 | ## Webhook troubleshooting 16 | 17 | The webhook fails open because the 18 | [`failurePolicy`](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) 19 | on the `MutatingWebhookConfiguration` is set to `Ignore`. This means that if 20 | there is an error calling the webhook, the API server allows the request to 21 | continue. 22 | 23 | If the webhook fails to look up the image digest, you can enable development 24 | mode logging and increase the logging verbosity. 25 | 26 | 1. Set the `DEBUG` environment variable to `true` in the webhook Deployment 27 | manifest and redeploy the webhook: 28 | 29 | ```sh 30 | kpt fn eval manifests --image gcr.io/kpt-fn/apply-setters:v0.2 -- debug=true 31 | kpt live apply manifests 32 | ``` 33 | 34 | 2. Tail the webhook logs: 35 | 36 | ```sh 37 | kubectl logs --follow deployment/digester-controller-manager \ 38 | --namespace digester-system --all-containers=true 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/workload-identity.md: -------------------------------------------------------------------------------- 1 | # Workload Identity on Google Kubernetes Engine 2 | 3 | If you use 4 | [Google Kubernetes Engine (GKE)](https://cloud.google.com/kubernetes-engine/docs), 5 | you can authenticate to 6 | [Container Registry](https://cloud.google.com/container-registry/docs) and 7 | [Artifact Registry](https://cloud.google.com/artifact-registry/docs) using 8 | [Workload Identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity). 9 | 10 | The following steps assume that the 11 | [Google service account](https://cloud.google.com/iam/docs/service-accounts) 12 | is in the same 13 | [project](https://cloud.google.com/resource-manager/docs/creating-managing-projects) 14 | as the Container Registry and Artifact Registry image repositories. 15 | 16 | 1. Enable the GKE and Artifact Registry APIs: 17 | 18 | ```sh 19 | gcloud services enable \ 20 | container.googleapis.com \ 21 | artifactregistry.googleapis.com 22 | ``` 23 | 24 | Note that enabling the GKE API also enables the Container Registry API. 25 | 26 | 2. Create a GKE cluster with Workload Identity, and assign the 27 | [`cloud-platform` access scope](https://cloud.google.com/compute/docs/access/service-accounts#service_account_permissions) 28 | to the nodes: 29 | 30 | ```sh 31 | PROJECT_ID=$(gcloud config get core/project) 32 | ZONE=us-central1-f 33 | 34 | gcloud container clusters create digester-webhook-test \ 35 | --enable-ip-alias \ 36 | --release-channel regular \ 37 | --scopes cloud-platform \ 38 | --workload-pool $PROJECT_ID.svc.id.goog \ 39 | --zone $ZONE 40 | ``` 41 | 42 | 3. Create a Google service account: 43 | 44 | ```sh 45 | GSA_NAME=digester-webhook 46 | GSA=$GSA_NAME@$PROJECT_ID.iam.gserviceaccount.com 47 | 48 | gcloud iam service-accounts create $GSA_NAME \ 49 | --display-name "Digester webhook service account" 50 | ``` 51 | 52 | The digester webhook 53 | [Kubernetes service account](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) 54 | impersonates this Google service account to authenticate to Container 55 | Registry and Artifact Registry. 56 | 57 | 4. Grant the 58 | [Container Registry Service Agent role](https://cloud.google.com/iam/docs/understanding-roles#service-agents-roles) 59 | to the Google service account at the project level: 60 | 61 | ```sh 62 | gcloud projects add-iam-policy-binding $PROJECT_ID \ 63 | --member "serviceAccount:$GSA" \ 64 | --role roles/containerregistry.ServiceAgent 65 | ``` 66 | 67 | 5. Grant the 68 | [Artifact Registry Reader](https://cloud.google.com/iam/docs/understanding-roles#artifact-registry-roles) 69 | to the Google service account at the project level: 70 | 71 | ```sh 72 | gcloud projects add-iam-policy-binding $PROJECT_ID \ 73 | --member "serviceAccount:$GSA" \ 74 | --role roles/artifactregistry.reader 75 | ``` 76 | 77 | 6. Grant the 78 | [Workload Identity User role](https://cloud.google.com/iam/docs/understanding-roles#service-accounts-roles) 79 | to the `digester-admin` Kubernetes service account in the `digester-system` 80 | namespace on the Google service account: 81 | 82 | ```sh 83 | gcloud iam service-accounts add-iam-policy-binding "$GSA" \ 84 | --member "serviceAccount:$PROJECT_ID.svc.id.goog[digester-system/digester-admin]" \ 85 | --role roles/iam.workloadIdentityUser 86 | ``` 87 | 88 | 7. Add the Workload Identity annotation to the digester webhook Kubernetes 89 | service account: 90 | 91 | ```sh 92 | kubectl annotate serviceaccount digester-admin --namespace digester-system \ 93 | "iam.gke.io/gcp-service-account=$GSA" 94 | ``` 95 | 96 | This annotation informs GKE that the Kubernetes service account 97 | `digester-admin` in the namespace `digester-system` can impersonate the 98 | Google service account `$GSA`. 99 | 100 | Workload Identity works with both 101 | [online and offline authentication](authentication.md). 102 | 103 | If you use Workload Identity to authenticate to Container Registry or Artifact 104 | Registry, and if you do not rely on `imagePullSecrets` to authenticate to 105 | other container image registries, you can enable offline authentication on the 106 | digester webhook without providing a Docker config file, see 107 | [`authentication.md`](authentication.md). 108 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 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 | module github.com/google/k8s-digester 16 | 17 | go 1.23.0 18 | 19 | toolchain go1.23.4 20 | 21 | require ( 22 | github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20241209220728-69e8c24e6fc1 23 | github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 24 | github.com/go-logr/logr v1.4.2 25 | github.com/go-logr/stdr v1.2.2 26 | github.com/go-logr/zapr v1.3.0 27 | github.com/google/go-cmp v0.6.0 28 | github.com/google/go-containerregistry v0.20.2 29 | github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20241111191718-6bce25ecf029 30 | github.com/open-policy-agent/cert-controller v0.12.0 31 | github.com/spf13/cobra v1.8.1 32 | github.com/spf13/pflag v1.0.5 33 | github.com/spf13/viper v1.19.0 34 | go.uber.org/zap v1.27.0 35 | gomodules.xyz/jsonpatch/v2 v2.4.0 36 | k8s.io/api v0.32.0 37 | k8s.io/apimachinery v0.32.0 38 | k8s.io/client-go v0.32.0 39 | k8s.io/klog/v2 v2.130.1 40 | sigs.k8s.io/controller-runtime v0.19.3 41 | sigs.k8s.io/kustomize/kyaml v0.18.1 42 | ) 43 | 44 | require ( 45 | cloud.google.com/go/compute/metadata v0.5.2 // indirect 46 | github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect 47 | github.com/Azure/go-autorest v14.2.0+incompatible // indirect 48 | github.com/Azure/go-autorest/autorest v0.11.29 // indirect 49 | github.com/Azure/go-autorest/autorest/adal v0.9.24 // indirect 50 | github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 // indirect 51 | github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect 52 | github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect 53 | github.com/Azure/go-autorest/logger v0.2.1 // indirect 54 | github.com/Azure/go-autorest/tracing v0.6.0 // indirect 55 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect 56 | github.com/aws/aws-sdk-go-v2 v1.32.6 // indirect 57 | github.com/aws/aws-sdk-go-v2/config v1.28.6 // indirect 58 | github.com/aws/aws-sdk-go-v2/credentials v1.17.47 // indirect 59 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21 // indirect 60 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 // indirect 61 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 // indirect 62 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect 63 | github.com/aws/aws-sdk-go-v2/service/ecr v1.36.7 // indirect 64 | github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.27.7 // indirect 65 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect 66 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6 // indirect 67 | github.com/aws/aws-sdk-go-v2/service/sso v1.24.7 // indirect 68 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6 // indirect 69 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.2 // indirect 70 | github.com/aws/smithy-go v1.22.1 // indirect 71 | github.com/beorn7/perks v1.0.1 // indirect 72 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 73 | github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect 74 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 75 | github.com/dimchansky/utfbom v1.1.1 // indirect 76 | github.com/docker/cli v27.4.0+incompatible // indirect 77 | github.com/docker/distribution v2.8.3+incompatible // indirect 78 | github.com/docker/docker-credential-helpers v0.8.2 // indirect 79 | github.com/emicklei/go-restful/v3 v3.12.1 // indirect 80 | github.com/evanphx/json-patch v5.9.0+incompatible // indirect 81 | github.com/evanphx/json-patch/v5 v5.9.0 // indirect 82 | github.com/fsnotify/fsnotify v1.8.0 // indirect 83 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 84 | github.com/go-errors/errors v1.5.1 // indirect 85 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 86 | github.com/go-openapi/jsonreference v0.21.0 // indirect 87 | github.com/go-openapi/swag v0.23.0 // indirect 88 | github.com/gogo/protobuf v1.3.2 // indirect 89 | github.com/golang-jwt/jwt/v4 v4.5.1 // indirect 90 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 91 | github.com/golang/protobuf v1.5.4 // indirect 92 | github.com/google/gnostic-models v0.6.9 // indirect 93 | github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20241111191718-6bce25ecf029 // indirect 94 | github.com/google/gofuzz v1.2.0 // indirect 95 | github.com/google/uuid v1.6.0 // indirect 96 | github.com/hashicorp/hcl v1.0.0 // indirect 97 | github.com/imdario/mergo v0.3.16 // indirect 98 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 99 | github.com/jmespath/go-jmespath v0.4.0 // indirect 100 | github.com/josharian/intern v1.0.0 // indirect 101 | github.com/json-iterator/go v1.1.12 // indirect 102 | github.com/klauspost/compress v1.17.11 // indirect 103 | github.com/magiconair/properties v1.8.9 // indirect 104 | github.com/mailru/easyjson v0.7.7 // indirect 105 | github.com/mitchellh/go-homedir v1.1.0 // indirect 106 | github.com/mitchellh/mapstructure v1.5.0 // indirect 107 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 108 | github.com/modern-go/reflect2 v1.0.2 // indirect 109 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect 110 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 111 | github.com/opencontainers/go-digest v1.0.0 // indirect 112 | github.com/opencontainers/image-spec v1.1.0 // indirect 113 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 114 | github.com/pkg/errors v0.9.1 // indirect 115 | github.com/prometheus/client_golang v1.20.5 // indirect 116 | github.com/prometheus/client_model v0.6.1 // indirect 117 | github.com/prometheus/common v0.61.0 // indirect 118 | github.com/prometheus/procfs v0.15.1 // indirect 119 | github.com/sagikazarmark/locafero v0.6.0 // indirect 120 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 121 | github.com/sirupsen/logrus v1.9.3 // indirect 122 | github.com/sourcegraph/conc v0.3.0 // indirect 123 | github.com/spf13/afero v1.11.0 // indirect 124 | github.com/spf13/cast v1.7.0 // indirect 125 | github.com/subosito/gotenv v1.6.0 // indirect 126 | github.com/vbatts/tar-split v0.11.6 // indirect 127 | github.com/x448/float16 v0.8.4 // indirect 128 | github.com/xlab/treeprint v1.2.0 // indirect 129 | go.uber.org/atomic v1.11.0 // indirect 130 | go.uber.org/multierr v1.11.0 // indirect 131 | golang.org/x/crypto v0.31.0 // indirect 132 | golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect 133 | golang.org/x/net v0.32.0 // indirect 134 | golang.org/x/oauth2 v0.24.0 // indirect 135 | golang.org/x/sync v0.10.0 // indirect 136 | golang.org/x/sys v0.28.0 // indirect 137 | golang.org/x/term v0.27.0 // indirect 138 | golang.org/x/text v0.21.0 // indirect 139 | golang.org/x/time v0.8.0 // indirect 140 | google.golang.org/protobuf v1.35.2 // indirect 141 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 142 | gopkg.in/inf.v0 v0.9.1 // indirect 143 | gopkg.in/ini.v1 v1.67.0 // indirect 144 | gopkg.in/yaml.v2 v2.4.0 // indirect 145 | gopkg.in/yaml.v3 v3.0.1 // indirect 146 | k8s.io/apiextensions-apiserver v0.32.0 // indirect 147 | k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect 148 | k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect 149 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 150 | sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect 151 | sigs.k8s.io/yaml v1.4.0 // indirect 152 | ) 153 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 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 | "os" 19 | 20 | flag "github.com/spf13/pflag" 21 | "sigs.k8s.io/controller-runtime/pkg/manager/signals" 22 | 23 | "github.com/google/k8s-digester/cmd/function" 24 | "github.com/google/k8s-digester/cmd/version" 25 | "github.com/google/k8s-digester/cmd/webhook" 26 | 27 | // blank import for all k8s auth plugins 28 | _ "k8s.io/client-go/plugin/pkg/client/auth" 29 | ) 30 | 31 | func main() { 32 | if err := run(); err != nil { 33 | os.Exit(1) 34 | } 35 | } 36 | 37 | func run() error { 38 | hideFlagsFromImportedPackages() 39 | ctx := signals.SetupSignalHandler() 40 | cmd := function.Cmd(ctx) 41 | cmd.AddCommand( 42 | webhook.Cmd, 43 | version.Cmd, 44 | ) 45 | return cmd.ExecuteContext(ctx) 46 | } 47 | 48 | func hideFlagsFromImportedPackages() { 49 | flag.VisitAll(func(f *flag.Flag) { 50 | f.Hidden = true 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /manifests/Kptfile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 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 | apiVersion: kpt.dev/v1 16 | kind: Kptfile 17 | metadata: 18 | name: digester 19 | annotations: 20 | config.kubernetes.io/local-config: "true" 21 | info: 22 | description: >- 23 | The digester mutating admission webhook resolves tags to digests for 24 | container and init container images in Kubernetes Pod and Pod template 25 | specs. 26 | -------------------------------------------------------------------------------- /manifests/Kustomization: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 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 | apiVersion: kustomize.config.k8s.io/v1beta1 16 | kind: Kustomization 17 | metadata: 18 | name: digester 19 | annotations: 20 | config.kubernetes.io/local-config: "true" 21 | resources: 22 | - cluster-role-binding.yaml 23 | - cluster-role.yaml 24 | - deployment.yaml 25 | - mutating-webhook-configuration.yaml 26 | - namespace.yaml 27 | - role-binding.yaml 28 | - role.yaml 29 | - secret.yaml 30 | - service-account.yaml 31 | - service.yaml 32 | -------------------------------------------------------------------------------- /manifests/README.md: -------------------------------------------------------------------------------- 1 | # Digester webhook package 2 | 3 | Package for the [digester](https://github.com/google/k8s-digester) 4 | Kubernetes mutating admission webhook. 5 | 6 | The digester mutating admission webhook resolves tags to digests for container 7 | and init container images in Kubernetes CronJob, Pod and Pod template specs. 8 | 9 | ## Preparing for deployment 10 | 11 | The digester webhook requires Kubernetes v1.16 or later. 12 | 13 | If you use Google Kubernetes Engine (GKE), grant yourself the 14 | `cluster-admin` Kubernetes 15 | [cluster role](https://kubernetes.io/docs/reference/access-authn-authz/rbac/): 16 | 17 | ```sh 18 | kubectl create clusterrolebinding cluster-admin-binding \ 19 | --clusterrole cluster-admin \ 20 | --user "$(gcloud config get core/account)" 21 | ``` 22 | 23 | To configure how the webhook authenticates to your container image registries, 24 | see the documentation on 25 | [Authenticating to container image registries](https://github.com/google/k8s-digester/blob/main/docs/authentication.md#authenticating-to-container-image-registries). 26 | 27 | If you use a private GKE cluster, see additional steps for 28 | [creating a firewall rule](../README.md#private-clusters). 29 | 30 | ## Deploying the webhook using kustomize 31 | 32 | 1. Install [kustomize](https://github.com/kubernetes-sigs/kustomize). 33 | 34 | 2. Apply this package: 35 | 36 | ```sh 37 | VERSION=v0.1.16 38 | kustomize build "https://github.com/google/k8s-digester.git/manifests?ref=$VERSION" | kubectl apply -f - 39 | ``` 40 | 41 | 3. Add the `digest-resolution: enabled` label to namespaces where you want 42 | the webhook to resolve tags to digests: 43 | 44 | ```sh 45 | kubectl label namespace [NAMESPACE] digest-resolution=enabled 46 | ``` 47 | 48 | ## Deploying the webhook using kpt 49 | 50 | 1. Install [kpt](https://kpt.dev/installation/) v1.0.0-beta.1 or later. 51 | 52 | 3. Fetch this package: 53 | 54 | ```sh 55 | VERSION=v0.1.16 56 | kpt pkg get "https://github.com/google/k8s-digester.git/manifests@${VERSION}" manifests 57 | ``` 58 | 59 | 4. Setup inventory tracking for the package: 60 | 61 | ```sh 62 | kubectl create namespace digester-system 63 | kpt live init manifests 64 | ``` 65 | 66 | 5. Apply the package: 67 | 68 | ```sh 69 | kpt live apply manifests --reconcile-timeout=3m --output=table 70 | ``` 71 | 72 | 6. Add the `digest-resolution: enabled` label to namespaces where you want 73 | the webhook to resolve tags to digests: 74 | 75 | ```sh 76 | kubectl label namespace [NAMESPACE] digest-resolution=enabled 77 | ``` 78 | -------------------------------------------------------------------------------- /manifests/cluster-role-binding.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 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 | apiVersion: rbac.authorization.k8s.io/v1 16 | kind: ClusterRoleBinding 17 | metadata: 18 | name: digester-manager-rolebinding 19 | labels: 20 | digester/system: "yes" 21 | roleRef: 22 | name: digester-manager-role 23 | kind: ClusterRole 24 | apiGroup: rbac.authorization.k8s.io 25 | subjects: 26 | - name: digester-admin 27 | namespace: digester-system 28 | kind: ServiceAccount 29 | -------------------------------------------------------------------------------- /manifests/cluster-role.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 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 | apiVersion: rbac.authorization.k8s.io/v1 16 | kind: ClusterRole 17 | metadata: 18 | name: digester-manager-role 19 | labels: 20 | digester/system: "yes" 21 | rules: 22 | - resources: 23 | - secrets # access to imagePullSecrets 24 | - serviceaccounts # access to imagepullSecrets 25 | apiGroups: 26 | - '' 27 | verbs: 28 | - get 29 | - list 30 | - watch 31 | - resources: 32 | - customresourcedefinitions 33 | apiGroups: 34 | - apiextensions.k8s.io 35 | verbs: 36 | - get 37 | - list 38 | - watch 39 | - resources: 40 | - mutatingwebhookconfigurations 41 | apiGroups: 42 | - admissionregistration.k8s.io 43 | verbs: 44 | - get 45 | - list 46 | - watch 47 | - resources: 48 | - mutatingwebhookconfigurations 49 | apiGroups: 50 | - admissionregistration.k8s.io 51 | resourceNames: 52 | - digester-mutating-webhook-configuration 53 | verbs: 54 | - create 55 | - delete 56 | - get 57 | - list 58 | - patch 59 | - update 60 | - watch 61 | -------------------------------------------------------------------------------- /manifests/deployment.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 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 | apiVersion: apps/v1 16 | kind: Deployment 17 | metadata: 18 | name: digester-controller-manager 19 | namespace: digester-system 20 | labels: 21 | control-plane: controller-manager 22 | digester/operation: webhook 23 | digester/system: "yes" 24 | spec: 25 | replicas: 3 # kpt-set: ${replicas} 26 | selector: 27 | matchLabels: 28 | control-plane: controller-manager 29 | digester/operation: webhook 30 | digester/system: "yes" 31 | template: 32 | metadata: 33 | labels: 34 | control-plane: controller-manager 35 | digester/operation: webhook 36 | digester/system: "yes" 37 | annotations: 38 | prometheus.io/port: "8888" # kpt-set: ${metrics-port} 39 | spec: 40 | serviceAccountName: digester-admin 41 | nodeSelector: 42 | kubernetes.io/os: linux 43 | containers: 44 | - name: manager 45 | image: k8s-digester # kpt-set: ${image} 46 | args: 47 | - webhook 48 | - --cert-dir=/certs # kpt-set: --cert-dir=${cert-dir} 49 | - --disable-cert-rotation=false # kpt-set: --disable-cert-rotation=${disable-cert-rotation} 50 | - --dry-run=false # kpt-set: --dry-run=${dry-run} 51 | - --health-addr=:9090 # kpt-set: --health-addr=:${health-port} 52 | - --metrics-addr=:8888 # kpt-set: --metrics-addr=:${metrics-port} 53 | - --offline=false # kpt-set: --offline=${offline} 54 | - --port=8443 # kpt-set: --port=${port} 55 | ports: 56 | - name: webhook-server 57 | protocol: TCP 58 | containerPort: 8443 # kpt-set: ${port} 59 | - name: metrics 60 | protocol: TCP 61 | containerPort: 8888 # kpt-set: ${metrics-port} 62 | - name: healthz 63 | protocol: TCP 64 | containerPort: 9090 # kpt-set: ${health-port} 65 | env: 66 | - name: DEBUG 67 | value: "false" # kpt-set: ${debug} 68 | - name: POD_NAME 69 | valueFrom: 70 | fieldRef: 71 | fieldPath: metadata.name 72 | - name: POD_NAMESPACE 73 | valueFrom: 74 | fieldRef: 75 | apiVersion: v1 76 | fieldPath: metadata.namespace 77 | resources: 78 | requests: 79 | cpu: 100m # kpt-set: ${request-cpu} 80 | ephemeral-storage: 256Mi # kpt-set: ${request-ephemeral-storage} 81 | memory: 256Mi # kpt-set: ${request-memory} 82 | volumeMounts: 83 | - name: cert 84 | readOnly: true 85 | mountPath: /certs # kpt-set: ${cert-dir} 86 | livenessProbe: 87 | httpGet: 88 | port: healthz 89 | path: /healthz 90 | readinessProbe: 91 | httpGet: 92 | port: healthz 93 | path: /readyz 94 | securityContext: 95 | allowPrivilegeEscalation: false 96 | capabilities: 97 | drop: 98 | - all 99 | readOnlyRootFilesystem: true 100 | runAsGroup: 65532 101 | runAsNonRoot: true 102 | runAsUser: 65532 103 | volumes: 104 | - name: cert 105 | secret: 106 | defaultMode: 420 107 | secretName: digester-webhook-server-cert 108 | -------------------------------------------------------------------------------- /manifests/mutating-webhook-configuration.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 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 | apiVersion: admissionregistration.k8s.io/v1 16 | kind: MutatingWebhookConfiguration 17 | metadata: 18 | name: digester-mutating-webhook-configuration 19 | labels: 20 | control-plane: controller-manager 21 | digester/operation: webhook 22 | digester/system: "yes" 23 | webhooks: 24 | - name: digester-webhook-service.digester-system.svc 25 | admissionReviewVersions: 26 | - v1 27 | - v1beta1 28 | clientConfig: 29 | service: 30 | name: digester-webhook-service 31 | namespace: digester-system 32 | path: /v1/mutate 33 | caBundle: Cg== 34 | failurePolicy: Ignore # kpt-set: ${failure-policy} 35 | namespaceSelector: 36 | matchLabels: 37 | digest-resolution: enabled 38 | reinvocationPolicy: IfNeeded 39 | rules: 40 | - resources: 41 | - pods 42 | - podtemplates 43 | - replicationcontrollers 44 | apiGroups: 45 | - '' 46 | apiVersions: 47 | - v1 48 | operations: 49 | - CREATE 50 | - UPDATE 51 | scope: Namespaced 52 | - resources: 53 | - daemonsets 54 | - deployments 55 | - replicasets 56 | - statefulsets 57 | apiGroups: 58 | - apps 59 | apiVersions: 60 | - v1 61 | operations: 62 | - CREATE 63 | - UPDATE 64 | scope: Namespaced 65 | - resources: 66 | - cronjobs 67 | - jobs 68 | apiGroups: 69 | - batch 70 | apiVersions: 71 | - v1 72 | - v1beta1 73 | operations: 74 | - CREATE 75 | - UPDATE 76 | scope: Namespaced 77 | - resources: 78 | - containersources 79 | apiGroups: 80 | - sources.knative.dev 81 | apiVersions: 82 | - v1 83 | operations: 84 | - CREATE 85 | - UPDATE 86 | scope: Namespaced 87 | sideEffects: None 88 | timeoutSeconds: 15 89 | -------------------------------------------------------------------------------- /manifests/namespace.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 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 | apiVersion: v1 16 | kind: Namespace 17 | metadata: 18 | name: digester-system 19 | labels: 20 | control-plane: controller-manager 21 | digester-injection: disabled 22 | digester/system: "yes" 23 | istio-injection: disabled 24 | -------------------------------------------------------------------------------- /manifests/role-binding.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 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 | apiVersion: rbac.authorization.k8s.io/v1 16 | kind: RoleBinding 17 | metadata: 18 | name: digester-manager-rolebinding 19 | namespace: digester-system 20 | labels: 21 | digester/system: "yes" 22 | roleRef: 23 | name: digester-manager-role 24 | kind: Role 25 | apiGroup: rbac.authorization.k8s.io 26 | subjects: 27 | - name: digester-admin 28 | namespace: digester-system 29 | kind: ServiceAccount 30 | -------------------------------------------------------------------------------- /manifests/role.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 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 | apiVersion: rbac.authorization.k8s.io/v1 16 | kind: Role 17 | metadata: 18 | name: digester-manager-role 19 | namespace: digester-system 20 | labels: 21 | digester/system: "yes" 22 | rules: 23 | - resources: 24 | - secrets 25 | apiGroups: 26 | - '' 27 | verbs: 28 | - create 29 | - delete 30 | - get 31 | - list 32 | - patch 33 | - update 34 | - watch 35 | -------------------------------------------------------------------------------- /manifests/secret.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 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 | apiVersion: v1 16 | kind: Secret 17 | metadata: 18 | name: digester-webhook-server-cert 19 | namespace: digester-system 20 | labels: 21 | control-plane: controller-manager 22 | digester/system: "yes" 23 | -------------------------------------------------------------------------------- /manifests/service-account.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 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 | apiVersion: v1 16 | kind: ServiceAccount 17 | metadata: 18 | name: digester-admin 19 | namespace: digester-system 20 | labels: 21 | control-plane: controller-manager 22 | digester/system: "yes" 23 | -------------------------------------------------------------------------------- /manifests/service.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 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 | apiVersion: v1 16 | kind: Service 17 | metadata: 18 | name: digester-webhook-service 19 | namespace: digester-system 20 | labels: 21 | control-plane: controller-manager 22 | digester/operation: webhook 23 | digester/system: "yes" 24 | spec: 25 | selector: 26 | control-plane: controller-manager 27 | digester/operation: webhook 28 | digester/system: "yes" 29 | ports: 30 | - port: 443 31 | targetPort: 8443 # kpt-set: ${port} 32 | -------------------------------------------------------------------------------- /pkg/handler/handler.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 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 handler provides the admission webhook handler 16 | package handler 17 | 18 | import ( 19 | "context" 20 | "net/http" 21 | 22 | "github.com/go-logr/logr" 23 | "gomodules.xyz/jsonpatch/v2" 24 | admissionv1 "k8s.io/api/admission/v1" 25 | "k8s.io/client-go/rest" 26 | "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 27 | "sigs.k8s.io/kustomize/kyaml/yaml" 28 | 29 | "github.com/google/k8s-digester/pkg/resolve" 30 | "github.com/google/k8s-digester/pkg/util" 31 | ) 32 | 33 | const ( 34 | reasonNoMutationForOperation = "NoMutationForOperation" 35 | reasonNoSelfManagement = "NoSelfManagement" 36 | reasonErrorIgnored = "ErrorIgnored" 37 | reasonNotPatched = "NotPatched" 38 | reasonPatched = "Patched" 39 | ) 40 | 41 | // Handler implements admission.Handler 42 | type Handler struct { 43 | Log logr.Logger 44 | DryRun bool 45 | IgnoreErrors bool 46 | Config *rest.Config 47 | SkipPrefixes []string 48 | } 49 | 50 | var resolveImageTags = resolve.ImageTags // override for testing 51 | 52 | // Handle processes the AdmissionRequest by invoking the underlying function. 53 | func (h *Handler) Handle(ctx context.Context, req admission.Request) admission.Response { 54 | h.Log.Info("received request", "name", req.Name, "namespace", req.Namespace, "gvk", req.Kind) 55 | if req.Operation != admissionv1.Create && req.Operation != admissionv1.Update { 56 | return admission.Allowed(reasonNoMutationForOperation) 57 | } 58 | r, err := yaml.Parse(string(req.Object.Raw)) 59 | if err != nil { 60 | return h.admissionError(err) 61 | } 62 | h.Log.V(1).Info("parsed resource", "resource", r) 63 | if req.Namespace == util.GetNamespace() { 64 | return admission.Allowed(reasonNoSelfManagement) 65 | } 66 | // The raw resource may not contain the namespace, but the admission review 67 | // request will. That's why the next step copies the namespace from the 68 | // request to the resource. This handles situations such as when the 69 | // ReplicaSetController creates a new pod. 70 | if err := r.SetNamespace(req.Namespace); err != nil { 71 | h.admissionError(err) 72 | } 73 | before, err := r.MarshalJSON() 74 | if err != nil { 75 | return h.admissionError(err) 76 | } 77 | 78 | if err = resolveImageTags(ctx, h.Log, h.Config, r, h.SkipPrefixes); err != nil { 79 | return h.admissionError(err) 80 | } 81 | 82 | after, err := r.MarshalJSON() 83 | if err != nil { 84 | return h.admissionError(err) 85 | } 86 | patches, err := jsonpatch.CreatePatch(before, after) 87 | if err != nil { 88 | return h.admissionError(err) 89 | } 90 | h.Log.V(1).Info("patched resource", "patches", patches) 91 | if h.DryRun { 92 | h.Log.Info("not mutating resource, because dry-run=true") 93 | patches = []jsonpatch.JsonPatchOperation{} 94 | } 95 | reason := reasonPatched 96 | if len(patches) == 0 { 97 | reason = reasonNotPatched 98 | } 99 | return admission.Patched(reason, patches...) 100 | } 101 | 102 | func (h *Handler) admissionError(err error) admission.Response { 103 | if h.IgnoreErrors { 104 | h.Log.Error(err, "ignored admission error") 105 | return admission.Allowed(reasonErrorIgnored) 106 | } 107 | h.Log.Error(err, "admission error") 108 | return admission.Errored(int32(http.StatusInternalServerError), err) 109 | } 110 | -------------------------------------------------------------------------------- /pkg/handler/handler_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 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 handler 16 | 17 | import ( 18 | "context" 19 | "net/http" 20 | "testing" 21 | 22 | "github.com/go-logr/logr" 23 | "github.com/google/go-cmp/cmp" 24 | "gomodules.xyz/jsonpatch/v2" 25 | admissionv1 "k8s.io/api/admission/v1" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | "k8s.io/client-go/rest" 28 | "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 29 | "sigs.k8s.io/kustomize/kyaml/yaml" 30 | 31 | "github.com/google/k8s-digester/pkg/logging" 32 | ) 33 | 34 | var ( 35 | ctx = context.Background() 36 | nullLog = logging.CreateDiscardLogger() 37 | log = logging.CreateStdLogger("handler_test").V(2) 38 | ) 39 | 40 | func Test_Handle_NoPatchesForDelete(t *testing.T) { 41 | req := admission.Request{ 42 | AdmissionRequest: admissionv1.AdmissionRequest{ 43 | Namespace: "default", 44 | Object: runtime.RawExtension{ 45 | Raw: []byte(`{}`), 46 | }, 47 | Operation: admissionv1.Delete, 48 | }, 49 | } 50 | h := &Handler{Log: log} 51 | 52 | resp := h.Handle(ctx, req) 53 | 54 | assertAdmissionAllowed(t, resp) 55 | assertMessage(t, resp, reasonNoMutationForOperation) 56 | assertNoPatches(t, resp) 57 | } 58 | 59 | func Test_Handle_DisallowedOnParseError(t *testing.T) { 60 | req := admission.Request{ 61 | AdmissionRequest: admissionv1.AdmissionRequest{ 62 | Object: runtime.RawExtension{ 63 | Raw: []byte("\U0001f4a9"), 64 | }, 65 | Operation: admissionv1.Create, 66 | }, 67 | } 68 | h := &Handler{Log: nullLog} // suppress output of expected error 69 | 70 | resp := h.Handle(ctx, req) 71 | 72 | assertAdmissionError(t, resp) 73 | } 74 | 75 | func Test_Handle_IgnoreError(t *testing.T) { 76 | req := admission.Request{ 77 | AdmissionRequest: admissionv1.AdmissionRequest{ 78 | Object: runtime.RawExtension{ 79 | Raw: []byte("\U0001f4a9"), 80 | }, 81 | Operation: admissionv1.Create, 82 | }, 83 | } 84 | h := &Handler{ 85 | Log: nullLog, // suppress output of expected error 86 | IgnoreErrors: true, 87 | } 88 | 89 | resp := h.Handle(ctx, req) 90 | 91 | assertAdmissionAllowed(t, resp) 92 | assertMessage(t, resp, reasonErrorIgnored) 93 | assertNoPatches(t, resp) 94 | } 95 | 96 | func Test_Handle_NoMutationOfDigesterNamespaceRequests(t *testing.T) { 97 | req := admission.Request{ 98 | AdmissionRequest: admissionv1.AdmissionRequest{ 99 | Namespace: "digester-system", 100 | Object: runtime.RawExtension{ 101 | Raw: []byte(`{}`), 102 | }, 103 | Operation: admissionv1.Create, 104 | }, 105 | } 106 | h := &Handler{Log: log} 107 | 108 | resp := h.Handle(ctx, req) 109 | 110 | assertAdmissionAllowed(t, resp) 111 | assertMessage(t, resp, reasonNoSelfManagement) 112 | assertNoPatches(t, resp) 113 | } 114 | 115 | func Test_Handle_NotPatchedWhenNoChange(t *testing.T) { 116 | req := admission.Request{ 117 | AdmissionRequest: admissionv1.AdmissionRequest{ 118 | Namespace: "test", 119 | Operation: admissionv1.Create, 120 | Object: runtime.RawExtension{ 121 | Raw: []byte(`{}`), 122 | }, 123 | }, 124 | } 125 | resolveImageTags = func(_ context.Context, _ logr.Logger, _ *rest.Config, _ *yaml.RNode, _ []string) error { 126 | return nil 127 | } 128 | h := &Handler{Log: log} 129 | 130 | resp := h.Handle(ctx, req) 131 | 132 | assertAdmissionAllowed(t, resp) 133 | assertMessage(t, resp, reasonNotPatched) 134 | assertNoPatches(t, resp) 135 | } 136 | 137 | func Test_Handle_Patch(t *testing.T) { 138 | req := admission.Request{ 139 | AdmissionRequest: admissionv1.AdmissionRequest{ 140 | Namespace: "test", 141 | Operation: admissionv1.Create, 142 | Object: runtime.RawExtension{ 143 | Raw: []byte(`{"spec": {"containers": [{"image": "registry.example.com/repository/image:tag"}]}}`), 144 | }, 145 | }, 146 | } 147 | imageWithDigest := "registry.example.com/repository/image:tag@sha256:digest" 148 | resolveImageTags = func(_ context.Context, _ logr.Logger, _ *rest.Config, n *yaml.RNode, _ []string) error { 149 | return n.PipeE(yaml.Lookup("spec", "containers", "0", "image"), yaml.FieldSetter{StringValue: imageWithDigest}) 150 | } 151 | h := &Handler{Log: log} 152 | 153 | resp := h.Handle(ctx, req) 154 | 155 | assertAdmissionAllowed(t, resp) 156 | assertMessage(t, resp, reasonPatched) 157 | if len(resp.Patches) < 1 { 158 | t.Errorf("expected len(resp.Patches) == 1, got %d", len(resp.Patches)) 159 | } 160 | if diff := cmp.Diff(resp.Patches, []jsonpatch.Operation{ 161 | jsonpatch.NewOperation("replace", "/spec/containers/0/image", imageWithDigest), 162 | }); diff != "" { 163 | t.Errorf("patch mismatch (-want +got):\n%s", diff) 164 | } 165 | } 166 | 167 | func assertAdmissionAllowed(t *testing.T, resp admission.Response) { 168 | if !resp.Allowed { 169 | t.Errorf("wanted allowed, got disallowed") 170 | } 171 | if resp.Result.Code != http.StatusOK { 172 | t.Logf("result message: %s", resp.Result.Message) 173 | t.Errorf("wanted code %d, got %d", http.StatusOK, resp.Result.Code) 174 | } 175 | } 176 | 177 | func assertAdmissionError(t *testing.T, resp admission.Response) { 178 | if resp.Allowed { 179 | t.Errorf("wanted disallowed, got allowed") 180 | } 181 | if resp.Result.Code != http.StatusInternalServerError { 182 | t.Errorf("wanted code %d, got %d", http.StatusInternalServerError, resp.Result.Code) 183 | } 184 | if resp.Result.Message == "" { 185 | t.Errorf("wanted result message, got empty string") 186 | } 187 | } 188 | 189 | func assertNoPatches(t *testing.T, resp admission.Response) { 190 | if len(resp.Patch) > 0 { 191 | t.Errorf("wanted empty Patch field, got %s", resp.Patch) 192 | } 193 | if len(resp.Patches) > 0 { 194 | t.Errorf("wanted empty Patches field, got %+v", resp.Patches) 195 | } 196 | } 197 | 198 | func assertMessage(t *testing.T, resp admission.Response, wantMessage string) { 199 | if resp.Result.Message != wantMessage { 200 | t.Errorf("wanted reason %s, got %s", wantMessage, resp.Result.Message) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /pkg/keychain/keychain.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 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 keychain creates credentials for authenticating to container image registries. 16 | package keychain 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "io/ioutil" 22 | 23 | "github.com/awslabs/amazon-ecr-credential-helper/ecr-login" 24 | "github.com/chrismellard/docker-credential-acr-env/pkg/credhelper" 25 | "github.com/go-logr/logr" 26 | "github.com/google/go-containerregistry/pkg/authn" 27 | "github.com/google/go-containerregistry/pkg/authn/github" 28 | "github.com/google/go-containerregistry/pkg/authn/k8schain" 29 | "github.com/google/go-containerregistry/pkg/v1/google" 30 | "k8s.io/client-go/kubernetes" 31 | "k8s.io/client-go/rest" 32 | "sigs.k8s.io/kustomize/kyaml/yaml" 33 | ) 34 | 35 | var ( 36 | amazonKeychain = authn.NewKeychainFromHelper(ecr.NewECRHelper(ecr.WithLogger(ioutil.Discard))) 37 | azureKeychain = authn.NewKeychainFromHelper(credhelper.NewACRCredentialsHelper()) 38 | 39 | createClientFn = createClient // override for testing 40 | ) 41 | 42 | // Create a multi keychain based in input arguments 43 | func Create(ctx context.Context, log logr.Logger, config *rest.Config, n *yaml.RNode) (authn.Keychain, error) { 44 | if config == nil { 45 | log.V(1).Info("creating offline keychain") 46 | return authn.NewMultiKeychain( 47 | google.Keychain, 48 | authn.DefaultKeychain, 49 | github.Keychain, 50 | amazonKeychain, 51 | azureKeychain, 52 | ), nil 53 | } 54 | client, err := createClientFn(config) 55 | if err != nil { 56 | return nil, fmt.Errorf("could not create Kubernetes Clientset: %w", err) 57 | } 58 | kkc, err := createK8schain(ctx, log, client, n) 59 | if err != nil { 60 | return nil, fmt.Errorf("could not create k8schain: %w", err) 61 | } 62 | log.V(1).Info("creating k8s keychain") 63 | return authn.NewMultiKeychain(kkc, authn.DefaultKeychain), nil 64 | } 65 | 66 | func createClient(config *rest.Config) (kubernetes.Interface, error) { 67 | return kubernetes.NewForConfig(config) 68 | } 69 | 70 | func createK8schain(ctx context.Context, log logr.Logger, client kubernetes.Interface, n *yaml.RNode) (authn.Keychain, error) { 71 | var namespace string 72 | namespaceNode, err := n.Pipe(yaml.Lookup("metadata", "namespace")) 73 | if err == nil { 74 | namespace = yaml.GetValue(namespaceNode) 75 | } 76 | var serviceAccountName string 77 | podServiceAccountNameNode, err := n.Pipe(yaml.Lookup("spec", "serviceAccountName")) 78 | if err == nil { 79 | serviceAccountName = yaml.GetValue(podServiceAccountNameNode) 80 | } 81 | if serviceAccountName == "" { 82 | podTemplateServiceAccountNameNode, err := n.Pipe(yaml.Lookup("spec", "template", "spec", "serviceAccountName")) 83 | if err == nil { 84 | serviceAccountName = yaml.GetValue(podTemplateServiceAccountNameNode) 85 | } 86 | if serviceAccountName == "" { 87 | cronJobServiceAccountNameNode, err := n.Pipe(yaml.Lookup("spec", "jobTemplate", "spec", "template", "spec", "serviceAccountName")) 88 | if err == nil { 89 | serviceAccountName = yaml.GetValue(cronJobServiceAccountNameNode) 90 | } 91 | } 92 | } 93 | var imagePullSecrets []string 94 | podImagePullSecretsNode, err := n.Pipe(yaml.Lookup("spec", "imagePullSecrets")) 95 | if err == nil { 96 | imagePullSecrets, _ = podImagePullSecretsNode.ElementValues("name") 97 | } 98 | if len(imagePullSecrets) == 0 { 99 | podTemplateImagePullSecretsNode, err := n.Pipe(yaml.Lookup("spec", "template", "spec", "imagePullSecrets")) 100 | if err == nil { 101 | imagePullSecrets, _ = podTemplateImagePullSecretsNode.ElementValues("name") 102 | } 103 | if len(imagePullSecrets) == 0 { 104 | cronJobImagePullSecretsNode, err := n.Pipe(yaml.Lookup("spec", "jobTemplate", "spec", "template", "spec", "imagePullSecrets")) 105 | if err == nil { 106 | imagePullSecrets, _ = cronJobImagePullSecretsNode.ElementValues("name") 107 | } 108 | } 109 | } 110 | log.V(1).Info("creating k8schain", 111 | "namespace", namespace, 112 | "serviceAccountName", serviceAccountName, 113 | "imagePullSecrets", imagePullSecrets) 114 | return k8schain.New(ctx, client, k8schain.Options{ 115 | Namespace: namespace, // defaults to "default" if empty 116 | ServiceAccountName: serviceAccountName, // defaults to "default" if empty 117 | ImagePullSecrets: imagePullSecrets, 118 | }) 119 | } 120 | -------------------------------------------------------------------------------- /pkg/keychain/keychain_stub_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 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 keychain 16 | 17 | import ( 18 | "context" 19 | "encoding/base64" 20 | "encoding/json" 21 | "fmt" 22 | 23 | corev1 "k8s.io/api/core/v1" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/client-go/kubernetes" 26 | "sigs.k8s.io/kustomize/kyaml/yaml" 27 | ) 28 | 29 | func createFakeClient() (kubernetes.Interface, error) { 30 | return createClientFn(nil) 31 | } 32 | 33 | func createDockerConfigSecret(ctx context.Context, client kubernetes.Interface, namespace, secretName, registry, username, password string) error { 34 | secretData := map[string]interface{}{ 35 | "auths": map[string]interface{}{ 36 | registry: map[string]string{ 37 | "auth": base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password))), 38 | }, 39 | }, 40 | } 41 | secretBytes, err := json.Marshal(secretData) 42 | if err != nil { 43 | return err 44 | } 45 | secret := &corev1.Secret{ 46 | ObjectMeta: metav1.ObjectMeta{ 47 | Name: secretName, 48 | }, 49 | Type: corev1.SecretTypeDockerConfigJson, 50 | Data: map[string][]byte{ 51 | corev1.DockerConfigJsonKey: secretBytes, 52 | }, 53 | } 54 | _, err = client.CoreV1().Secrets(namespace).Create(ctx, secret, metav1.CreateOptions{}) 55 | if err != nil { 56 | return err 57 | } 58 | return nil 59 | } 60 | 61 | func createServiceAccount(ctx context.Context, client kubernetes.Interface, namespace, serviceAccountName string, imagePullSecrets ...string) error { 62 | serviceAccount := &corev1.ServiceAccount{ 63 | ObjectMeta: metav1.ObjectMeta{ 64 | Name: serviceAccountName, 65 | }, 66 | } 67 | for _, secret := range imagePullSecrets { 68 | serviceAccount.ImagePullSecrets = append(serviceAccount.ImagePullSecrets, 69 | corev1.LocalObjectReference{Name: secret}) 70 | } 71 | _, err := client.CoreV1().ServiceAccounts(namespace).Create(ctx, serviceAccount, metav1.CreateOptions{}) 72 | if err != nil { 73 | return err 74 | } 75 | return nil 76 | } 77 | 78 | type M map[string]interface{} 79 | 80 | func createPodNode(namespace, serviceAccountName, imageTag string, imagePullSecrets ...string) (*yaml.RNode, error) { 81 | node, err := yaml.FromMap(M{ 82 | "apiVersion": "v1", 83 | "kind": "Pod", 84 | "metadata": M{ 85 | "name": "test-pod", 86 | }, 87 | "spec": M{ 88 | "containers": []M{ 89 | { 90 | "name": "test-image", 91 | "image": "example.com/repository/image", 92 | }, 93 | }, 94 | }, 95 | }) 96 | if err != nil { 97 | return nil, err 98 | } 99 | if err := node.PipeE( 100 | yaml.LookupCreate(yaml.ScalarNode, "metadata", "namespace"), 101 | yaml.FieldSetter{StringValue: namespace}, 102 | ); err != nil { 103 | return nil, err 104 | } 105 | if err := node.PipeE( 106 | yaml.LookupCreate(yaml.ScalarNode, "spec", "serviceAccountName"), 107 | yaml.FieldSetter{StringValue: serviceAccountName}, 108 | ); err != nil { 109 | return nil, err 110 | } 111 | if err := node.PipeE( 112 | yaml.Lookup("spec", "containers", "[name=test-image]"), 113 | yaml.FieldSetter{ 114 | Name: "image", 115 | StringValue: imageTag, 116 | }, 117 | ); err != nil { 118 | return nil, err 119 | } 120 | for _, secret := range imagePullSecrets { 121 | if err := node.PipeE( 122 | yaml.LookupCreate(yaml.SequenceNode, "spec", "imagePullSecrets"), 123 | yaml.Append(yaml.NewMapRNode(&map[string]string{"name": secret}).YNode()), 124 | ); err != nil { 125 | return nil, err 126 | } 127 | } 128 | return node, nil 129 | } 130 | 131 | func createDeploymentNode(namespace, serviceAccountName, imageTag string, imagePullSecrets ...string) (*yaml.RNode, error) { 132 | node, err := yaml.FromMap(M{ 133 | "apiVersion": "apps/v1", 134 | "kind": "Deployment", 135 | "metadata": M{ 136 | "name": "test-deployment", 137 | }, 138 | "spec": M{ 139 | "replicas": 1, 140 | "selector": M{ 141 | "matchLabels": M{ 142 | "app": "test", 143 | }, 144 | }, 145 | "template": M{ 146 | "metadata": M{ 147 | "labels": M{ 148 | "app": "test", 149 | }, 150 | }, 151 | "spec": M{ 152 | "containers": []M{ 153 | { 154 | "name": "test-image", 155 | "image": "example.com/repository/image", 156 | }, 157 | }, 158 | }, 159 | }, 160 | }, 161 | }) 162 | if err != nil { 163 | return nil, err 164 | } 165 | if err := node.PipeE( 166 | yaml.LookupCreate(yaml.ScalarNode, "metadata", "namespace"), 167 | yaml.FieldSetter{StringValue: namespace}, 168 | ); err != nil { 169 | return nil, err 170 | } 171 | if err := node.PipeE( 172 | yaml.LookupCreate(yaml.ScalarNode, "spec", "template", "spec", "serviceAccountName"), 173 | yaml.FieldSetter{StringValue: serviceAccountName}, 174 | ); err != nil { 175 | return nil, err 176 | } 177 | if err := node.PipeE( 178 | yaml.Lookup("spec", "template", "spec", "containers", "[name=test-image]"), 179 | yaml.FieldSetter{ 180 | Name: "image", 181 | StringValue: imageTag, 182 | }, 183 | ); err != nil { 184 | return nil, err 185 | } 186 | for _, secret := range imagePullSecrets { 187 | if err := node.PipeE( 188 | yaml.LookupCreate(yaml.SequenceNode, "spec", "template", "spec", "imagePullSecrets"), 189 | yaml.Append(yaml.NewMapRNode(&map[string]string{"name": secret}).YNode()), 190 | ); err != nil { 191 | return nil, err 192 | } 193 | } 194 | return node, nil 195 | } 196 | -------------------------------------------------------------------------------- /pkg/keychain/keychain_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 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 keychain 16 | 17 | import ( 18 | "context" 19 | "testing" 20 | 21 | "github.com/google/go-cmp/cmp" 22 | "github.com/google/go-containerregistry/pkg/authn" 23 | "github.com/google/go-containerregistry/pkg/name" 24 | "k8s.io/client-go/kubernetes" 25 | "k8s.io/client-go/kubernetes/fake" 26 | "k8s.io/client-go/rest" 27 | 28 | "github.com/google/k8s-digester/pkg/logging" 29 | ) 30 | 31 | var ( 32 | ctx = context.Background() 33 | // log = logging.CreateDiscardLogger() 34 | log = logging.CreateStdLogger("keychain_test") 35 | 36 | authConfigComparer = cmp.Comparer(func(l, r *authn.AuthConfig) bool { 37 | return l.Username == r.Username && l.Password == r.Password 38 | }) 39 | ) 40 | 41 | func init() { 42 | createClientFn = func(_ *rest.Config) (kubernetes.Interface, error) { 43 | return fake.NewSimpleClientset(), nil 44 | } 45 | } 46 | 47 | func Test_Create_Keychain_for_nil_config(t *testing.T) { 48 | kc, err := Create(ctx, log, nil, nil) 49 | if err != nil { 50 | t.Fatalf("error creating keychain: %v", err) 51 | } 52 | if kc == nil { 53 | t.Errorf("expected keychain, got nil") 54 | } 55 | } 56 | 57 | func Test_createK8schain_ImagePullSecretOnPod(t *testing.T) { 58 | namespace := "test-ns" 59 | serviceAccountName := "test-sa" 60 | secretName := "test-secret" 61 | registry := "registry.example.com" 62 | username := "username" 63 | password := "password" 64 | imageTag := "registry.example.com/repository/image:tag" 65 | client, err := createFakeClient() 66 | if err != nil { 67 | t.Fatalf("error creating fake Kubernetes client: %v", err) 68 | } 69 | if err := createDockerConfigSecret(ctx, client, namespace, "wrong-secret", "wrong.example.com", "foo", "bar"); err != nil { 70 | t.Fatalf("error creating secret: %v", err) 71 | } 72 | if err := createDockerConfigSecret(ctx, client, namespace, secretName, registry, username, password); err != nil { 73 | t.Fatalf("error creating secret: %v", err) 74 | } 75 | if err := createServiceAccount(ctx, client, namespace, serviceAccountName); err != nil { 76 | t.Fatalf("error creating service account: %v", err) 77 | } 78 | node, err := createPodNode(namespace, serviceAccountName, imageTag, "wrong-secret", secretName) 79 | if err != nil { 80 | t.Fatalf("error creating pod node: %v", err) 81 | } 82 | t.Logf("%s", node.MustString()) 83 | 84 | kc, err := createK8schain(ctx, log, client, node) 85 | if err != nil { 86 | t.Fatalf("error creating k8schain: %v", err) 87 | } 88 | 89 | authConfig, err := getAuthConfig(kc, imageTag) 90 | if err != nil { 91 | t.Fatalf("error getting authconfig: %v", err) 92 | } 93 | want := &authn.AuthConfig{ 94 | Username: username, 95 | Password: password, 96 | } 97 | if diff := cmp.Diff(want, authConfig, authConfigComparer); diff != "" { 98 | t.Errorf("createK8schain() authConfig mismatch (-want +got):\n%s", diff) 99 | } 100 | } 101 | 102 | func Test_createK8schain_ImagePullSecretOnPodTemplateSpec(t *testing.T) { 103 | namespace := "test-ns" 104 | serviceAccountName := "test-sa" 105 | secretName := "test-secret" 106 | registry := "registry.example.com" 107 | username := "username" 108 | password := "password" 109 | imageTag := "registry.example.com/repository/image:tag" 110 | client, err := createFakeClient() 111 | if err != nil { 112 | t.Fatalf("error creating fake Kubernetes client: %v", err) 113 | } 114 | if err := createDockerConfigSecret(ctx, client, namespace, secretName, registry, username, password); err != nil { 115 | t.Fatalf("error creating secret: %v", err) 116 | } 117 | if err := createServiceAccount(ctx, client, namespace, serviceAccountName); err != nil { 118 | t.Fatalf("error creating service account: %v", err) 119 | } 120 | node, err := createDeploymentNode(namespace, serviceAccountName, imageTag, secretName) 121 | if err != nil { 122 | t.Fatalf("error creating deployment node: %v", err) 123 | } 124 | t.Logf("%s", node.MustString()) 125 | 126 | kc, err := createK8schain(ctx, log, client, node) 127 | if err != nil { 128 | t.Fatalf("error creating k8schain: %v", err) 129 | } 130 | 131 | authConfig, err := getAuthConfig(kc, imageTag) 132 | if err != nil { 133 | t.Fatalf("error getting authconfig: %v", err) 134 | } 135 | want := &authn.AuthConfig{ 136 | Username: username, 137 | Password: password, 138 | } 139 | if diff := cmp.Diff(want, authConfig, authConfigComparer); diff != "" { 140 | t.Errorf("createK8schain() authConfig mismatch (-want +got):\n%s", diff) 141 | } 142 | } 143 | 144 | func Test_createK8schain_ImagePullSecretOnDefaultServiceAccount(t *testing.T) { 145 | namespace := "default" 146 | serviceAccountName := "default" 147 | secretName := "test-secret" 148 | registry := "registry.example.com" 149 | username := "username" 150 | password := "password" 151 | imageTag := "registry.example.com/repository/image:tag" 152 | client, err := createFakeClient() 153 | if err != nil { 154 | t.Fatalf("error creating fake Kubernetes client: %v", err) 155 | } 156 | if err := createDockerConfigSecret(ctx, client, namespace, secretName, registry, username, password); err != nil { 157 | t.Fatalf("error creating secret: %v", err) 158 | } 159 | if err := createServiceAccount(ctx, client, namespace, serviceAccountName, secretName); err != nil { 160 | t.Fatalf("error creating service account: %v", err) 161 | } 162 | 163 | kc, err := createK8schain(ctx, log, client, nil) 164 | if err != nil { 165 | t.Fatalf("error creating k8schain: %v", err) 166 | } 167 | 168 | authConfig, err := getAuthConfig(kc, imageTag) 169 | if err != nil { 170 | t.Fatalf("error getting authconfig: %v", err) 171 | } 172 | want := &authn.AuthConfig{ 173 | Username: username, 174 | Password: password, 175 | } 176 | if diff := cmp.Diff(want, authConfig, authConfigComparer); diff != "" { 177 | t.Errorf("createK8schain() authConfig mismatch (-want +got):\n%s", diff) 178 | } 179 | } 180 | 181 | func getAuthConfig(kc authn.Keychain, imageTag string) (*authn.AuthConfig, error) { 182 | tag, err := name.NewTag(imageTag) 183 | if err != nil { 184 | return nil, err 185 | } 186 | authenticator, err := kc.Resolve(tag) 187 | if err != nil { 188 | return nil, err 189 | } 190 | return authenticator.Authorization() 191 | } 192 | -------------------------------------------------------------------------------- /pkg/logging/discard.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 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 logging 16 | 17 | import ( 18 | "github.com/go-logr/logr" 19 | ) 20 | 21 | // CreateDiscardLogger for testing 22 | func CreateDiscardLogger() logr.Logger { 23 | return logr.Discard() 24 | } 25 | -------------------------------------------------------------------------------- /pkg/logging/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 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 logging provides functions to create various logr.Logger implementations. 16 | package logging 17 | -------------------------------------------------------------------------------- /pkg/logging/klog.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 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 logging 16 | 17 | import ( 18 | "github.com/go-logr/logr" 19 | "k8s.io/klog/v2/klogr" 20 | ) 21 | 22 | // CreateKlogLogger using Kubernetes klog 23 | func CreateKlogLogger() logr.Logger { 24 | return klogr.New() 25 | } 26 | -------------------------------------------------------------------------------- /pkg/logging/std.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 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 logging 16 | 17 | import ( 18 | "log" 19 | "os" 20 | 21 | "github.com/go-logr/logr" 22 | "github.com/go-logr/stdr" 23 | 24 | "github.com/google/k8s-digester/pkg/util" 25 | ) 26 | 27 | // CreateStdLogger creates a logr.Logger using Go's standard log package 28 | func CreateStdLogger(name string) logr.Logger { 29 | verbosity := 0 30 | if util.IsDebug() { 31 | verbosity = 3 32 | } 33 | stdr.SetVerbosity(verbosity) 34 | return stdr.New(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile)).WithName(name) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/logging/zap.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 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 logging 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/go-logr/logr" 21 | "github.com/go-logr/zapr" 22 | "go.uber.org/zap" 23 | 24 | "github.com/google/k8s-digester/pkg/util" 25 | ) 26 | 27 | // SyncLogger contains a logger and an associated sync function that should 28 | // be called using `defer`. 29 | type SyncLogger struct { 30 | Log logr.Logger 31 | Sync func() error 32 | } 33 | 34 | // CreateZapLogger for structured logging 35 | func CreateZapLogger(name string) (*SyncLogger, error) { 36 | zLog, err := createZapLogger() 37 | if err != nil { 38 | return nil, fmt.Errorf("could not create zap logger %w", err) 39 | } 40 | log := zapr.NewLogger(zLog).WithName(name) 41 | return &SyncLogger{ 42 | Log: log, 43 | Sync: zLog.Sync, 44 | }, nil 45 | } 46 | 47 | func createZapLogger() (*zap.Logger, error) { 48 | if util.IsDebug() { 49 | return zap.NewDevelopment() 50 | } 51 | return zap.NewProduction() 52 | } 53 | -------------------------------------------------------------------------------- /pkg/resolve/resolve.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 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 resolve looks up image references in resources and resolves tags to 16 | // digests using the `crane` package from `go-containerregistry`. 17 | package resolve 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "strings" 23 | 24 | "github.com/go-logr/logr" 25 | "github.com/google/go-containerregistry/pkg/authn" 26 | "github.com/google/go-containerregistry/pkg/crane" 27 | "k8s.io/client-go/rest" 28 | "sigs.k8s.io/kustomize/kyaml/yaml" 29 | 30 | "github.com/google/k8s-digester/pkg/keychain" 31 | "github.com/google/k8s-digester/pkg/version" 32 | ) 33 | 34 | var resolveTagFn = resolveTag // override for unit testing 35 | 36 | // ImageTags looks up the digest and adds it to the image field 37 | // for containers and initContainers in pods and pod template specs. 38 | // 39 | // It looks for image fields with tags in these sequence nodes: 40 | // - `spec.containers` 41 | // - `spec.initContainers` 42 | // - `spec.template.spec.containers` 43 | // - `spec.template.spec.initContainers` 44 | // - `spec.jobTemplate.spec.template.spec.containers` 45 | // - `spec.jobTemplate.spec.template.spec.initContainers` 46 | // The `config` input parameter can be null. In this case, the function 47 | // will not attempt to retrieve imagePullSecrets from the cluster. 48 | func ImageTags(ctx context.Context, log logr.Logger, config *rest.Config, n *yaml.RNode, skipPrefixes []string) error { 49 | kc, err := keychain.Create(ctx, log, config, n) 50 | if err != nil { 51 | return fmt.Errorf("could not create keychain: %w", err) 52 | } 53 | imageTagFilter := &ImageTagFilter{ 54 | Log: log, 55 | Keychain: kc, 56 | SkipPrefixes: &skipPrefixes, 57 | } 58 | // if input is a CronJob, we need to look up the image tags in the 59 | // `spec.jobTemplate.spec.template.spec` path as well 60 | if n.GetKind() == "CronJob" { 61 | return n.PipeE( 62 | yaml.Lookup("spec", "jobTemplate", "spec", "template", "spec"), 63 | yaml.Tee(yaml.Lookup("containers"), imageTagFilter), 64 | yaml.Tee(yaml.Lookup("initContainers"), imageTagFilter), 65 | ) 66 | } 67 | // otherwise, we look up the image tags in the `spec.template.spec` path 68 | return n.PipeE( 69 | yaml.Lookup("spec"), 70 | yaml.Tee(yaml.Lookup("containers"), imageTagFilter), 71 | yaml.Tee(yaml.Lookup("initContainers"), imageTagFilter), 72 | yaml.Lookup("template", "spec"), 73 | yaml.Tee(yaml.Lookup("containers"), imageTagFilter), 74 | yaml.Tee(yaml.Lookup("initContainers"), imageTagFilter), 75 | ) 76 | } 77 | 78 | // ImageTagFilter resolves image tags to digests 79 | type ImageTagFilter struct { 80 | Log logr.Logger 81 | Keychain authn.Keychain 82 | SkipPrefixes *[]string 83 | } 84 | 85 | var _ yaml.Filter = &ImageTagFilter{} 86 | 87 | // Filter to resolve image tags to digests for a list of containers 88 | func (f *ImageTagFilter) Filter(n *yaml.RNode) (*yaml.RNode, error) { 89 | if err := n.VisitElements(f.filterImage); err != nil { 90 | return nil, err 91 | } 92 | return n, nil 93 | } 94 | 95 | func (f *ImageTagFilter) filterImage(n *yaml.RNode) error { 96 | imageNode, err := n.Pipe(yaml.Lookup("image")) 97 | if err != nil { 98 | s, _ := n.String() 99 | return fmt.Errorf("could not lookup image in node %v: %w", s, err) 100 | } 101 | image := yaml.GetValue(imageNode) 102 | for _, prefix := range *f.SkipPrefixes { 103 | if strings.HasPrefix(image, prefix) { 104 | // Image should be excluded from digest resolution 105 | return nil 106 | } 107 | } 108 | if strings.Contains(image, "@") { 109 | return nil // already has digest, skip 110 | } 111 | digest, err := resolveTagFn(image, f.Keychain) 112 | if err != nil { 113 | return fmt.Errorf("could not get digest for %s: %w", image, err) 114 | } 115 | f.Log.V(1).Info("resolved tag to digest", "image", image, "digest", digest) 116 | imageWithDigest := fmt.Sprintf("%s@%s", image, digest) 117 | n.Pipe(yaml.Lookup("image"), yaml.Set(yaml.NewStringRNode(imageWithDigest))) 118 | return nil 119 | } 120 | 121 | func resolveTag(image string, keychain authn.Keychain) (string, error) { 122 | return crane.Digest(image, 123 | crane.WithAuthFromKeychain(keychain), 124 | crane.WithUserAgent(fmt.Sprintf("cloud-solutions/%s-%s", "k8s-digester", version.Version))) 125 | } 126 | -------------------------------------------------------------------------------- /pkg/resolve/resolve_stub_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 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 resolve 16 | 17 | import ( 18 | "fmt" 19 | 20 | "sigs.k8s.io/kustomize/kyaml/yaml" 21 | ) 22 | 23 | func createPodNode(containerImages []string, initContainerImages []string) (*yaml.RNode, error) { 24 | node, err := yaml.FromMap(M{ 25 | "apiVersion": "v1", 26 | "kind": "Pod", 27 | "metadata": M{ 28 | "name": "test-pod", 29 | }, 30 | }) 31 | if err != nil { 32 | return nil, err 33 | } 34 | for index, image := range containerImages { 35 | if err := node.PipeE( 36 | yaml.LookupCreate(yaml.SequenceNode, "spec", "containers"), 37 | yaml.Append(yaml.NewMapRNode(&map[string]string{ 38 | "name": fmt.Sprintf("container%d", index), 39 | "image": image, 40 | }).YNode()), 41 | ); err != nil { 42 | return nil, err 43 | } 44 | } 45 | for index, image := range initContainerImages { 46 | if err := node.PipeE( 47 | yaml.LookupCreate(yaml.SequenceNode, "spec", "initContainers"), 48 | yaml.Append(yaml.NewMapRNode(&map[string]string{ 49 | "name": fmt.Sprintf("initcontainer%d", index), 50 | "image": image, 51 | }).YNode()), 52 | ); err != nil { 53 | return nil, err 54 | } 55 | } 56 | return node, nil 57 | } 58 | 59 | func createCronJobNode(containerImages []string, initContainerImages []string) (*yaml.RNode, error) { 60 | node, err := yaml.FromMap(M{ 61 | "apiVersion": "batch/v1beta1", 62 | "kind": "CronJob", 63 | "metadata": M{ 64 | "name": "test-pod", 65 | }, 66 | }) 67 | if err != nil { 68 | return nil, err 69 | } 70 | for index, image := range containerImages { 71 | if err := node.PipeE( 72 | yaml.LookupCreate(yaml.SequenceNode, "spec", "jobTemplate", "spec", "template", "spec", "containers"), 73 | yaml.Append(yaml.NewMapRNode(&map[string]string{ 74 | "name": fmt.Sprintf("container%d", index), 75 | "image": image, 76 | }).YNode()), 77 | ); err != nil { 78 | return nil, err 79 | } 80 | } 81 | for index, image := range initContainerImages { 82 | if err := node.PipeE( 83 | yaml.LookupCreate(yaml.SequenceNode, "spec", "jobTemplate", "spec", "template", "spec", "initContainers"), 84 | yaml.Append(yaml.NewMapRNode(&map[string]string{ 85 | "name": fmt.Sprintf("initcontainer%d", index), 86 | "image": image, 87 | }).YNode()), 88 | ); err != nil { 89 | return nil, err 90 | } 91 | } 92 | return node, nil 93 | } 94 | 95 | func createDeploymentNode(containerImages []string, initContainerImages []string) (*yaml.RNode, error) { 96 | node, err := yaml.FromMap(M{ 97 | "apiVersion": "apps/v1", 98 | "kind": "Deployment", 99 | "metadata": M{ 100 | "name": "test-deployment", 101 | }, 102 | }) 103 | if err != nil { 104 | return nil, err 105 | } 106 | for index, image := range containerImages { 107 | if err := node.PipeE( 108 | yaml.LookupCreate(yaml.SequenceNode, "spec", "template", "spec", "containers"), 109 | yaml.Append(yaml.NewMapRNode(&map[string]string{ 110 | "name": fmt.Sprintf("container%d", index), 111 | "image": image, 112 | }).YNode()), 113 | ); err != nil { 114 | return nil, err 115 | } 116 | } 117 | for index, image := range initContainerImages { 118 | if err := node.PipeE( 119 | yaml.LookupCreate(yaml.SequenceNode, "spec", "template", "spec", "initContainers"), 120 | yaml.Append(yaml.NewMapRNode(&map[string]string{ 121 | "name": fmt.Sprintf("initcontainer%d", index), 122 | "image": image, 123 | }).YNode()), 124 | ); err != nil { 125 | return nil, err 126 | } 127 | } 128 | return node, nil 129 | } 130 | -------------------------------------------------------------------------------- /pkg/resolve/resolve_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 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 resolve 16 | 17 | import ( 18 | "context" 19 | "crypto/sha256" 20 | "encoding/hex" 21 | "fmt" 22 | "strings" 23 | "testing" 24 | 25 | "github.com/google/go-containerregistry/pkg/authn" 26 | "sigs.k8s.io/kustomize/kyaml/yaml" 27 | 28 | "github.com/google/k8s-digester/pkg/logging" 29 | ) 30 | 31 | type M map[string]interface{} 32 | 33 | type anonymousKeychain struct{} 34 | 35 | var _ authn.Keychain = &anonymousKeychain{} 36 | 37 | func (kc *anonymousKeychain) Resolve(_ authn.Resource) (authn.Authenticator, error) { 38 | return authn.Anonymous, nil 39 | } 40 | 41 | var ( 42 | ctx = context.Background() 43 | log = logging.CreateDiscardLogger() 44 | filter = &ImageTagFilter{ 45 | Log: log, 46 | Keychain: &anonymousKeychain{}, 47 | SkipPrefixes: &[]string{}, 48 | } 49 | ) 50 | 51 | func init() { 52 | // Implementation of resolveTagFn that computes the SHA-256 sum of the 53 | // image name. We could make this simpler since it's just for testing, but 54 | // it means the digest values have the same 'shape' as real values. 55 | resolveTagFn = func(image string, _ authn.Keychain) (string, error) { 56 | if image == "" || image == "error" { 57 | return "", fmt.Errorf("intentional error resolving image [%s]", image) 58 | } 59 | sumBytes := sha256.Sum256([]byte(image)) 60 | digest := strings.TrimRight(hex.EncodeToString(sumBytes[:]), "\r\n") 61 | return fmt.Sprintf("sha256:%s", digest), nil 62 | } 63 | } 64 | 65 | func TestSkip(t *testing.T) { 66 | if testing.Short() { 67 | t.Skip("skipping test in short mode") 68 | } 69 | } 70 | 71 | func Test_ImageTagFilter_filterImage_Container(t *testing.T) { 72 | imageTag := "registry.example.com/repository/image:tag" 73 | node, err := yaml.FromMap(M{ 74 | "name": "test-container-image", 75 | "image": imageTag, 76 | }) 77 | if err != nil { 78 | t.Fatalf("could not create container node: %v", err) 79 | } 80 | 81 | if err := filter.filterImage(node); err != nil { 82 | t.Fatalf("unexpected Filter error: %v", err) 83 | } 84 | 85 | imageNode, err := node.Pipe(yaml.Lookup("image")) 86 | if err != nil { 87 | t.Fatalf("could not get image field from node: %s: %v", node.MustString(), err) 88 | } 89 | gotImage := yaml.GetValue(imageNode) 90 | // wantDigest=$(echo -n "registry.example.com/repository/image:tag" | shasum -a 256 | cut -d' ' -f1) 91 | wantDigest := "sha256:90dc9a6dfb6f86fe35508c9e94d255922bce69c3d5c520b29d65685fab4ee18d" 92 | wantImage := fmt.Sprintf("%s@%s", imageTag, wantDigest) 93 | if wantImage != gotImage { 94 | t.Errorf("wanted [%s], got [%s]", wantImage, gotImage) 95 | } 96 | } 97 | 98 | func Test_ImageTags_Pod(t *testing.T) { 99 | node, err := createPodNode([]string{"image0", "image1"}, []string{"image2", "image3"}) 100 | if err != nil { 101 | t.Fatalf("could not create pod node: %v", err) 102 | } 103 | 104 | if err := ImageTags(ctx, log, nil, node, []string{}); err != nil { 105 | t.Fatalf("problem resolving image tags: %v", err) 106 | } 107 | t.Log(node.MustString()) 108 | 109 | assertContainer(t, node, "image0@sha256:07d7d43fe9dd151e40f0a8d54c5211a8601b04e4a8fa7ad57ea5e73e4ffa7e4a", "spec", "containers", "[name=container0]") 110 | assertContainer(t, node, "image1@sha256:cc292b92ce7f10f2e4f727ecdf4b12528127c51b6ddf6058e213674603190d06", "spec", "containers", "[name=container1]") 111 | assertContainer(t, node, "image2@sha256:5bb21ac469b5e7df4e17899d4aae0adfb430f0f0b336a2242ef1a22d25bd2e53", "spec", "initContainers", "[name=initcontainer0]") 112 | assertContainer(t, node, "image3@sha256:b0542da3f90bad69318e16ec7fcb6b13b089971886999e08bec91cea34891f0f", "spec", "initContainers", "[name=initcontainer1]") 113 | } 114 | 115 | func Test_ImageTags_Pod_Skip_Prefixes(t *testing.T) { 116 | node, err := createPodNode([]string{"image0", "skip1.local/image1"}, []string{"image2", "skip2.local/image3"}) 117 | if err != nil { 118 | t.Fatalf("could not create pod node: %v", err) 119 | } 120 | 121 | if err := ImageTags(ctx, log, nil, node, []string{"skip1.local", "skip2.local"}); err != nil { 122 | t.Fatalf("problem resolving image tags: %v", err) 123 | } 124 | t.Log(node.MustString()) 125 | 126 | assertContainer(t, node, "image0@sha256:07d7d43fe9dd151e40f0a8d54c5211a8601b04e4a8fa7ad57ea5e73e4ffa7e4a", "spec", "containers", "[name=container0]") 127 | assertContainer(t, node, "skip1.local/image1", "spec", "containers", "[name=container1]") 128 | assertContainer(t, node, "image2@sha256:5bb21ac469b5e7df4e17899d4aae0adfb430f0f0b336a2242ef1a22d25bd2e53", "spec", "initContainers", "[name=initcontainer0]") 129 | assertContainer(t, node, "skip2.local/image3", "spec", "initContainers", "[name=initcontainer1]") 130 | } 131 | 132 | func Test_ImageTags_CronJob(t *testing.T) { 133 | node, err := createCronJobNode([]string{"image0", "image1"}, []string{"image2", "image3"}) 134 | if err != nil { 135 | t.Fatalf("could not create pod node: %v", err) 136 | } 137 | 138 | if err := ImageTags(ctx, log, nil, node, []string{}); err != nil { 139 | t.Fatalf("problem resolving image tags: %v", err) 140 | } 141 | t.Log(node.MustString()) 142 | 143 | assertContainer(t, node, "image0@sha256:07d7d43fe9dd151e40f0a8d54c5211a8601b04e4a8fa7ad57ea5e73e4ffa7e4a", "spec", "jobTemplate", "spec", "template", "spec", "containers", "[name=container0]") 144 | assertContainer(t, node, "image1@sha256:cc292b92ce7f10f2e4f727ecdf4b12528127c51b6ddf6058e213674603190d06", "spec", "jobTemplate", "spec", "template", "spec", "containers", "[name=container1]") 145 | assertContainer(t, node, "image2@sha256:5bb21ac469b5e7df4e17899d4aae0adfb430f0f0b336a2242ef1a22d25bd2e53", "spec", "jobTemplate", "spec", "template", "spec", "initContainers", "[name=initcontainer0]") 146 | assertContainer(t, node, "image3@sha256:b0542da3f90bad69318e16ec7fcb6b13b089971886999e08bec91cea34891f0f", "spec", "jobTemplate", "spec", "template", "spec", "initContainers", "[name=initcontainer1]") 147 | } 148 | 149 | func Test_ImageTags_Deployment(t *testing.T) { 150 | node, err := createDeploymentNode([]string{"image0", "image1"}, []string{"image2", "image3"}) 151 | if err != nil { 152 | t.Fatalf("could not create deployment node: %v", err) 153 | } 154 | 155 | if err := ImageTags(ctx, log, nil, node, []string{}); err != nil { 156 | t.Fatalf("problem resolving image tags: %v", err) 157 | } 158 | t.Log(node.MustString()) 159 | 160 | assertContainer(t, node, "image0@sha256:07d7d43fe9dd151e40f0a8d54c5211a8601b04e4a8fa7ad57ea5e73e4ffa7e4a", "spec", "template", "spec", "containers", "[name=container0]") 161 | assertContainer(t, node, "image1@sha256:cc292b92ce7f10f2e4f727ecdf4b12528127c51b6ddf6058e213674603190d06", "spec", "template", "spec", "containers", "[name=container1]") 162 | assertContainer(t, node, "image2@sha256:5bb21ac469b5e7df4e17899d4aae0adfb430f0f0b336a2242ef1a22d25bd2e53", "spec", "template", "spec", "initContainers", "[name=initcontainer0]") 163 | assertContainer(t, node, "image3@sha256:b0542da3f90bad69318e16ec7fcb6b13b089971886999e08bec91cea34891f0f", "spec", "template", "spec", "initContainers", "[name=initcontainer1]") 164 | } 165 | 166 | func assertContainer(t *testing.T, n *yaml.RNode, imageWithDigest string, path ...string) { 167 | container, err := n.Pipe(yaml.Lookup(path...), yaml.Get("image")) 168 | if err != nil { 169 | t.Fatalf("could not find container-0: %v", err) 170 | } 171 | got := yaml.GetValue(container) 172 | want := imageWithDigest 173 | if want != got { 174 | t.Errorf("wanted [%s], got [%s]", want, got) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /pkg/util/debug.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 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 util 16 | 17 | import ( 18 | "os" 19 | "strings" 20 | ) 21 | 22 | // IsDebug returns true if we're running in debug mode 23 | func IsDebug() bool { 24 | debugEnv := strings.ToLower(os.Getenv("DEBUG")) 25 | return debugEnv == "true" || debugEnv == "yes" || debugEnv == "1" 26 | } 27 | -------------------------------------------------------------------------------- /pkg/util/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 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 util contains miscellaneous utility functions. 16 | package util 17 | -------------------------------------------------------------------------------- /pkg/util/pod_info.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 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 util 16 | 17 | import ( 18 | "io/ioutil" 19 | "os" 20 | ) 21 | 22 | const DefaultNamespace = "digester-system" 23 | 24 | // GetPodName returns the name of the pod 25 | func GetPodName() string { 26 | pod, found := os.LookupEnv("POD_NAME") 27 | if found { 28 | return pod 29 | } 30 | podb, err := ioutil.ReadFile("/etc/hostname") 31 | if err == nil && len(podb) > 0 { 32 | return string(podb) 33 | } 34 | return os.Getenv("HOSTNAME") 35 | } 36 | 37 | // GetNamespace returns the namespace of the pod 38 | func GetNamespace() string { 39 | ns, found := os.LookupEnv("POD_NAMESPACE") 40 | if found { 41 | return ns 42 | } 43 | nsb, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") 44 | if err == nil && len(nsb) > 0 { 45 | return string(nsb) 46 | } 47 | return DefaultNamespace 48 | } 49 | -------------------------------------------------------------------------------- /pkg/util/stringarray.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func StringArray(str string) []string { 8 | return strings.FieldsFunc(str, func(r rune) bool { 9 | return r == ':' || r == ';' 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 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 version provides the version of this tool. 16 | // Ref: https://pkg.go.dev/embed 17 | package version 18 | 19 | import ( 20 | _ "embed" 21 | "strings" 22 | ) 23 | 24 | var ( 25 | Version = strings.TrimSpace(version) 26 | 27 | //go:embed version.txt 28 | version string 29 | ) 30 | -------------------------------------------------------------------------------- /pkg/version/version.txt: -------------------------------------------------------------------------------- 1 | (devel) 2 | -------------------------------------------------------------------------------- /skaffold.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 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 | apiVersion: skaffold/v3 16 | kind: Config 17 | metadata: 18 | name: digester 19 | build: 20 | artifacts: 21 | - image: k8s-digester 22 | ko: 23 | dependencies: 24 | paths: 25 | - .ko.yaml 26 | - go.mod 27 | - '**/*.go' 28 | ignore: 29 | - '**/*_test.go' 30 | tagPolicy: 31 | inputDigest: {} 32 | manifests: 33 | rawYaml: 34 | - manifests/*.yaml 35 | profiles: 36 | - name: release 37 | patches: 38 | - op: add 39 | path: /build/artifacts/0/platforms 40 | value: 41 | - linux/amd64 42 | - linux/arm64 43 | - op: add 44 | path: /build/artifacts/0/ko/labels 45 | value: 46 | org.opencontainers.image.description: >- 47 | Digester adds digests to container and init container images in 48 | Kubernetes pod and pod template specs. Use either as a mutating 49 | admission webhook, or as a client-side KRM function with kpt or 50 | kustomize. 51 | org.opencontainers.image.licenses: Apache-2.0 52 | org.opencontainers.image.revision: '{{.REVISION}}' 53 | org.opencontainers.image.source: '{{.SOURCE}}' 54 | org.opencontainers.image.title: k8s-digester 55 | org.opencontainers.image.url: '{{.URL}}' 56 | org.opencontainers.image.vendor: Google LLC 57 | org.opencontainers.image.version: '{{.VERSION}}' 58 | - op: add 59 | path: /build/artifacts/0/ko/flags 60 | value: 61 | - -v 62 | - op: add 63 | path: /build/artifacts/0/ko/ldflags 64 | value: 65 | - -s 66 | - -w 67 | --------------------------------------------------------------------------------