Repository: ko-build/ko Branch: main Commit: 757161aaa19e Files: 186 Total size: 594.7 KB Directory structure: gitextract_aa0jajfb/ ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── analyze.yaml │ ├── boilerplate.yaml │ ├── build.yaml │ ├── donotsubmit.yaml │ ├── e2e.yaml │ ├── image.yaml │ ├── kind-e2e.yaml │ ├── modules-integration-test.yaml │ ├── publish-site.yaml │ ├── registries.yaml │ ├── release.yml │ ├── sbom.yaml │ ├── stale.yaml │ ├── style.yaml │ ├── test.yaml │ └── verify.yaml ├── .gitignore ├── .golangci.yaml ├── .goreleaser.yml ├── .ko.yaml ├── .wokeignore ├── CNAME ├── CONTRIBUTING.md ├── LICENSE ├── MAINTAINERS.md ├── README.md ├── ROADMAP.md ├── cmd/ │ └── help/ │ └── main.go ├── code-of-conduct.md ├── docs/ │ ├── CNAME │ ├── README.md │ ├── advanced/ │ │ ├── faq.md │ │ ├── go-packages.md │ │ ├── lambda.md │ │ ├── limitations.md │ │ ├── linux-capabilities.md │ │ ├── migrating-from-dockerfile.md │ │ ├── root-ca-certificates.md │ │ └── terraform.md │ ├── community.md │ ├── configuration.md │ ├── custom/ │ │ ├── main.html │ │ └── partials/ │ │ └── copyright.html │ ├── deployment.md │ ├── features/ │ │ ├── build-cache.md │ │ ├── debugging.md │ │ ├── k8s.md │ │ ├── multi-platform.md │ │ ├── sboms.md │ │ └── static-assets.md │ ├── get-started.md │ ├── index.md │ ├── install.md │ └── reference/ │ ├── ko.md │ ├── ko_apply.md │ ├── ko_build.md │ ├── ko_create.md │ ├── ko_delete.md │ ├── ko_login.md │ ├── ko_resolve.md │ ├── ko_run.md │ └── ko_version.md ├── go.mod ├── go.sum ├── hack/ │ ├── boilerplate/ │ │ ├── boilerplate.go.txt │ │ └── boilerplate.sh.txt │ ├── presubmit.sh │ ├── tools.go │ ├── update-codegen.sh │ └── update-deps.sh ├── integration_test.sh ├── internal/ │ └── sbom/ │ ├── sbom.go │ └── spdx.go ├── main.go ├── mkdocs.yml ├── pkg/ │ ├── build/ │ │ ├── build.go │ │ ├── cache.go │ │ ├── config.go │ │ ├── doc.go │ │ ├── future.go │ │ ├── future_test.go │ │ ├── gobuild.go │ │ ├── gobuild_test.go │ │ ├── gobuilds.go │ │ ├── gobuilds_test.go │ │ ├── layer.go │ │ ├── limit.go │ │ ├── limit_test.go │ │ ├── options.go │ │ ├── recorder.go │ │ ├── recorder_test.go │ │ ├── shared.go │ │ ├── shared_test.go │ │ ├── strict.go │ │ └── strict_test.go │ ├── caps/ │ │ ├── caps.go │ │ ├── caps_dd_test.go │ │ ├── caps_test.go │ │ ├── gen.sh │ │ └── new_file_caps_test.go │ ├── commands/ │ │ ├── apply.go │ │ ├── build.go │ │ ├── cache.go │ │ ├── commands.go │ │ ├── config.go │ │ ├── config_test.go │ │ ├── create.go │ │ ├── delete.go │ │ ├── options/ │ │ │ ├── build.go │ │ │ ├── build_test.go │ │ │ ├── filestuff.go │ │ │ ├── namer_test.go │ │ │ ├── publish.go │ │ │ ├── selector.go │ │ │ ├── testdata/ │ │ │ │ ├── bad-config/ │ │ │ │ │ └── .ko.yaml/ │ │ │ │ │ └── .gitignore │ │ │ │ ├── config/ │ │ │ │ │ ├── .ko.yaml │ │ │ │ │ └── my-ko.yaml │ │ │ │ ├── multiple-platforms/ │ │ │ │ │ └── .ko.yaml │ │ │ │ └── paths/ │ │ │ │ ├── .ko.yaml │ │ │ │ └── app/ │ │ │ │ ├── cmd/ │ │ │ │ │ └── foo/ │ │ │ │ │ └── main.go │ │ │ │ └── go.mod │ │ │ └── validate.go │ │ ├── publisher.go │ │ ├── publisher_test.go │ │ ├── resolve.go │ │ ├── resolver.go │ │ ├── resolver_test.go │ │ ├── root.go │ │ ├── run.go │ │ └── version.go │ ├── doc.go │ ├── internal/ │ │ ├── git/ │ │ │ ├── clone.go │ │ │ ├── errors.go │ │ │ ├── git.go │ │ │ ├── info.go │ │ │ └── info_test.go │ │ ├── gittesting/ │ │ │ ├── git.go │ │ │ └── git_test.go │ │ └── testing/ │ │ ├── daemon.go │ │ ├── doc.go │ │ ├── fixed.go │ │ └── fixed_test.go │ ├── publish/ │ │ ├── daemon.go │ │ ├── daemon_test.go │ │ ├── default.go │ │ ├── default_test.go │ │ ├── doc.go │ │ ├── future.go │ │ ├── future_test.go │ │ ├── kind/ │ │ │ ├── doc.go │ │ │ ├── write.go │ │ │ └── write_test.go │ │ ├── kind.go │ │ ├── layout.go │ │ ├── layout_test.go │ │ ├── multi.go │ │ ├── multi_test.go │ │ ├── options.go │ │ ├── publish.go │ │ ├── recorder.go │ │ ├── recorder_test.go │ │ ├── shared.go │ │ ├── shared_test.go │ │ ├── tarball.go │ │ └── tarball_test.go │ └── resolve/ │ ├── doc.go │ ├── resolve.go │ ├── resolve_test.go │ ├── selector.go │ └── selector_test.go └── test/ ├── build-configs/ │ ├── .ko.yaml │ ├── bar/ │ │ ├── cmd/ │ │ │ └── main.go │ │ └── go.mod │ ├── caps/ │ │ ├── cmd/ │ │ │ └── main.go │ │ └── go.mod │ ├── caps.ko.yaml │ ├── foo/ │ │ ├── cmd/ │ │ │ └── main.go │ │ └── go.mod │ └── toolexec/ │ ├── cmd/ │ │ └── main.go │ └── go.mod ├── kodata/ │ ├── a │ ├── kenobi │ └── subdir/ │ └── file.txt ├── main.go └── test.yaml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "daily" - package-ecosystem: github-actions directory: / schedule: interval: "daily" ================================================ FILE: .github/workflows/analyze.yaml ================================================ name: Analyze on: workflow_dispatch: push: branches: ['main'] pull_request: branches: ['main'] permissions: {} jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: security-events: write steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. fetch-depth: 2 persist-credentials: false - uses: github/codeql-action/init@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9 with: languages: go - uses: github/codeql-action/autobuild@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9 - uses: github/codeql-action/analyze@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9 ================================================ FILE: .github/workflows/boilerplate.yaml ================================================ name: Boilerplate on: pull_request: branches: ['main'] permissions: {} jobs: check: name: Boilerplate Check runs-on: ubuntu-latest permissions: contents: read strategy: fail-fast: false # Keep running if one leg fails. matrix: extension: - go - sh # Map between extension and human-readable name. include: - extension: go language: Go - extension: sh language: Bash steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: chainguard-dev/actions/boilerplate@e82b4e5ae10182af72972addcb3fedf7454621c8 # main with: extension: ${{ matrix.extension }} language: ${{ matrix.language }} ================================================ FILE: .github/workflows/build.yaml ================================================ name: Build on: pull_request: branches: - "main" permissions: {} jobs: build: name: Build runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version: "1.24" check-latest: true - uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4 with: go-version-input: "1.25" - run: | go build ./... go test -run=^$ ./... ================================================ FILE: .github/workflows/donotsubmit.yaml ================================================ name: Do Not Submit on: pull_request: branches: ['main'] permissions: {} jobs: donotsubmit: name: Do Not Submit runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: chainguard-dev/actions/donotsubmit@84c993eaf02da1c325854fb272a4df9184bd80fc # main ================================================ FILE: .github/workflows/e2e.yaml ================================================ name: Basic e2e test on: pull_request: branches: - 'main' permissions: {} jobs: e2e: strategy: fail-fast: false matrix: platform: - ubuntu-latest - windows-latest name: e2e ${{ matrix.platform }} runs-on: ${{ matrix.platform }} permissions: contents: read steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version-file: 'go.mod' check-latest: true - name: Build and run ko container env: KO_DOCKER_REPO: ko.local shell: bash run: | set -o errexit set -o nounset set -o pipefail set -x # Check that building without push prints the tag (and sha) KO_DOCKER_REPO="" go run ./ build --push=false ./test | grep ":latest@sha256:" KO_DOCKER_REPO="" go run ./ build --push=false -t test ./test | grep ":test@sha256:" KO_DOCKER_REPO="" go run ./ build --push=false -t test --tag-only ./test | grep ":test$" # Check that using sbom-dir works. KO_DOCKER_REPO="" go run ./ build -t test --push=false --sbom-dir ./sbom-data ./test jq . ./sbom-data/test-linux-amd64.spdx.json # Check that using sbom-dir works for multi-arch KO_DOCKER_REPO="" go run ./ build --platform=linux/amd64,linux/arm64 -t test --push=false --sbom-dir ./sbom-data2 ./test jq . ./sbom-data2/test-index.spdx.json jq . ./sbom-data2/test-linux-amd64.spdx.json jq . ./sbom-data2/test-linux-arm64.spdx.json export PLATFORM=$(go env GOOS)/$(go env GOARCH) if [[ "${{ matrix.platform }}" == "windows-latest" ]]; then OSVERSION="10.0.20348" PLATFORM=${PLATFORM}:${OSVERSION} export KO_DEFAULTBASEIMAGE=mcr.microsoft.com/windows/nanoserver:ltsc2022 else # Explicitly test multiple platform builds (a subset of what's in the base!) export PLATFORM=${PLATFORM},linux/arm64 fi echo platform is ${PLATFORM} # Build and run the ko binary, which should be runnable. docker run $(go run ./ build ./ --platform=${PLATFORM} --preserve-import-paths) version # Build and run the test/ binary, which should log "Hello there" served from KO_DATA_PATH testimg=$(go run ./ build ./test --platform=${PLATFORM} --preserve-import-paths) docker run ${testimg} --wait=false 2>&1 | tee >(grep "Hello there") # Log all output too. # Check that symlinks in kodata are chased. # Skip this test on Windows. if [[ "$RUNNER_OS" == "Linux" ]]; then docker run ${testimg} --wait=false -f b fi # Check that using ldflags to set variables works. cat > .ko.yaml << EOF builds: - id: test main: ./test/ ldflags: - "-X main.version=${{ github.sha }}" EOF docker run $(go run ./ build ./test/ --platform=${PLATFORM}) --wait=false 2>&1 | grep "${{ github.sha }}" # TODO: check why it is failing when building for windows if [[ "${{ matrix.platform }}" != "windows-latest" ]]; then # Check that --debug adds dlv to the image, and that dlv is runnable. docker run --entrypoint="dlv" $(go run ./ build ./test/ --platform=${PLATFORM} --debug) version | grep "Delve Debugger" fi ================================================ FILE: .github/workflows/image.yaml ================================================ name: image on: push: branches: - 'main' workflow_dispatch: permissions: {} jobs: image: runs-on: ubuntu-latest permissions: contents: read packages: write id-token: write steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version-file: 'go.mod' check-latest: true - uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 # Build ko from HEAD, build and push an image tagged with the commit SHA, # then keylessly sign it with cosign. - name: Publish and sign image env: KO_DOCKER_REPO: ghcr.io/${{ github.repository }} run: | go build ./ echo "${{ github.token }}" | ./ko login ghcr.io --username "${{ github.actor }}" --password-stdin img=$(./ko build --bare --platform=all -t latest -t ${{ github.sha }} ./) echo "built ${img}" cosign sign ${img} --yes \ -a sha=${{ github.sha }} \ -a run_id=${{ github.run_id }} \ -a run_attempt=${{ github.run_attempt }} ================================================ FILE: .github/workflows/kind-e2e.yaml ================================================ name: KinD e2e tests on: workflow_dispatch: # Allow manual runs. pull_request: branches: - 'main' permissions: {} jobs: e2e-tests: name: e2e tests runs-on: ubuntu-latest env: # https://github.com/google/go-containerregistry/pull/125 allows insecure registry for # '*.local' hostnames. This works both for `ko` and our own tag-to-digest resolution logic, # thus allowing us to test without bypassing tag-to-digest resolution. REGISTRY_NAME: registry.local REGISTRY_PORT: 5000 KO_DOCKER_REPO: registry.local:5000/ko permissions: contents: read steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version-file: 'go.mod' check-latest: true - name: Install ko run: go install ./ - name: Setup Cluster uses: chainguard-dev/actions/setup-kind@29fb6e979a0b3efc79748a17e8cec08d0594cbfd # main with: k8s-version: v1.28.x registry-authority: ${{ env.REGISTRY_NAME }}:${{ env.REGISTRY_PORT }} - name: Install Cosign uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 - name: Run Smoke Test run: | # Test with kind load KO_DOCKER_REPO=kind.local ko apply --platform=all -f ./test kubectl wait --timeout=10s --for=condition=Ready pod/kodata kubectl delete pod kodata # Test with registry ko apply --platform=all -f ./test kubectl wait --timeout=60s --for=condition=Ready pod/kodata kubectl delete pod kodata # Test ko run with kind load # This tests that --labels are passed to kubectl, and -wait is passed to the test binary. KO_DOCKER_REPO=kind.local ko run ./test -- --labels=foo=bar -- -wait=false - name: Check SBOM run: | set -o pipefail echo '::group:: SBOM' cosign download sbom $(ko build ./test) echo "${SBOM}" echo '::endgroup::' - name: Collect diagnostics and upload if: ${{ failure() }} uses: chainguard-dev/actions/kind-diag@29fb6e979a0b3efc79748a17e8cec08d0594cbfd # main ================================================ FILE: .github/workflows/modules-integration-test.yaml ================================================ name: Integration Test on: pull_request: branches: - 'main' permissions: {} jobs: test: name: Module Tests runs-on: 'ubuntu-latest' permissions: contents: read steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version: '1.24' check-latest: true - env: GOPATH: does not matter run: ./integration_test.sh ================================================ FILE: .github/workflows/publish-site.yaml ================================================ name: publish on: workflow_dispatch: push: branches: - 'main' permissions: {} jobs: publish: runs-on: ubuntu-latest permissions: contents: write pages: write steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: 3.x - run: pip install mkdocs-material mkdocs-redirects - run: mkdocs gh-deploy --force ================================================ FILE: .github/workflows/registries.yaml ================================================ name: Push to registries on: push: branches: - 'main' workflow_dispatch: # Allow manual runs. permissions: {} jobs: quay: name: Push to quay.io runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version-file: 'go.mod' check-latest: true - env: QUAY_USERNAME: ko-testing+test QUAY_PASSWORD: ${{ secrets.QUAY_PASSWORD }} KO_DOCKER_REPO: quay.io/ko-testing/test run: | echo ${QUAY_PASSWORD} | go run ./ login --username=${QUAY_USERNAME} --password-stdin quay.io go run ./ build --platform=all ./test/ --sbom=none --bare dockerhub: name: Push to dockerhub runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version-file: 'go.mod' check-latest: true - env: DOCKERHUB_USERNAME: kotesting DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} KO_DOCKER_REPO: kotesting/test run: | echo ${DOCKERHUB_PASSWORD} | go run ./ login --username=${DOCKERHUB_USERNAME} --password-stdin index.docker.io go run ./ build --platform=all ./test/ --bare ecr: name: Push to ECR runs-on: ubuntu-latest env: # This is an AWS account that Chainguard provides to enable # go-containerregistry and ko to test ECR support. AWS_ACCOUNT: 479305788615 AWS_REGION: us-west-2 REPOSITORY: ko-ecr-e2e-testing permissions: # This lets us clone the repo contents: read # This lets us mint identity tokens for federation with AWS. id-token: write steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version-file: 'go.mod' check-latest: true - name: Install ko run: go install . - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 with: role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT }}:role/federated-ecr-readwrite aws-region: ${{ env.AWS_REGION }} - name: Test ko build run: | export KO_DOCKER_REPO=${{ env.AWS_ACCOUNT }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.REPOSITORY }} ko build --bare ./test ================================================ FILE: .github/workflows/release.yml ================================================ name: goreleaser on: push: tags: - '*' permissions: {} jobs: goreleaser: outputs: hashes: ${{ steps.hash.outputs.hashes }} tag_name: ${{ steps.tag.outputs.tag_name }} permissions: packages: write id-token: write contents: write runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - run: git fetch --prune --unshallow - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version-file: 'go.mod' check-latest: true # This installs the current latest release. We have to manually bump this. - uses: ko-build/setup-ko@3aebd0597dc1e9d1a26bcfdb7cbeb19c131d3037 # v0.7 with: version: v0.18.0 # DO NOT REMOVE THIS OR IT WILL CREATE A CYCLE. - uses: imjasonh/setup-crane@31b88efe9de28ae0ffa220711af4b60be9435f6e # v0.4 - uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 - name: Set tag output id: tag run: echo "tag_name=${GITHUB_REF#refs/*/}" >> "$GITHUB_OUTPUT" - uses: goreleaser/goreleaser-action@9ed2f89a662bf1735a48bc8557fd212fa902bebf # v6.1.0 id: run-goreleaser with: version: latest args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: sign ko-image run: | digest=$(crane digest "${REGISTRY}":"${GIT_TAG}") cosign sign --yes \ -a GIT_HASH="${GIT_HASH}" \ -a GIT_TAG="${GIT_TAG}" \ -a RUN_ID="${RUN_ID}" \ -a RUN_ATTEMPT="${RUN_ATTEMPT}" \ "${REGISTRY}@${digest}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GIT_HASH: ${{ github.sha }} GIT_TAG: ${{ steps.tag.outputs.tag_name }} RUN_ATTEMPT: ${{ github.run_attempt }} RUN_ID: ${{ github.run_id }} REGISTRY: "ghcr.io/${{ github.repository }}" - name: Generate subject id: hash env: ARTIFACTS: "${{ steps.run-goreleaser.outputs.artifacts }}" run: | set -euo pipefail checksum_file=$(echo "$ARTIFACTS" | jq -r '.[] | select (.type=="Checksum") | .path') echo "hashes=$(cat $checksum_file | base64 -w0)" >> "$GITHUB_OUTPUT" provenance: needs: - goreleaser permissions: actions: read # To read the workflow path. id-token: write # To sign the provenance. contents: write # To add assets to a release. uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0 with: base64-subjects: "${{ needs.goreleaser.outputs.hashes }}" upload-assets: true upload-tag-name: "${{ needs.goreleaser.outputs.tag_name }}" verification: needs: - goreleaser - provenance runs-on: ubuntu-latest permissions: read-all steps: # Note: this will be replaced with the GHA in the future. - name: Install the verifier env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail gh -R slsa-framework/slsa-verifier release download v1.3.2 -p "slsa-verifier-linux-amd64" chmod ug+x slsa-verifier-linux-amd64 # Note: see https://github.com/slsa-framework/slsa-verifier/blob/main/SHA256SUM.md COMPUTED_HASH=$(sha256sum slsa-verifier-linux-amd64 | cut -d ' ' -f1) EXPECTED_HASH="b1d6c9bbce6274e253f0be33158cacd7fb894c5ebd643f14a911bfe55574f4c0" if [[ "$EXPECTED_HASH" != "$COMPUTED_HASH" ]];then echo "error: expected $EXPECTED_HASH, computed $COMPUTED_HASH" exit 1 fi - name: Download assets env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PROVENANCE: "${{ needs.provenance.outputs.provenance-name }}" run: | set -euo pipefail gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "*.tar.gz" gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "$PROVENANCE" - name: Verify assets env: CHECKSUMS: ${{ needs.goreleaser.outputs.hashes }} PROVENANCE: "${{ needs.provenance.outputs.provenance-name }}" run: | set -euo pipefail checksums=$(echo "$CHECKSUMS" | base64 -d) while read -r line; do fn=$(echo $line | cut -d ' ' -f2) echo "Verifying $fn" ./slsa-verifier-linux-amd64 -artifact-path "$fn" \ -provenance "$PROVENANCE" \ -source "github.com/$GITHUB_REPOSITORY" \ -tag "$GITHUB_REF_NAME" done <<<"$checksums" ================================================ FILE: .github/workflows/sbom.yaml ================================================ name: Validate SBOMs on: pull_request: branches: - 'main' env: SPDX_TOOLS_VERSION: 1.1.0 permissions: {} jobs: spdx: name: Validate SPDX SBOM runs-on: ubuntu-latest permissions: contents: read env: KO_DOCKER_REPO: localhost:1338 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version-file: 'go.mod' check-latest: true - uses: chainguard-dev/actions/setup-registry@main - uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 - name: Install SPDX Tools run: | wget https://github.com/spdx/tools-java/releases/download/v${SPDX_TOOLS_VERSION}/tools-java-${SPDX_TOOLS_VERSION}.zip unzip tools-java-${SPDX_TOOLS_VERSION}.zip - name: Generate and Validate run: | cosign download sbom $(go run ./ build) | tee spdx.json java -jar ./tools-java-${SPDX_TOOLS_VERSION}-jar-with-dependencies.jar Verify spdx.json - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 if: ${{ always() }} with: name: spdx.json path: spdx.json spdx-multi-arch: name: Validate SPDX multi-arch SBOM runs-on: ubuntu-latest env: KO_DOCKER_REPO: localhost:1338 permissions: contents: read steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version-file: 'go.mod' check-latest: true - uses: chainguard-dev/actions/setup-registry@main - uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 - name: Install SPDX Tools run: | wget https://github.com/spdx/tools-java/releases/download/v${SPDX_TOOLS_VERSION}/tools-java-${SPDX_TOOLS_VERSION}.zip unzip tools-java-${SPDX_TOOLS_VERSION}.zip - name: Generate and Validate run: | img=$(go run ./ build --platform=linux/amd64,linux/arm64) cosign download sbom $img | tee spdx-multi-arch.json java -jar ./tools-java-${SPDX_TOOLS_VERSION}-jar-with-dependencies.jar Verify spdx-multi-arch.json - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 if: ${{ always() }} with: name: spdx-multi-arch.json path: spdx-multi-arch.json ================================================ FILE: .github/workflows/stale.yaml ================================================ name: 'Close stale' on: schedule: - cron: '0 1 * * *' permissions: {} jobs: stale: runs-on: 'ubuntu-latest' permissions: issues: write pull-requests: write steps: - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: |- This issue is stale because it has been open for 90 days with no activity. It will automatically close after 30 more days of inactivity. Keep fresh with the 'lifecycle/frozen' label. stale-issue-label: 'lifecycle/stale' exempt-issue-labels: 'lifecycle/frozen' stale-pr-message: |- This Pull Request is stale because it has been open for 90 days with no activity. It will automatically close after 30 more days of inactivity. Keep fresh with the 'lifecycle/frozen' label. stale-pr-label: 'lifecycle/stale' exempt-pr-labels: 'lifecycle/frozen' days-before-stale: 90 days-before-close: 30 ================================================ FILE: .github/workflows/style.yaml ================================================ name: Code Style on: pull_request: branches: - 'main' permissions: {} jobs: gofmt: name: check gofmt runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version-file: 'go.mod' check-latest: true - uses: chainguard-dev/actions/gofmt@d886686603afb809f7ef9b734b333e20b7ce5cda with: args: -s goimports: name: check goimports runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version-file: 'go.mod' check-latest: true - uses: chainguard-dev/actions/goimports@d886686603afb809f7ef9b734b333e20b7ce5cda lint: name: Lint runs-on: ubuntu-latest permissions: contents: read steps: - name: Check out code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Set up Go uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version-file: 'go.mod' check-latest: true - uses: chainguard-dev/actions/trailing-space@d886686603afb809f7ef9b734b333e20b7ce5cda if: ${{ always() }} - uses: chainguard-dev/actions/eof-newline@d886686603afb809f7ef9b734b333e20b7ce5cda if: ${{ always() }} - uses: reviewdog/action-misspell@18ffb61effb93b47e332f185216be7e49592e7e1 # v1.26.1 if: ${{ always() }} with: github_token: ${{ secrets.GITHUB_TOKEN }} fail_level: warning locale: "US" exclude: | ./.golangci.yaml - uses: get-woke/woke-action-reviewdog@d71fd0115146a01c3181439ce714e21a69d75e31 # v0 if: ${{ always() }} with: github-token: ${{ secrets.GITHUB_TOKEN }} reporter: github-pr-check level: error fail-on-error: true ================================================ FILE: .github/workflows/test.yaml ================================================ name: Test on: push: branches: - 'main' pull_request: branches: - 'main' permissions: {} jobs: test: name: Unit Tests runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version-file: 'go.mod' check-latest: true - run: go test -coverprofile=coverage.txt -covermode=atomic -race ./... ================================================ FILE: .github/workflows/verify.yaml ================================================ name: Verify on: pull_request: branches: - "main" permissions: {} jobs: verify: name: Verify Codegen runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version-file: "go.mod" check-latest: true - name: Verify run: ./hack/presubmit.sh golangci: name: lint runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version-file: "go.mod" check-latest: true - name: golangci-lint uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.4.0 with: version: v2.7.2 ================================================ FILE: .gitignore ================================================ # Ignore GoLand (IntelliJ) files. .idea/ ko .DS_Store /vendor/ ================================================ FILE: .golangci.yaml ================================================ version: "2" linters: enable: - asciicheck - errorlint - gosec - importas - misspell - prealloc - revive - staticcheck - tparallel - unconvert - unparam - whitespace disable: - depguard - errcheck settings: gosec: excludes: - G115 exclusions: generated: lax presets: - comments - common-false-positives - legacy - std-error-handling rules: - linters: - gosec path: test paths: - third_party$ - builtin$ - examples$ formatters: enable: - gofmt - goimports exclusions: generated: lax paths: - third_party$ - builtin$ - examples$ ================================================ FILE: .goreleaser.yml ================================================ version: 2 before: hooks: - go mod tidy - /bin/bash -c 'if [ -n "$(git --no-pager diff --exit-code go.mod go.sum)" ]; then exit 1; fi' builds: - id: binary main: ./main.go env: - CGO_ENABLED=0 flags: - -trimpath ldflags: - "-s -w -X github.com/google/ko/pkg/commands.Version={{.Version}}" goos: - windows - linux - darwin goarch: - amd64 - arm64 - s390x - 386 - mips64le - ppc64le - riscv64 kos: - id: ko-image build: binary main: . base_image: golang:latest ldflags: - "-s -w -X github.com/google/ko/pkg/commands.Version={{.Version}}" platforms: - all tags: - '{{ .Tag }}' - '{{ .FullCommit }}' - latest sbom: spdx bare: true preserve_import_paths: false base_import_paths: false archives: - id: with-version name_template: >- {{ .ProjectName }}_ {{- .Version }}_ {{- title .Os }}_ {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} - id: without-version name_template: >- {{ .ProjectName }}_ {{- title .Os }}_ {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} checksum: name_template: 'checksums.txt' snapshot: version_template: "{{ .Tag }}-next" changelog: sort: asc use: github filters: exclude: - '^docs:' - '^test:' ================================================ FILE: .ko.yaml ================================================ baseImageOverrides: github.com/google/ko: golang:latest builds: - id: ko ldflags: - "{{ .Env.LDFLAGS }}" ================================================ FILE: .wokeignore ================================================ # Uses some Cobra methods pkg/commands/* ================================================ FILE: CNAME ================================================ ko.build ================================================ FILE: CONTRIBUTING.md ================================================ # How to Contribute to ko We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. ## Code reviews All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests. ## Testing Ensure the following passes: ``` ./hack/presubmit.sh ``` and commit any resultant changes to `go.mod` and `go.sum`. To update any docs after client changes, run: ``` ./hack/update-codegen.sh ``` ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: MAINTAINERS.md ================================================ # Maintainers This page lists all active members of the maintainers for the `ko` project and any subprojects. - Jon Johnson (@jonjohnsonjr) - Matt Moore (@mattmoor) - Jason Hall (@imjasonh) New projects and new maintainers can be added or removed with the consensus approval of the current maintainers. ================================================ FILE: README.md ================================================ # `ko`: Easy Go Containers [![GitHub Actions Build Status](https://github.com/ko-build/ko/workflows/Build/badge.svg)](https://github.com/ko-build/ko/actions?query=workflow%3ABuild) [![GoDoc](https://godoc.org/github.com/google/ko?status.svg)](https://godoc.org/github.com/google/ko) [![Go Report Card](https://goreportcard.com/badge/ko-build/ko)](https://goreportcard.com/report/ko-build/ko) [![SLSA 3](https://slsa.dev/images/gh-badge-level3.svg)](https://slsa.dev/images/gh-badge-level3.svg) --- > 🎉 Google has applied for `ko` to join the Cloud Native Computing Foundation as a Sandbox project! Learn more [here](https://opensource.googleblog.com/2022/10/ko-applies-to-become-a-cncf-sandbox-project.html)! `ko` is a simple, fast container image builder for Go applications. It's ideal for use cases where your image contains a single Go application without any/many dependencies on the OS base image (e.g., no cgo, no OS package dependencies). `ko` builds images by effectively executing `go build` on your local machine, and as such doesn't require `docker` to be installed. This can make it a good fit for lightweight CI/CD use cases. `ko` makes [multi-platform builds](https://ko.build/features/multi-platform/) easy, produces [SBOMs](https://ko.build/features/sboms/) by default, and includes support for simple YAML templating which makes it a powerful tool for [Kubernetes applications](https://ko.build/features/k8s/). # [Install `ko`](https://ko.build/install/) and [get started](https://ko.build/get-started/)! ### Acknowledgements This work is based heavily on experience from having built the [Docker](https://github.com/bazelbuild/rules_docker) and [Kubernetes](https://github.com/bazelbuild/rules_k8s) support for [Bazel](https://bazel.build). That work was presented [here](https://www.youtube.com/watch?v=RS1aiQqgUTA). ### Discuss Questions? Comments? Ideas? Come discuss `ko` with us in the `#ko-build` channel on the [Kubernetes Slack](https://slack.k8s.io)! See you there! ### Community Meetings You can find all the necessary details about the community meetings in this [page](https://ko.build/community). ================================================ FILE: ROADMAP.md ================================================ # `ko` Project Roadmap _Last updated October 2022_ - Foster a community of contributors and users - give talks, do outreach, expand the pool of contributors - identify projects that could benefit from using `ko`, and help onboard them - publish case studies from successful migrations - Integrate [sigstore](https://sigstore.dev) for built artifacts - attach signed SBOMs - attach signed provenance attestations - support the OCI referrers API and [fallback tag scheme](https://github.com/opencontainers/distribution-spec/blob/main/spec.md#referrers-tag-schema) - integrate with CI workload identity (e.g., GitHub OIDC) to keylessly sign artifacts - Faster builds - identify unnecessary work and avoid it when possible - Ecosystem integrations - support Terraform provider, and potentially Pulumi and CDK, others - provide working examples of these integrations ================================================ FILE: cmd/help/main.go ================================================ // Copyright 2021 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "os" "github.com/google/ko/pkg/commands" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" ) var dir string var root = &cobra.Command{ Use: "gendoc", Short: "Generate ko's help docs", Args: cobra.NoArgs, RunE: func(*cobra.Command, []string) error { return doc.GenMarkdownTree(commands.Root, dir) }, } func init() { root.Flags().StringVarP(&dir, "dir", "d", ".", "Path to directory in which to generate docs") } func main() { if err := root.Execute(); err != nil { fmt.Println(err) os.Exit(1) } } ================================================ FILE: code-of-conduct.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: - Using welcoming and inclusive language - Being respectful of differing viewpoints and experiences - Gracefully accepting constructive criticism - Focusing on what is best for the community - Showing empathy towards other community members Examples of unacceptable behavior by participants include: - The use of sexualized language or imagery and unwelcome sexual attention or advances - Trolling, insulting/derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or electronic address, without explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org ================================================ FILE: docs/CNAME ================================================ ko.build ================================================ FILE: docs/README.md ================================================ # Docs for https://ko.build ## Development Update `.md` files to update content. Update `mkdocs.yml` to update sidebar headers and ordering. To run locally: - [install `mkdocs` and `mkdocs-material`](https://squidfunk.github.io/mkdocs-material/getting-started/) and run `mkdocs serve`, or - `docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material` - on an M1 Mac, use `ghcr.io/afritzler/mkdocs-material` instead. This will start a local server on localhost:8000 that autoupdates as you make changes. ## Deployment When PRs are merged, the site will be rebuilt and published automatically. ### Credits The site is powered by [mkdocs-material](https://squidfunk.github.io/mkdocs-material). The code and theme are released under the MIT license. Content is licensed [CC-BY](https://creativecommons.org/licenses/by/4.0/). ================================================ FILE: docs/advanced/faq.md ================================================ # Frequently Asked Questions ## How can I set `ldflags`? [Using -ldflags](https://blog.cloudflare.com/setting-go-variables-at-compile-time/) is a common way to embed version info in go binaries (In fact, we do this for `ko`!). Unfortunately, because `ko` wraps `go build`, it's not possible to use this flag directly; however, you can use the `GOFLAGS` environment variable instead: ```sh GOFLAGS="-ldflags=-X=main.version=1.2.3" ko build . ``` Currently, there is a limitation that does not allow to set multiple arguments in `ldflags` using `GOFLAGS`. Using `-ldflags` multiple times also does not work. In this use case, it works best to use the [`builds` section](./../configuration.md) in the `.ko.yaml` file. ## Why are my images all created in 1970? In order to support [reproducible builds](https://reproducible-builds.org), `ko` doesn't embed timestamps in the images it produces by default. However, `ko` does respect the [`SOURCE_DATE_EPOCH`](https://reproducible-builds.org/docs/source-date-epoch/) environment variable, which will set the container image's timestamp accordingly. Similarly, the `KO_DATA_DATE_EPOCH` environment variable can be used to set the _modtime_ timestamp of the files in `KO_DATA_PATH`. For example, you can set the container image's timestamp to the current timestamp by executing: ```sh export SOURCE_DATE_EPOCH=$(date +%s) ``` or set the timestamp of the files in `KO_DATA_PATH` to the latest git commit's timestamp with: ```sh export KO_DATA_DATE_EPOCH=$(git log -1 --format='%ct') ``` ## Can I build Windows containers? Yes, but support for Windows containers is new, experimental, and tenuous. Be prepared to file bugs. 🐛 The default base image does not provide a Windows image. You can try out building a Windows container image by [setting the base image](./../configuration.md) to a Windows base image and building with `--platform=windows/amd64` or `--platform=all`: For example, to build a Windows container image, update your `.ko.yaml` to set the base image: ```plaintext defaultBaseImage: mcr.microsoft.com/windows/nanoserver:ltsc2022 ``` And build for `windows/amd64`. ```sh ko build ./ --platform=windows/amd64 ``` ### Known issues 🐛 - Symlinks in `kodata` are ignored when building Windows images; only regular files and directories will be included in the Windows image. ## Does `ko` support autocompletion? Yes! `ko completion` generates a Bash/Zsh/Fish/PowerShell completion script. You can get how to load it from help document. ```sh ko completion [bash|zsh|fish|powershell] --help ``` Or, you can source it directly: ```bash source <(ko completion) ``` ## Does `ko` work with [Kustomize](https://kustomize.io/)? Yes! `ko resolve -f -` will read and process input from stdin, so you can have `ko` easily process the output of the `kustomize` command. ```sh kustomize build config | ko resolve -f - ``` ## Does `ko` integrate with other build and development tools? Oh, you betcha. Here's a partial list: - `ko` support in [Skaffold](https://skaffold.dev/docs/pipeline-stages/builders/ko/) - `ko` support for [goreleaser](https://goreleaser.com/customization/ko/) - `ko` task in the [Tekton catalog](https://github.com/tektoncd/catalog/tree/main/task/ko/) - `ko` support in [Carvel's `kbld`](https://carvel.dev/kbld/docs/latest/config/#ko) - `ko` extension for [Tilt](https://github.com/tilt-dev/tilt-extensions/tree/master/ko) ## Does `ko` work with [OpenShift Internal Registry](https://access.redhat.com/documentation/en-us/openshift_container_platform/4.11/html/registry/registry-overview#registry-integrated-openshift-registry_registry-overview)? Yes! Follow these steps: 1. [Connect to your OpenShift installation](https://docs.openshift.com/container-platform/latest/cli_reference/openshift_cli/getting-started-cli.html#cli-logging-in_cli-developer-commands) 1. [Expose the OpenShift Internal Registry](https://docs.openshift.com/container-platform/latest/registry/securing-exposing-registry.html) so you can push to it: 1. Export your token to `$HOME/.docker/config.json`: ```sh oc registry login --to=$HOME/.docker/config.json ``` 1. Create a namespace where you will push your images, i.e: `ko-images` 1. Execute this command to set `KO_DOCKER_REPO` to publish images to the internal registry. ```sh export KO_DOCKER_REPO=$(oc registry info --public)/ko-images ``` ================================================ FILE: docs/advanced/go-packages.md ================================================ # Go Packages `ko`'s functionality can be consumed as a library in a Go application. To build an image, use [`pkg/build`](https://pkg.go.dev/github.com/google/ko/pkg/build), and publish it with [`pkg/publish`](https://pkg.go.dev/github.com/google/ko/pkg/publish). This is a minimal example of using the packages together, to implement the core subset of `ko`'s functionality: ```go package main import ( "context" "fmt" "log" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/ko/pkg/build" "github.com/google/ko/pkg/publish" ) const ( baseImage = "cgr.dev/chainguard/static:latest" targetRepo = "example.registry/my-repo" importpath = "github.com/my-org/miniko" commitSHA = "deadbeef" ) func main() { ctx := context.Background() b, err := build.NewGo(ctx, ".", build.WithPlatforms("linux/amd64"), // only build for these platforms. build.WithBaseImages(func(ctx context.Context, _ string) (name.Reference, build.Result, error) { ref := name.MustParseReference(baseImage) base, err := remote.Index(ref, remote.WithContext(ctx)) return ref, base, err })) if err != nil { log.Fatalf("NewGo: %v", err) } r, err := b.Build(ctx, importpath) if err != nil { log.Fatalf("Build: %v", err) } p, err := publish.NewDefault(targetRepo, // publish to example.registry/my-repo publish.WithTags([]string{commitSHA}), // tag with :deadbeef publish.WithAuthFromKeychain(authn.DefaultKeychain)) // use credentials from ~/.docker/config.json if err != nil { log.Fatalf("NewDefault: %v", err) } ref, err := p.Publish(ctx, r, importpath) if err != nil { log.Fatalf("Publish: %v", err) } fmt.Println(ref.String()) } ``` ================================================ FILE: docs/advanced/lambda.md ================================================ # AWS Lambda `ko` can build images that can be deployed as AWS Lambda functions, using [Lambda's container support](https://docs.aws.amazon.com/lambda/latest/dg/images-create.html). For best results, use the [Go runtime interface client](https://docs.aws.amazon.com/lambda/latest/dg/go-image.html#go-image-clients) provided by the [`lambda` package](https://pkg.go.dev/github.com/aws/aws-lambda-go/lambda). For example: ```go package main import ( "context" "fmt" "github.com/aws/aws-lambda-go/lambda" ) type Event struct { Name string `json:"name"` // TODO: add other request fields here. } func main() { lambda.Start(func(ctx context.Context, event Event) (string, error) { return fmt.Sprintf("Hello %s!", event.Name), nil }) } ``` See AWS's [documentation](https://docs.aws.amazon.com/lambda/latest/dg/golang-handler.html) for more information on writing Lambda functions in Go. To deploy to Lambda, you must push to AWS Elastic Container Registry (ECR): ```sh KO_DOCKER_REPO=[account-id].dkr.ecr.[region].amazonaws.com/my-repo image=$(ko build ./cmd/app) ``` Then, create a Lambda function using the image in ECR: ```sh aws lambda create-function \ --function-name hello-world \ --package-type Image \ --code ImageUri=${image} \ --role arn:aws:iam::[account-id]:role/lambda-ex ``` See AWS's [documentation](https://docs.aws.amazon.com/lambda/latest/dg/go-image.html) for more information on deploying Lambda functions using Go container images, including how to configure push access to ECR, and how to configure the IAM role for the function. The base image that `ko` uses by default supports both x86 and Graviton2 architectures. You can also use the [`ko` Terraform provider](./terraform.md) to build and deploy Lambda functions as part of your IaC workflow, using the [`aws_lambda_function` resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function.html). See the [provider example](https://github.com/ko-build/terraform-provider-ko/tree/main/provider-examples/lambda) to get started. ================================================ FILE: docs/advanced/limitations.md ================================================ # Limitations `ko` works best when your application has no dependencies on the underlying image. This means `ko` is ideal when you don't require [cgo](https://pkg.go.dev/cmd/cgo), and builds are executed with `CGO_ENABLED=0` by default. To install other OS packages, make those available in your [configured base image](../../configuration). `ko` only supports Go applications. For a similar tool targeting Java applications, try [Jib](https://github.com/GoogleContainerTools/jib). For other languages, try [apko](https://github.com/chainguard-dev/apko) and [melange](https://github.com/chainguard-dev/melange). ================================================ FILE: docs/advanced/linux-capabilities.md ================================================ # Linux Capabilities In Linux, capabilities are a way to selectively grant privileges to a running process. Docker provides `--cap-add` and `--cap-drop` [run options](https://docs.docker.com/engine/containers/run/#runtime-privilege-and-linux-capabilities) to tweak container capabilities, e.g: ``` docker run --cap-add bpf hello-world ``` If container runs as a non-root user, capabilities are narrowed by intersecting with *file* capabilities of the application binary. When building images with a Dockerfile, one typically uses `setcap` tool to modify file capabilities, e.g: `setcap FILE bpf=ep`. To set file capabilities with `ko`, specify `linux_capabilities` in builds configuration section in your `.ko.yaml`. Use `setcap` syntax: ```yaml builds: - id: caps linux_capabilities: bpf=ep ``` ## Alternative spelling ```yaml builds: - id: caps linux_capabilities: - cap1 - cap2 - cap3 ``` A list of capability names is equivalent to `cap1,cap2,cap3=p`. ## Improving UX in capability-reliant apps A capability can be *permitted* (`=p`), or both *permitted* and *effective* (`=ep`). Effective capabilities are used for permission checks. A program can promote permitted capability to effective when needed. ```yaml builds: - id: caps linux_capabilities: bpf,perfmon,net_admin=ep ``` Initially, `=ep` might look like a good idea. There's no need to explicitly promote *permitted* capabilities. Application takes advantage of *effective* capabilities right away. There is a catch though. ``` $ docker run --cap-add bpf ko.local/caps.test-4b8f7bca75c467b3d2803e1c087a3287 exec /ko-app/caps.test: operation not permitted ``` When run options request fewer capabilities than specified in file capabilities, container fails to start. It is hard to tell what went wrong since `operation not permitted` is a generic error. (Docker is unlikely to improve diagnostics for this failure case since the check is implemented in Linux kernel.) We suggest to use `=p` instead. This option puts application in charge of verifying and promoting permitted capabilities to effective. It can produce much better diagnostics: ``` $ docker run --cap-add bpf ko.local/caps.test-4b8f7bca75c467b3d2803e1c087a3287 current capabilities: cap_bpf=p activating capabilities: cap_net_admin,cap_perfmon,cap_bpf=ep: operation not permitted ``` [Sample code](https://go.dev/play/p/uPMzyotkNHg). ================================================ FILE: docs/advanced/migrating-from-dockerfile.md ================================================ # Migrating from Dockerfile If your `Dockerfile` looks like either of the examples in the [official tutorial for writing a Dockerfile to containerize a Go application](https://docs.docker.com/language/golang/build-images/), you can easily migrate to use `ko` instead. Let's review the best practice multi-stage Dockerfile in that tutorial first: ```Dockerfile ## Build FROM golang:1.16-buster AS build WORKDIR /app COPY go.mod ./ COPY go.sum ./ RUN go mod download COPY *.go ./ RUN go build -o /docker-gs-ping ## Deploy FROM gcr.io/distroless/base-debian10 WORKDIR / COPY --from=build /docker-gs-ping /docker-gs-ping EXPOSE 8080 USER nonroot:nonroot ENTRYPOINT ["/docker-gs-ping"] ``` This `Dockerfile`: 1. pulls the `golang:1.16` image 1. `COPY`s your local source into the container environment (`COPY`ing `go.mod` and `go.sum` first and running `go mod download`, to cache dependencies in the container environment) 1. `RUN`s `go build` on your source, inside the container, to produce an executable 1. `COPY`s the executable built in the previous step into a new image, on top of a minimal [distroless](https://github.com/GoogleContainerTools/distroless) base image. The result is a Go application built on a minimal base image, with an optimally cached build sequence. After running `docker build` on this `Dockerfile`, don't forget to push that image to the registry so you can deploy it. --- ## Migrating to `ko` If your Go source is laid out as described in the tutorial, and you've [installed](../../install) and [set up your environment](../../get-started), you can simply run `ko build ./` to build and push the container image to your registry. You're done. You can delete your `Dockerfile` and uninstall `docker`. `ko` takes advantage of your local [Go build cache](../../features/build-cache) without needing to be told to, and it sets the `ENTRYPOINT` and uses a nonroot distroless base image by default. To build a multi-arch image, simply add `--platform=all`. Compare this to the [equivalent Docker instructions](https://docs.docker.com/desktop/multi-arch/). ================================================ FILE: docs/advanced/root-ca-certificates.md ================================================ # Root CA Certificates To install a [root certificate](https://en.wikipedia.org/wiki/Root_certificate) into your container built using `ko`, you can use one of the following methods. ## incert [`incert`](https://github.com/chainguard-dev/incert) allows you to append CA certificates to an image and push the modified image to a specified registry. `incert` can be run after `ko build` to build your Go application container image with custom root CA certificates. ### Example 1. Build and push your Go application container image using `ko build` ```sh KO_DOCKER_REPO=mycompany/myimage:latest ko build . ``` 2. Append the built image with your custom CA certificate(s) using `incert` ```sh incert -image-url=mycompany/myimage:latest -ca-certs-file=/path/to/cacerts.pem -dest-image-url=myregistry/myimage:latest ``` ## Custom Base Image New root certificates can be [installed into a custom image](https://stackoverflow.com/questions/42292444/how-do-i-add-a-ca-root-certificate-inside-a-docker-image) using standard OS packages. Then, this custom image can be used [to override the base image for `ko`](https://ko.build/configuration/#overriding-base-images). Once the Go application container image is built using `ko` with the custom base image, the root certificates installed on the base image will be trusted by the Go application. ### Example 1. Make a custom container image with your new root certificates ```dockerfile # Dockerfile FROM alpine RUN apk update RUN apk add ca-certificates ADD new-root-ca.crt /usr/local/share/ca-certificates/new-root-ca.crt RUN chmod 644 /usr/local/share/ca-certificates/new-root-ca.crt RUN update-ca-certificates ``` 2. Build and push the custom container image to a container registry ```sh docker build . -t docker.io/ko-build/image-with-new-root-certs docker push docker.io/ko-build/image-with-new-root-certs ``` 3. Configure `ko` to [override the default base image](https://ko.build/configuration/#overriding-base-images) with the custom image ```yaml # .ko.yaml defaultBaseImage: docker.io/ko-build/image-with-new-root-certs ``` **OR** ```sh export KO_DEFAULTBASEIMAGE=docker.io/ko-build/image-with-new-root-certs ``` 4. Build the Go app container image with `ko` ```sh ko build . ``` ## Static Assets Alternatively, root certificates can be installed into the Go application container image using a combination of [`ko` static assets](https://ko.build/features/static-assets/) and [overriding the default system location for SSL certificates](https://pkg.go.dev/crypto/x509#SystemCertPool). Using `ko`'s support for static assets, root certificates can be stored in the `/kodata` directory (either checked into the repository, or injected dynamically by a CI pipeline). After running `ko build`, the certificate files are then bundled into the built image at the path `$KO_DATA_PATH`. To enable the Go application to trust the bundled certificate(s), the container runtime or orchestrator (Docker, Kubernetes, etc) must set the environment variable `SSL_CERT_DIR` to the same value as `KO_DATA_PATH`. Go [uses `SSL_CERT_DIR` to determine the directory to check for SSL certificate files](https://go.dev/src/crypto/x509/root_unix.go). Once this variable is set, the Go application will trust the bundled root certificates in `$KO_DATA_PATH`. ### Example 1. Copy the root certificate(s) to the `/kodata/` directory ```sh # $(pwd) assumed to be at for this example mkdir -p kodata cp $CERT_FILE_DIR/*.crt kodata/ ``` 2. Build the Go application container image ```sh KO_DOCKER_REPO=docker.io/ko-build/static-assets-certs ko build . ``` 3. Run the Go application container image with `SSL_CERT_DIR` equal to `/var/run/ko` (the default value for `$KO_DATA_PATH`) ```sh docker run -e SSL_CERT_DIR=/var/run/ko docker.io/ko-build/static-assets-certs ``` A functional client-server example for this can be seen [here](https://github.com/kosamson/ko-private-ca-test). ================================================ FILE: docs/advanced/terraform.md ================================================ # Terraform Provider In addition to the CLI, `ko`'s functionality is also available as a Terraform provider. This allows `ko` to be integrated with your Infrastructure-as-Code (IaC) workflows, and makes building your code a seamless part of your deployment process. Using the Terraform provider is as simple as adding a `ko_build` resource to your Terraform configuration: ```hcl // Require the `ko-build/ko` provider. terraform { required_providers { ko = { source = "ko-build/ko" } } } // Configure the provider to push to your repo. provider "ko" { repo = "example.registry/my-repo" // equivalent to KO_DOCKER_REPO } // Build your code. resource "ko_build" "app" { importpath = "github.com/example/repo/cmd/app" } // TODO: use the `ko_build.app` resource elsewhere in your Terraform configuration. // Report the build image's digest. output "image" { value = ko_build.app.image_ref } ``` See the [`ko-build/ko` provider on the Terraform Registry](https://registry.terraform.io/providers/ko-build/ko/latest) for more information, and the [GitHub repo](https://github.com/ko-build/terraform-provider-ko) for more examples. ================================================ FILE: docs/community.md ================================================ # Community ## Meetings We have a bi-weekly community meeting on [Wednesdays at 1:00 PM US Eastern time, 10:00 AM US Western time](https://dateful.com/eventlink/2763257725). The main goal of these meetings is that we want to hear from you! We want to know what you're using `ko` for, what you'd like to see in `ko`, how we can make `ko` better for you. With any remaining time we can go through open issues and PRs. We have a [meeting agenda](https://ko.build/agenda) you can use to propose topics for discussion/ideas. You can also just show up and we'll figure out what to talk about. ## Slack Come discuss `ko` with us in the `#ko-build` channel on the [Kubernetes Slack](https://ko.build/slack)! See you there! ================================================ FILE: docs/configuration.md ================================================ # Configuration ## Basic Configuration Aside from certain environment variables (see [below](#environment-variables-advanced)) like `KO_DOCKER_REPO`, you can configure `ko`'s behavior using a `.ko.yaml` file. The location of this file can be overridden with `KO_CONFIG_PATH`. ### Overriding Base Images By default, `ko` bases images on `cgr.dev/chainguard/static`. This is a small image that provides the bare necessities to run your Go binary. You can override this base image in two ways: 1. To override the base image for all images `ko` builds, add this line to your `.ko.yaml` file: ```yaml defaultBaseImage: registry.example.com/base/image ``` You can also use the `KO_DEFAULTBASEIMAGE` environment variable to set the default base image, which overrides the YAML configuration: ```shell KO_DEFAULTBASEIMAGE=registry.example.com/base/image ko build . ``` 2. To override the base image for certain importpaths: ```yaml baseImageOverrides: github.com/my-user/my-repo/cmd/app: registry.example.com/base/for/app github.com/my-user/my-repo/cmd/foo: registry.example.com/base/for/foo ``` ### Overriding Go build settings By default, `ko` builds the binary with no additional build flags other than `-trimpath`. You can replace the default build arguments by providing build flags and ldflags using a [GoReleaser](https://github.com/goreleaser/goreleaser) influenced `builds` configuration section in your `.ko.yaml`. ```yaml builds: - id: foo dir: . # default is . main: ./foobar/foo env: - GOPRIVATE=git.internal.example.com,source.developers.google.com flags: - -tags - netgo ldflags: - -s -w - -extldflags "-static" - -X main.version={{.Env.VERSION}} - id: bar dir: ./bar main: . # default is . env: - GOCACHE=/workspace/.gocache ldflags: - -s - -w ``` If your repository contains multiple modules (multiple `go.mod` files in different directories), use the `dir` field to specify the directory where `ko` should run `go build`. `ko` picks the entry from `builds` based on the import path you request. The import path is matched against the result of joining `dir` and `main`. The paths specified in `dir` and `main` are relative to the working directory of the `ko` process. The `ldflags` default value is `[]`. ### Templating support The `ko` builds supports templating of `flags` and `ldflags`, similar to the [GoReleaser `builds` section](https://goreleaser.com/customization/build/). The table below lists the supported template parameters. | Template param | Description | |-----------------------|----------------------------------------------------------| | `Env` | Map of environment variables used for the build | | `GoEnv` | Map of `go env` environment variables used for the build | | `Date` | The UTC build date in RFC 3339 format | | `Timestamp` | The UTC build date as Unix epoc seconds | | `Git.Branch` | The current git branch | | `Git.Tag` | The current git tag | | `Git.ShortCommit` | The git commit short hash | | `Git.FullCommit` | The git commit full hash | | `Git.CommitDate` | The UTC commit date in RFC 3339 format | | `Git.CommitTimestamp` | The UTC commit date in Unix format | | `Git.IsDirty` | Whether or not current git state is dirty | | `Git.IsClean` | Whether or not current git state is clean. | | `Git.TreeState` | Either `clean` or `dirty` | ### Setting default platforms By default, `ko` builds images based on the platform it runs on. If your target platform differs from your build platform you can specify the build platform: **As a parameter** See [Multi-Platform Images](./features/multi-platform.md). **In .ko.yaml** Add this to your `.ko.yaml` file: ```yaml defaultPlatforms: - linux/arm64 - linux/amd64 ``` You can also use the `KO_DEFAULTPLATFORMS` environment variable to set the default platforms, which overrides the YAML configuration: ```shell KO_DEFAULTPLATFORMS=linux/arm64,linux/amd64 ``` ### Setting build environment variables By default, `ko` builds use the ambient environment from the system (i.e. `os.Environ()`). These values can be overridden for your build. ```yaml defaultEnv: - FOO=foo builds: - id: foo dir: . main: ./foobar/foo env: - FOO=bar - id: bar # Will use defaultEnv. dir: ./bar main: . ``` For a given build, the environment variables are merged in the following order: - System `os.Environ` (lowest precedence) - Build variables: `build.env` if specified, otherwise `defaultEnv` (highest precedence) ### Setting build flags and ldflags You can specify both `flags` and `ldflags` globally as well as per-build. ```yaml defaultFlags: - -v defaultLdflags: - -s builds: - id: foo dir: . main: ./foobar/foo flags: - -trimpath ldflags: - -w - id: bar # Will use defaultFlags and defaultLdflags. dir: ./bar main: . ``` The values for a `build` will be used if specified, otherwise their respective defaults will be used. Both default and per-build values may use [template parameters](#templating-support). ### Environment Variables (advanced) For ease of use, backward compatibility and advanced use cases, `ko` supports the following environment variables to influence the build process. | Variable | Default Value | Description | |------------------|--------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `KO_DOCKER_REPO` | (not set) | Container repository where to push images built with `ko` (required) | | `KO_GO_PATH` | `go` | `go` binary to use for builds, relative or absolute path, otherwise looked up via $PATH (optional) | | `KO_CONFIG_PATH` | `./.ko.yaml` | Path to `ko` configuration file (optional) | | `KOCACHE` | (not set) | This tells `ko` to store a local mapping between the `go build` inputs to the image layer that they produce, so `go build` can be skipped entirely if the layer is already present in the image registry (optional). | ## Naming Images `ko` provides a few different strategies for naming the image it pushes, to workaround certain registry limitations and user preferences: Given `KO_DOCKER_REPO=registry.example.com/repo`, by default, `ko build ./cmd/app` will produce an image named like `registry.example.com/repo/app-`, which includes the MD5 hash of the full import path, to avoid collisions. - `--preserve-import-paths` (`-P`) will include the entire importpath: `registry.example.com/repo/github.com/my-user/my-repo/cmd/app` - `--base-import-paths` (`-B`) will omit the MD5 portion: `registry.example.com/repo/app` - `--bare` will only include the `KO_DOCKER_REPO`: `registry.example.com/repo` ## Local Publishing Options `ko` is normally used to publish images to container image registries, identified by `KO_DOCKER_REPO`. `ko` can also load images to a local Docker daemon, if available, by setting `KO_DOCKER_REPO=ko.local`, or by passing the `--local` (`-L`) flag. Local images can be used as a base image for other `ko` images: ```yaml defaultBaseImage: ko.local/example/base/image ``` `ko` can also load images into a local [KinD](https://kind.sigs.k8s.io) cluster, if available, by setting `KO_DOCKER_REPO=kind.local`. By default this loads into the default KinD cluster name (`kind`). To load into another KinD cluster, set `KIND_CLUSTER_NAME=my-other-cluster`. ================================================ FILE: docs/custom/main.html ================================================ {% extends "base.html" %} {% block site_meta %} {{ super() }} {% if page and page.meta and page.meta.ko_meta %} {% endif %} {% endblock %} ================================================ FILE: docs/custom/partials/copyright.html ================================================ The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage ================================================ FILE: docs/deployment.md ================================================ # Deployment _See [Kubernetes Integration](../features/k8s) for information about deploying to Kubernetes._ Because the output of `ko build` is an image reference, you can easily pass it to other tools that expect to take an image reference. ### [`docker run`](https://docs.docker.com/engine/reference/run/) To run the container locally: ```plaintext docker run -p 8080:8080 $(ko build ./cmd/app) ``` --- ### [Google Cloud Run](https://cloud.google.com/run) ```plaintext gcloud run deploy --image=$(ko build ./cmd/app) ``` > 💡 **Note:** The image must be pushed to [Google Container Registry](https://cloud.google.com/container-registry) or [Artifact Registry](https://cloud.google.com/artifact-registry). --- ### [fly.io](https://fly.io) ```plaintext flyctl launch --image=$(ko build ./cmd/app) ``` > 💡 **Note:** The image must be pushed to Fly.io's container registry at `registry.fly.io`, or if not, the image must be publicly available. When pushing to `registry.fly.io`, you must first log in with [`flyctl auth docker`](https://fly.io/docs/flyctl/auth-docker/). --- ### [AWS Lambda](https://aws.amazon.com/lambda/) ```plaintext aws lambda update-function-code \ --function-name=my-function-name \ --image-uri=$(ko build ./cmd/app) ``` > 💡 **Note:** The image must be pushed to [ECR](https://aws.amazon.com/ecr/), based on the AWS provided base image, and use the [`aws-lambda-go`](https://github.com/aws/aws-lambda-go) framework. See [official docs](https://docs.aws.amazon.com/lambda/latest/dg/go-image.html) for more information. --- ### [Azure Container Apps](https://azure.microsoft.com/services/container-apps/) ```plaintext az containerapp update \ --name my-container-app --resource-group my-resource-group --image $(ko build ./cmd/app) ``` > 💡 **Note:** The image must be pushed to [ACR](https://azure.microsoft.com/services/container-registry/) or other registry service. See [official docs](https://docs.microsoft.com/azure/container-apps/) for more information. ================================================ FILE: docs/features/build-cache.md ================================================ # Build Cache Because `ko` just runs `go build` in your normal development environment, it automatically reuses your [`go build` cache](https://pkg.go.dev/cmd/go#hdr-Build_and_test_caching) from previous builds, making iterative development faster. `ko` also avoids pushing blobs to the remote image registry if they're already present, making pushes faster. You can make `ko` even faster by setting the `KOCACHE` environment variable. This tells `ko` to store a local mapping between the `go build` inputs to the image layer that they produce, so `go build` can be skipped entirely if the layer is already present in the image registry. ================================================ FILE: docs/features/debugging.md ================================================ # Debugging Sometimes it's challenging to track down the cause of unexpected behavior in an app. Because `ko` makes it simple to make tweaks to your app and immediately rebuild your image, it's possible to iteratively explore various aspects of your app, such as by adding log lines that print variable values. But to help you solve the problem _as fast as possible_, `ko` supports debugging your Go app with [delve](https://github.com/go-delve/delve). To use this feature, just add the `--debug` flag to your `ko build` command. This adjusts how the image is built: - It installs `delve` in the image (in addition to your own app). - It sets the image's `ENTRYPOINT` to a `delve exec ...` command that runs the Go app in debug-mode, listening on port `40000` for a debugger client. - It ensures your compiled Go app includes debug symbols needed to enable debugging. **Note:** This feature is geared toward development workflows. It **should not** be used in production. ### How it works Build the image using the debug feature. ```plaintext ko build . --debug ``` Run the container, ensuring that the debug port (`40000`) is exposed to allow clients to connect to it. ```plaintext docker run -p 40000:40000 ``` This sets up your app to be waiting to run the command you've specified. All that's needed now is to connect your debugger client to the running container! ================================================ FILE: docs/features/k8s.md ================================================ # Kubernetes Integration You _could_ stop at just building and pushing images. But, because building images is so _easy_ with `ko`, and because building with `ko` only requires a string importpath to identify the image, we can integrate this with YAML generation to make Kubernetes use cases much simpler. ## YAML Changes Traditionally, you might have a Kubernetes deployment, defined in a YAML file, that runs an image: ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: my-deployment spec: selector: matchLabels: app: my-app template: metadata: labels: app: my-app spec: containers: - name: my-app image: registry.example.com/my-app:v1.2.3 ``` ...which you apply to your cluster with `kubectl apply`: ```plaintext kubectl apply -f deployment.yaml ``` With `ko`, you can instead reference your Go binary by its importpath, prefixed with `ko://`: ```yaml ... spec: containers: - name: my-app image: ko://github.com/my-user/my-repo/cmd/app ``` ## `ko resolve` With this small change, running `ko resolve -f deployment.yaml` will instruct `ko` to: 1. scan the YAML file(s) for values with the `ko://` prefix, 2. for each unique `ko://`-prefixed string, execute `ko build ` to build and push an image, 3. replace `ko://`-prefixed string(s) in the input YAML with the fully-specified image reference of the built image(s), as above. 4. Print the resulting resolved YAML to stdout. The result can be redirected to a file, to distribute to others: ```plaintext ko resolve -f config/ > release.yaml ``` Taken together, `ko resolve` aims to make packaging, pushing, and referencing container images an invisible implementation detail of your Kubernetes deployment, and let you focus on writing code in Go. ## `ko apply` To apply the resulting resolved YAML config, you can redirect the output of `ko resolve` to `kubectl apply`: ```plaintext ko resolve -f config/ | kubectl apply -f - ``` Since this is a relatively common use case, the same functionality is available using `ko apply`: ```plaintext ko apply -f config/ ``` Also, any flags passed after `--` are passed to `kubectl apply` directly, for example to specify context and kubeconfig: ``` ko apply -f config -- --context=foo --kubeconfig=cfg.yaml ``` **NB:** This requires that `kubectl` is available. ## `ko delete` To teardown resources applied using `ko apply`, you can run `ko delete`: ```plaintext ko delete -f config/ ``` This is purely a convenient alias for `kubectl delete`, and doesn't perform any builds, or delete any previously built images. ================================================ FILE: docs/features/multi-platform.md ================================================ # Multi-Platform Images Because Go supports cross-compilation to other CPU architectures and operating systems, `ko` excels at producing multi-platform images. To build and push an image for all platforms supported by the configured base image, simply add `--platform=all`. This will instruct `ko` to look up all the supported platforms in the base image, execute `GOOS= GOARCH= GOARM= go build` for each platform, and produce a manifest list containing an image for each platform. You can also select specific platforms, for example, `--platform=linux/amd64,linux/arm64`. `ko` also has experimental support for building for Windows images. See [FAQ](../../advanced/faq#can-i-build-windows-containers). ================================================ FILE: docs/features/sboms.md ================================================ # SBOMs A [Software Bill of Materials (SBOM)](https://en.wikipedia.org/wiki/Software_bill_of_materials) is a list of software components that a software artifact depends on. Having a list of dependencies can be helpful in determining whether any vulnerable components were used to build the software artifact. **From v0.9+, `ko` generates and uploads an SBOM for every image it produces by default.** ko will generate an SBOM in the [SPDX](https://spdx.dev/) format by default. To disable SBOM generation, pass `--sbom=none`. These SBOMs can be downloaded using the [`cosign download sbom`](https://github.com/sigstore/cosign/blob/main/doc/cosign_download_sbom.md) command. ================================================ FILE: docs/features/static-assets.md ================================================ # Static Assets `ko` can also bundle static assets into the images it produces. By convention, any contents of a directory named `/kodata/` will be bundled into the image, and the path where it's available in the image will be identified by the environment variable `KO_DATA_PATH`. As an example, you can bundle and serve static contents in your image: ``` cmd/ app/ main.go kodata/ favicon.ico index.html ``` Then, in your `main.go`: ```go func main() { http.Handle("/", http.FileServer(http.Dir(os.Getenv("KO_DATA_PATH")))) log.Fatal(http.ListenAndServe(":8080", nil)) } ``` You can simulate `ko`'s behavior outside of the container image by setting the `KO_DATA_PATH` environment variable yourself with `KO_DATA_PATH=cmd/app/kodata/ go run ./cmd/app`. > 💡 **Tip:** Symlinks in `kodata` are followed and included as well. For example, you can include Git commit information in your image with `ln -s -r .git/HEAD ./cmd/app/kodata/` Also note that `http.FileServer` will not serve the `Last-Modified` header (or validate `If-Modified-Since` request headers) because `ko` does not embed timestamps by default. This can be supported by manually setting the `KO_DATA_DATE_EPOCH` environment variable during build ([See FAQ](../../advanced/faq#why-are-my-images-all-created-in-1970)). ================================================ FILE: docs/get-started.md ================================================ # Get Started ## Setup First, [install `ko`](../install). ### Authenticate `ko` depends on the authentication configured in your Docker config (typically `~/.docker/config.json`). ✨ **If you can push an image with `docker push`, you are already authenticated for `ko`!** ✨ Since `ko` doesn't require `docker`, `ko login` also provides a surface for logging in to a container image registry with a username and password, similar to [`docker login`](https://docs.docker.com/engine/reference/commandline/login/). Additionally, even if auth is not configured in the Docker config, `ko` includes built-in support for authenticating to the following container registries using credentials configured in the environment: - Google Container Registry and Artifact Registry, using [Application Default Credentials](https://cloud.google.com/docs/authentication/production) or auth configured in `gcloud`. - Amazon Elastic Container Registry, using [AWS credentials](https://github.com/awslabs/amazon-ecr-credential-helper/#aws-credentials) - Azure Container Registry, using [environment variables](https://github.com/chrismellard/docker-credential-acr-env/) - GitHub Container Registry, using the `GITHUB_TOKEN` environment variable ### Choose Destination `ko` depends on an environment variable, `KO_DOCKER_REPO`, to identify where it should push images that it builds. Typically this will be a remote registry, e.g.: - `KO_DOCKER_REPO=gcr.io/my-project`, or - `KO_DOCKER_REPO=ghcr.io/my-org/my-repo`, or - `KO_DOCKER_REPO=my-dockerhub-user` ## Build an Image `ko build ./cmd/app` builds and pushes a container image, and prints the resulting image digest to stdout. In this example, `./cmd/app` must be a `package main` that defines `func main()`. ```plaintext $ ko build ./cmd/app ... registry.example.com/my-project/app-099ba5bcefdead87f92606265fb99ac0@sha256:6e398316742b7aa4a93161dce4a23bc5c545700b862b43347b941000b112ec3e ``` > 💡 **Note**: Prior to v0.10, the command was called `ko publish` -- this is equivalent to `ko build`, and both commands will work and do the same thing. The executable binary that was built from `./cmd/app` is available in the image at `/ko-app/app` -- the binary name matches the base import path name -- and that binary is the image's entrypoint. ================================================ FILE: docs/index.md ================================================ --- ko_meta: true --- # Introduction `ko` makes building Go container images easy, fast, and secure by default. ![Demo of ko build](./images/demo.png) `ko` is a simple, fast container image builder for Go applications. It's ideal for use cases where your image contains a single Go application without many dependencies on the OS base image (e.g., no cgo, no OS package dependencies). `ko` builds images by executing `go build` on your local machine, and as such doesn't require `docker` to be installed. This can make it a good fit for lightweight CI/CD use cases. `ko` makes [multi-platform builds](https://ko.build/features/multi-platform/) easy, produces [SBOMs](https://ko.build/features/sboms/) by default, and includes support for simple YAML templating which makes it a powerful tool for [Kubernetes applications](https://ko.build/features/k8s/). --- > 🏃 [Install `ko`](./install) and [get started](./get-started)! --- `ko` is used and loved by these open source projects: - [Knative](https://knative.dev) - [Tekton](https://tekton.dev) - [Karpenter](https://karpenter.sh) - [Kyverno](https://kyverno.io) - [Sigstore](https://sigstore.dev) - [Shipwright](https://shipwright.io) - [Capsule](https://capsule.clastix.io/) - [CloudScript](https://cloudscript.com.br/) - [Kamaji](https://kamaji.clastix.io/) [_Add your project here!_](https://github.com/ko-build/ko/edit/main/docs/index.md) --- `ko` is a Cloud Native Computing Foundation Sandbox project. CNCF logo CNCF logo ================================================ FILE: docs/install.md ================================================ # Installation ### Install from [GitHub Releases](https://github.com/ko-build/ko/releases) ``` $ VERSION=TODO # choose the latest version (without v prefix) $ OS=Linux # or Darwin $ ARCH=x86_64 # or arm64, i386, s390x ``` We generate [SLSA3 provenance](https://slsa.dev) using the OpenSSF's [slsa-framework/slsa-github-generator](https://github.com/slsa-framework/slsa-github-generator). To verify our release, install the verification tool from [slsa-framework/slsa-verifier#installation](https://github.com/slsa-framework/slsa-verifier#installation) and verify as follows: ```shell $ curl -sSfL "https://github.com/ko-build/ko/releases/download/v${VERSION}/ko_${VERSION}_${OS}_${ARCH}.tar.gz" > ko.tar.gz $ curl -sSfL https://github.com/ko-build/ko/releases/download/v${VERSION}/multiple.intoto.jsonl > multiple.intoto.jsonl $ slsa-verifier verify-artifact --provenance-path multiple.intoto.jsonl --source-uri github.com/ko-build/ko --source-tag "v${VERSION}" ko.tar.gz Verified signature against tlog entry index 24413745 at URL: https://rekor.sigstore.dev/api/v1/log/entries/24296fb24b8ad77ab97a5263b5fa8f35789618348a39358b1f9470b0c31045effbbe5e23e77a5836 Verified build using builder "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@refs/tags/v1.7.0" at commit 200db7243f02b5c0303e21d8ab8e3b4ad3a229d0 Verifying artifact /Users/batuhanapaydin/workspace/ko/ko.tar.gz: PASSED PASSED: Verified SLSA provenance ``` ```shell $ tar xzf ko.tar.gz ko $ chmod +x ./ko ``` ### Install using [Homebrew](https://brew.sh) ```plaintext brew install ko ``` ### Install using [MacPorts](https://www.macports.org) ```plaintext sudo port install ko ``` More info [here](https://ports.macports.org/port/ko/) ### Install on Windows using [Scoop](https://scoop.sh) ```plaintext scoop install ko ``` ### Install on [Alpine Linux](https://www.alpinelinux.org) Installation on Alpine requires using the [`testing` repository](https://wiki.alpinelinux.org/wiki/Enable_Community_Repository#Using_testing_repositories) ``` echo https://dl-cdn.alpinelinux.org/alpine/edge/testing/ >> /etc/apk/repositories apk update apk add ko ``` ### Build and Install from source With Go 1.16+, build and install the latest released version: ```plaintext go install github.com/google/ko@latest ``` ### Setup on GitHub Actions You can use the [setup-ko](https://github.com/ko-build/setup-ko) action to install ko and setup auth to [GitHub Container Registry](https://github.com/features/packages) in a GitHub Action workflow: ```plaintext steps: - uses: ko-build/setup-ko@v0.6 ``` ================================================ FILE: docs/reference/ko.md ================================================ ## ko Rapidly iterate with Go, Containers, and Kubernetes. ``` ko [flags] ``` ### Options ``` -h, --help help for ko -v, --verbose Enable debug logs ``` ### SEE ALSO * [ko apply](ko_apply.md) - Apply the input files with image references resolved to built/pushed image digests. * [ko build](ko_build.md) - Build and publish container images from the given importpaths. * [ko create](ko_create.md) - Create the input files with image references resolved to built/pushed image digests. * [ko delete](ko_delete.md) - See "kubectl help delete" for detailed usage. * [ko login](ko_login.md) - Log in to a registry * [ko resolve](ko_resolve.md) - Print the input files with image references resolved to built/pushed image digests. * [ko run](ko_run.md) - A variant of `kubectl run` that containerizes IMPORTPATH first. * [ko version](ko_version.md) - Print ko version. ================================================ FILE: docs/reference/ko_apply.md ================================================ ## ko apply Apply the input files with image references resolved to built/pushed image digests. ### Synopsis This sub-command finds import path references within the provided files, builds them into Go binaries, containerizes them, publishes them, and then feeds the resulting yaml into "kubectl apply". ``` ko apply -f FILENAME [flags] ``` ### Examples ``` # Build and publish import path references to a Docker # Registry as: # ${KO_DOCKER_REPO}/- # Then, feed the resulting yaml into "kubectl apply". # When KO_DOCKER_REPO is ko.local, it is the same as if # --local was passed. ko apply -f config/ # Build and publish import path references to a Docker # Registry preserving import path names as: # ${KO_DOCKER_REPO}/ # Then, feed the resulting yaml into "kubectl apply". ko apply --preserve-import-paths -f config/ # Build and publish import path references to a Docker # daemon as: # ko.local/ # Then, feed the resulting yaml into "kubectl apply". ko apply --local -f config/ # Apply from stdin: cat config.yaml | ko apply -f - # Any flags passed after '--' are passed to 'kubectl apply' directly: ko apply -f config -- --namespace=foo --kubeconfig=cfg.yaml ``` ### Options ``` --bare Whether to just use KO_DOCKER_REPO without additional context (may not work properly with --tags). -B, --base-import-paths Whether to use the base path without MD5 hash after KO_DOCKER_REPO (may not work properly with --tags). --debug Include Delve debugger into image and wrap around ko-app. This debugger will listen to port 40000. --disable-optimizations Disable optimizations when building Go code. Useful when you want to interactively debug the created container. -f, --filename strings Filename, directory, or URL to files to use to create the resource -h, --help help for apply --image-annotation strings Which annotations (key=value[,key=value]) to add to the OCI manifest. --image-label strings Which labels (key=value[,key=value]) to add to the image. --image-refs string Path to file where a list of the published image references will be written. --image-user string The default user the image should be run as. --insecure-registry Whether to skip TLS verification on the registry -j, --jobs int The maximum number of concurrent builds (default GOMAXPROCS) -L, --local Load into images to local docker daemon. --oci-layout-path string Path to save the OCI image layout of the built images --platform strings Which platform to use when pulling a multi-platform base. Format: all | [/[/]][,platform]* -P, --preserve-import-paths Whether to preserve the full import path after KO_DOCKER_REPO. --push Push images to KO_DOCKER_REPO (default true) -R, --recursive Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory. --sbom string The SBOM media type to use (none will disable SBOM synthesis and upload). (default "spdx") --sbom-dir string Path to file where the SBOM will be written. -l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2) --tag-only Include tags but not digests in resolved image references. Useful when digests are not preserved when images are repopulated. -t, --tags strings Which tags to use for the produced image instead of the default 'latest' tag (may not work properly with --base-import-paths or --bare). (default [latest]) --tarball string File to save images tarballs ``` ### Options inherited from parent commands ``` -v, --verbose Enable debug logs ``` ### SEE ALSO * [ko](ko.md) - Rapidly iterate with Go, Containers, and Kubernetes. ================================================ FILE: docs/reference/ko_build.md ================================================ ## ko build Build and publish container images from the given importpaths. ### Synopsis This sub-command builds the provided import paths into Go binaries, containerizes them, and publishes them. ``` ko build IMPORTPATH... [flags] ``` ### Examples ``` # Build and publish import path references to a Docker Registry as: # ${KO_DOCKER_REPO}/- # When KO_DOCKER_REPO is ko.local, it is the same as if --local and # --preserve-import-paths were passed. # If the import path is not provided, the current working directory is the # default. ko build github.com/foo/bar/cmd/baz github.com/foo/bar/cmd/blah # Build and publish a relative import path as: # ${KO_DOCKER_REPO}/- # When KO_DOCKER_REPO is ko.local, it is the same as if --local and # --preserve-import-paths were passed. ko build ./cmd/blah # Build and publish a relative import path as: # ${KO_DOCKER_REPO}/ # When KO_DOCKER_REPO is ko.local, it is the same as if --local was passed. ko build --preserve-import-paths ./cmd/blah # Build and publish import path references to a Docker daemon as: # ko.local/ # This always preserves import paths. ko build --local github.com/foo/bar/cmd/baz github.com/foo/bar/cmd/blah ``` ### Options ``` --bare Whether to just use KO_DOCKER_REPO without additional context (may not work properly with --tags). -B, --base-import-paths Whether to use the base path without MD5 hash after KO_DOCKER_REPO (may not work properly with --tags). --debug Include Delve debugger into image and wrap around ko-app. This debugger will listen to port 40000. --disable-optimizations Disable optimizations when building Go code. Useful when you want to interactively debug the created container. -h, --help help for build --image-annotation strings Which annotations (key=value[,key=value]) to add to the OCI manifest. --image-label strings Which labels (key=value[,key=value]) to add to the image. --image-refs string Path to file where a list of the published image references will be written. --image-user string The default user the image should be run as. --insecure-registry Whether to skip TLS verification on the registry -j, --jobs int The maximum number of concurrent builds (default GOMAXPROCS) -L, --local Load into images to local docker daemon. --oci-layout-path string Path to save the OCI image layout of the built images --platform strings Which platform to use when pulling a multi-platform base. Format: all | [/[/]][,platform]* -P, --preserve-import-paths Whether to preserve the full import path after KO_DOCKER_REPO. --push Push images to KO_DOCKER_REPO (default true) --sbom string The SBOM media type to use (none will disable SBOM synthesis and upload). (default "spdx") --sbom-dir string Path to file where the SBOM will be written. --tag-only Include tags but not digests in resolved image references. Useful when digests are not preserved when images are repopulated. -t, --tags strings Which tags to use for the produced image instead of the default 'latest' tag (may not work properly with --base-import-paths or --bare). (default [latest]) --tarball string File to save images tarballs ``` ### Options inherited from parent commands ``` -v, --verbose Enable debug logs ``` ### SEE ALSO * [ko](ko.md) - Rapidly iterate with Go, Containers, and Kubernetes. ================================================ FILE: docs/reference/ko_create.md ================================================ ## ko create Create the input files with image references resolved to built/pushed image digests. ### Synopsis This sub-command finds import path references within the provided files, builds them into Go binaries, containerizes them, publishes them, and then feeds the resulting yaml into "kubectl create". ``` ko create -f FILENAME [flags] ``` ### Examples ``` # Build and publish import path references to a Docker # Registry as: # ${KO_DOCKER_REPO}/- # Then, feed the resulting yaml into "kubectl create". # When KO_DOCKER_REPO is ko.local, it is the same as if # --local was passed. ko create -f config/ # Build and publish import path references to a Docker # Registry preserving import path names as: # ${KO_DOCKER_REPO}/ # Then, feed the resulting yaml into "kubectl create". ko create --preserve-import-paths -f config/ # Build and publish import path references to a Docker # daemon as: # ko.local/ # Then, feed the resulting yaml into "kubectl create". ko create --local -f config/ # Create from stdin: cat config.yaml | ko create -f - # Any flags passed after '--' are passed to 'kubectl apply' directly: ko apply -f config -- --namespace=foo --kubeconfig=cfg.yaml ``` ### Options ``` --bare Whether to just use KO_DOCKER_REPO without additional context (may not work properly with --tags). -B, --base-import-paths Whether to use the base path without MD5 hash after KO_DOCKER_REPO (may not work properly with --tags). --debug Include Delve debugger into image and wrap around ko-app. This debugger will listen to port 40000. --disable-optimizations Disable optimizations when building Go code. Useful when you want to interactively debug the created container. -f, --filename strings Filename, directory, or URL to files to use to create the resource -h, --help help for create --image-annotation strings Which annotations (key=value[,key=value]) to add to the OCI manifest. --image-label strings Which labels (key=value[,key=value]) to add to the image. --image-refs string Path to file where a list of the published image references will be written. --image-user string The default user the image should be run as. --insecure-registry Whether to skip TLS verification on the registry -j, --jobs int The maximum number of concurrent builds (default GOMAXPROCS) -L, --local Load into images to local docker daemon. --oci-layout-path string Path to save the OCI image layout of the built images --platform strings Which platform to use when pulling a multi-platform base. Format: all | [/[/]][,platform]* -P, --preserve-import-paths Whether to preserve the full import path after KO_DOCKER_REPO. --push Push images to KO_DOCKER_REPO (default true) -R, --recursive Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory. --sbom string The SBOM media type to use (none will disable SBOM synthesis and upload). (default "spdx") --sbom-dir string Path to file where the SBOM will be written. -l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2) --tag-only Include tags but not digests in resolved image references. Useful when digests are not preserved when images are repopulated. -t, --tags strings Which tags to use for the produced image instead of the default 'latest' tag (may not work properly with --base-import-paths or --bare). (default [latest]) --tarball string File to save images tarballs ``` ### Options inherited from parent commands ``` -v, --verbose Enable debug logs ``` ### SEE ALSO * [ko](ko.md) - Rapidly iterate with Go, Containers, and Kubernetes. ================================================ FILE: docs/reference/ko_delete.md ================================================ ## ko delete See "kubectl help delete" for detailed usage. ``` ko delete [flags] ``` ### Options ``` -h, --help help for delete ``` ### Options inherited from parent commands ``` -v, --verbose Enable debug logs ``` ### SEE ALSO * [ko](ko.md) - Rapidly iterate with Go, Containers, and Kubernetes. ================================================ FILE: docs/reference/ko_login.md ================================================ ## ko login Log in to a registry ``` ko login [OPTIONS] [SERVER] [flags] ``` ### Examples ``` # Log in to reg.example.com ko login reg.example.com -u AzureDiamond -p hunter2 ``` ### Options ``` -h, --help help for login -p, --password string Password --password-stdin Take the password from stdin -u, --username string Username ``` ### Options inherited from parent commands ``` -v, --verbose Enable debug logs ``` ### SEE ALSO * [ko](ko.md) - Rapidly iterate with Go, Containers, and Kubernetes. ================================================ FILE: docs/reference/ko_resolve.md ================================================ ## ko resolve Print the input files with image references resolved to built/pushed image digests. ### Synopsis This sub-command finds import path references within the provided files, builds them into Go binaries, containerizes them, publishes them, and prints the resulting yaml. ``` ko resolve -f FILENAME [flags] ``` ### Examples ``` # Build and publish import path references to a Docker # Registry as: # ${KO_DOCKER_REPO}/- # When KO_DOCKER_REPO is ko.local, it is the same as if # --local and --preserve-import-paths were passed. ko resolve -f config/ # Build and publish import path references to a Docker # Registry preserving import path names as: # ${KO_DOCKER_REPO}/ # When KO_DOCKER_REPO is ko.local, it is the same as if # --local was passed. ko resolve --preserve-import-paths -f config/ # Build and publish import path references to a Docker # daemon as: # ko.local/ # This always preserves import paths. ko resolve --local -f config/ ``` ### Options ``` --bare Whether to just use KO_DOCKER_REPO without additional context (may not work properly with --tags). -B, --base-import-paths Whether to use the base path without MD5 hash after KO_DOCKER_REPO (may not work properly with --tags). --debug Include Delve debugger into image and wrap around ko-app. This debugger will listen to port 40000. --disable-optimizations Disable optimizations when building Go code. Useful when you want to interactively debug the created container. -f, --filename strings Filename, directory, or URL to files to use to create the resource -h, --help help for resolve --image-annotation strings Which annotations (key=value[,key=value]) to add to the OCI manifest. --image-label strings Which labels (key=value[,key=value]) to add to the image. --image-refs string Path to file where a list of the published image references will be written. --image-user string The default user the image should be run as. --insecure-registry Whether to skip TLS verification on the registry -j, --jobs int The maximum number of concurrent builds (default GOMAXPROCS) -L, --local Load into images to local docker daemon. --oci-layout-path string Path to save the OCI image layout of the built images --platform strings Which platform to use when pulling a multi-platform base. Format: all | [/[/]][,platform]* -P, --preserve-import-paths Whether to preserve the full import path after KO_DOCKER_REPO. --push Push images to KO_DOCKER_REPO (default true) -R, --recursive Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory. --sbom string The SBOM media type to use (none will disable SBOM synthesis and upload). (default "spdx") --sbom-dir string Path to file where the SBOM will be written. -l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2) --tag-only Include tags but not digests in resolved image references. Useful when digests are not preserved when images are repopulated. -t, --tags strings Which tags to use for the produced image instead of the default 'latest' tag (may not work properly with --base-import-paths or --bare). (default [latest]) --tarball string File to save images tarballs ``` ### Options inherited from parent commands ``` -v, --verbose Enable debug logs ``` ### SEE ALSO * [ko](ko.md) - Rapidly iterate with Go, Containers, and Kubernetes. ================================================ FILE: docs/reference/ko_run.md ================================================ ## ko run A variant of `kubectl run` that containerizes IMPORTPATH first. ### Synopsis This sub-command combines "ko build" and "kubectl run" to support containerizing and running Go binaries on Kubernetes in a single command. ``` ko run IMPORTPATH [flags] ``` ### Examples ``` # Publish the image and run it on Kubernetes as: # ${KO_DOCKER_REPO}/- # When KO_DOCKER_REPO is ko.local, it is the same as if # --local and --preserve-import-paths were passed. ko run github.com/foo/bar/cmd/baz # This supports relative import paths as well. ko run ./cmd/baz # You can also supply args and flags to the command. ko run ./cmd/baz -- -v arg1 arg2 --yes ``` ### Options ``` --bare Whether to just use KO_DOCKER_REPO without additional context (may not work properly with --tags). -B, --base-import-paths Whether to use the base path without MD5 hash after KO_DOCKER_REPO (may not work properly with --tags). --debug Include Delve debugger into image and wrap around ko-app. This debugger will listen to port 40000. --disable-optimizations Disable optimizations when building Go code. Useful when you want to interactively debug the created container. -h, --help help for run --image-annotation strings Which annotations (key=value[,key=value]) to add to the OCI manifest. --image-label strings Which labels (key=value[,key=value]) to add to the image. --image-refs string Path to file where a list of the published image references will be written. --image-user string The default user the image should be run as. --insecure-registry Whether to skip TLS verification on the registry -j, --jobs int The maximum number of concurrent builds (default GOMAXPROCS) -L, --local Load into images to local docker daemon. --oci-layout-path string Path to save the OCI image layout of the built images --platform strings Which platform to use when pulling a multi-platform base. Format: all | [/[/]][,platform]* -P, --preserve-import-paths Whether to preserve the full import path after KO_DOCKER_REPO. --push Push images to KO_DOCKER_REPO (default true) --sbom string The SBOM media type to use (none will disable SBOM synthesis and upload). (default "spdx") --sbom-dir string Path to file where the SBOM will be written. --tag-only Include tags but not digests in resolved image references. Useful when digests are not preserved when images are repopulated. -t, --tags strings Which tags to use for the produced image instead of the default 'latest' tag (may not work properly with --base-import-paths or --bare). (default [latest]) --tarball string File to save images tarballs ``` ### Options inherited from parent commands ``` -v, --verbose Enable debug logs ``` ### SEE ALSO * [ko](ko.md) - Rapidly iterate with Go, Containers, and Kubernetes. ================================================ FILE: docs/reference/ko_version.md ================================================ ## ko version Print ko version. ``` ko version [flags] ``` ### Options ``` -h, --help help for version ``` ### Options inherited from parent commands ``` -v, --verbose Enable debug logs ``` ### SEE ALSO * [ko](ko.md) - Rapidly iterate with Go, Containers, and Kubernetes. ================================================ FILE: go.mod ================================================ module github.com/google/ko go 1.25.7 require ( github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.12.0 github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 github.com/dprotaso/go-yit v0.0.0-20260209000607-dfb86291624d github.com/go-training/helloworld v0.0.0-20200225145412-ba5f4379d78b github.com/go-viper/mapstructure/v2 v2.5.0 github.com/google/go-cmp v0.7.0 github.com/google/go-containerregistry v0.21.3 github.com/moby/moby/api v1.54.0 github.com/moby/moby/client v0.3.0 github.com/opencontainers/image-spec v1.1.1 github.com/sigstore/cosign/v3 v3.0.5 github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 go.uber.org/automaxprocs v1.6.0 go.yaml.in/yaml/v4 v4.0.0-rc.4 golang.org/x/sync v0.20.0 golang.org/x/tools v0.43.0 k8s.io/apimachinery v0.35.3 sigs.k8s.io/kind v0.31.0 ) require ( al.essio.dev/pkg/shellescape v1.6.0 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest v0.11.30 // indirect github.com/Azure/go-autorest/autorest/adal v0.9.24 // indirect github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 // indirect github.com/Azure/go-autorest/autorest/azure/cli v0.4.7 // indirect github.com/Azure/go-autorest/autorest/date v0.3.1 // indirect github.com/Azure/go-autorest/logger v0.2.2 // indirect github.com/Azure/go-autorest/tracing v0.6.1 // indirect github.com/BurntSushi/toml v1.6.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go-v2 v1.41.3 // indirect github.com/aws/aws-sdk-go-v2/config v1.32.11 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.11 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect github.com/aws/aws-sdk-go-v2/service/ecr v1.56.0 // indirect github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.38.11 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect github.com/aws/smithy-go v1.24.2 // indirect github.com/blang/semver v3.5.1+incompatible // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect github.com/coreos/go-oidc/v3 v3.17.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/digitorus/pkcs7 v0.0.0-20250730155240-ffadbf3f398c // indirect github.com/digitorus/timestamp v0.0.0-20250524132541-c45532741eea // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/cli v29.3.0+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.5 // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.24.3 // indirect github.com/go-openapi/errors v0.22.7 // indirect github.com/go-openapi/jsonpointer v0.22.5 // indirect github.com/go-openapi/jsonreference v0.21.5 // indirect github.com/go-openapi/loads v0.23.3 // indirect github.com/go-openapi/runtime v0.29.3 // indirect github.com/go-openapi/spec v0.22.4 // indirect github.com/go-openapi/strfmt v0.26.0 // indirect github.com/go-openapi/swag v0.25.5 // indirect github.com/go-openapi/swag/cmdutils v0.25.5 // indirect github.com/go-openapi/swag/conv v0.25.5 // indirect github.com/go-openapi/swag/fileutils v0.25.5 // indirect github.com/go-openapi/swag/jsonname v0.25.5 // indirect github.com/go-openapi/swag/jsonutils v0.25.5 // indirect github.com/go-openapi/swag/loading v0.25.5 // indirect github.com/go-openapi/swag/mangling v0.25.5 // indirect github.com/go-openapi/swag/netutils v0.25.5 // indirect github.com/go-openapi/swag/stringutils v0.25.5 // indirect github.com/go-openapi/swag/typeutils v0.25.5 // indirect github.com/go-openapi/swag/yamlutils v0.25.5 // indirect github.com/go-openapi/validate v0.25.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/google/certificate-transparency-go v1.3.3 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/in-toto/attestation v1.1.2 // indirect github.com/in-toto/in-toto-golang v0.10.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7 // indirect github.com/klauspost/compress v1.18.4 // indirect github.com/letsencrypt/boulder v0.20251208.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/term v0.5.2 // indirect github.com/oklog/ulid/v2 v2.1.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/sassoftware/relic v7.2.1+incompatible // indirect github.com/secure-systems-lab/go-securesystemslib v0.10.0 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/sigstore/protobuf-specs v0.5.0 // indirect github.com/sigstore/rekor v1.5.1 // indirect github.com/sigstore/rekor-tiles/v2 v2.2.1 // indirect github.com/sigstore/sigstore v1.10.4 // indirect github.com/sigstore/sigstore-go v1.1.4 // indirect github.com/sigstore/timestamp-authority/v2 v2.0.5 // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/theupdateframework/go-tuf v0.7.0 // indirect github.com/theupdateframework/go-tuf/v2 v2.4.1 // indirect github.com/transparency-dev/formats v0.1.0 // indirect github.com/transparency-dev/merkle v0.0.2 // indirect github.com/vbatts/tar-split v0.12.2 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect go.opentelemetry.io/otel v1.42.0 // indirect go.opentelemetry.io/otel/metric v1.42.0 // indirect go.opentelemetry.io/otel/trace v1.42.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.49.0 // indirect golang.org/x/mod v0.34.0 // indirect golang.org/x/net v0.52.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/term v0.41.0 // indirect golang.org/x/text v0.35.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.140.0 // indirect k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) ================================================ FILE: go.sum ================================================ al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA= al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= cloud.google.com/go/kms v1.25.0 h1:gVqvGGUmz0nYCmtoxWmdc1wli2L1apgP8U4fghPGSbQ= cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk= cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg= github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA= github.com/Azure/go-autorest/autorest v0.11.30 h1:iaZ1RGz/ALZtN5eq4Nr1SOFSlf2E4pDI3Tcsl+dZPVE= github.com/Azure/go-autorest/autorest v0.11.30/go.mod h1:t1kpPIOpIVX7annvothKvb0stsrXa37i7b+xpmBW8Fs= github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= github.com/Azure/go-autorest/autorest/adal v0.9.24 h1:BHZfgGsGwdkHDyZdtQRQk1WeUdW0m2WPAwuHZwUi5i4= github.com/Azure/go-autorest/autorest/adal v0.9.24/go.mod h1:7T1+g0PYFmACYW5LlG2fcoPiPlFHjClyRGL7dRlP5c8= github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 h1:Ov8avRZi2vmrE2JcXw+tu5K/yB41r7xK9GZDiBF7NdM= github.com/Azure/go-autorest/autorest/azure/auth v0.5.13/go.mod h1:5BAVfWLWXihP47vYrPuBKKf4cS0bXI+KM9Qx6ETDJYo= github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0= github.com/Azure/go-autorest/autorest/azure/cli v0.4.7 h1:Q9R3utmFg9K1B4OYtAZ7ZUUvIUdzQt7G2MN5Hi/d670= github.com/Azure/go-autorest/autorest/azure/cli v0.4.7/go.mod h1:bVrAueELJ0CKLBpUHDIvD516TwmHmzqwCpvONWRsw3s= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/autorest/date v0.3.1 h1:o9Z8Jyt+VJJTCZ/UORishuHOusBwolhjokt9s5k8I4w= github.com/Azure/go-autorest/autorest/date v0.3.1/go.mod h1:Dz/RDmXlfiFFS/eW+b/xMUSFs1tboPVy6UjgADToWDM= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/logger v0.2.2 h1:hYqBsEBywrrOSW24kkOCXRcKfKhK76OzLTfF+MYDE2o= github.com/Azure/go-autorest/logger v0.2.2/go.mod h1:I5fg9K52o+iuydlWfa9T5K6WFos9XYr9dYTFzpqgibw= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/Azure/go-autorest/tracing v0.6.1 h1:YUMSrC/CeD1ZnnXcNYU4a/fzsO35u2Fsful9L/2nyR0= github.com/Azure/go-autorest/tracing v0.6.1/go.mod h1:/3EgjbsjraOqiicERAeu3m7/z0x1TzjQGAwDrJrXGkc= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA= github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs= github.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o44w/Thk9+8YIlzWMmo= github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc= github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= github.com/aws/aws-sdk-go-v2/service/ecr v1.56.0 h1:XxNya31nOtsClGghvQ2VkhIB2S/rggb64x5vkHl4xZQ= github.com/aws/aws-sdk-go-v2/service/ecr v1.56.0/go.mod h1:T+Tz2Xp1gnvtlgvP7OyRHlr84KtI3fZW5Ax/e+s9b64= github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.38.11 h1:2T9NCuNzzBh6RUrwYZBFl1D9lLJ2r2CCbg7w383DjQE= github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.38.11/go.mod h1:FkD34cqOmnqfAEiNHeqOT50SoXqHEgdDsa8BrMw9t+w= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA= github.com/aws/aws-sdk-go-v2/service/kms v1.49.5 h1:DKibav4XF66XSeaXcrn9GlWGHos6D/vJ4r7jsK7z5CE= github.com/aws/aws-sdk-go-v2/service/kms v1.49.5/go.mod h1:1SdcmEGUEQE1mrU2sIgeHtcMSxHuybhPvuEPANzIDfI= github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row= github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU= github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias= github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8= github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo= github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI= github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.12.0 h1:JFWXO6QPihCknDdnL6VaQE57km4ZKheHIGd9YiOGcTo= github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.12.0/go.mod h1:046/oLyFlYdAghYQE2yHXi/E//VM5Cf3/dFmA+3CZ0c= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 h1:krfRl01rzPzxSxyLyrChD+U+MzsBXbm0OwYYB67uF+4= github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589/go.mod h1:OuDyvmLnMCwa2ep4Jkm6nyA0ocJuZlGyk2gGseVzERM= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/digitorus/pkcs7 v0.0.0-20230713084857-e76b763bdc49/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= github.com/digitorus/pkcs7 v0.0.0-20250730155240-ffadbf3f398c h1:g349iS+CtAvba7i0Ee9EP1TlTZ9w+UncBY6HSmsFZa0= github.com/digitorus/pkcs7 v0.0.0-20250730155240-ffadbf3f398c/go.mod h1:mCGGmWkOQvEuLdIRfPIpXViBfpWto4AhwtJlAvo62SQ= github.com/digitorus/timestamp v0.0.0-20250524132541-c45532741eea h1:ALRwvjsSP53QmnN3Bcj0NpR8SsFLnskny/EIMebAk1c= github.com/digitorus/timestamp v0.0.0-20250524132541-c45532741eea/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/cli v29.3.0+incompatible h1:z3iWveU7h19Pqx7alZES8j+IeFQZ1lhTwb2F+V9SVvk= github.com/docker/cli v29.3.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY= github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dprotaso/go-yit v0.0.0-20260209000607-dfb86291624d h1:/USl0X37Afc2SyjRG4/eNrbm4CZRfZLdzwTy9YXxowA= github.com/dprotaso/go-yit v0.0.0-20260209000607-dfb86291624d/go.mod h1:k03zg0AFMepR2TrssNeMUISoI0QcX2N58Sl0qPU6MZs= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/analysis v0.24.3 h1:a1hrvMr8X0Xt69KP5uVTu5jH62DscmDifrLzNglAayk= github.com/go-openapi/analysis v0.24.3/go.mod h1:Nc+dWJ/FxZbhSow5Yh3ozg5CLJioB+XXT6MdLvJUsUw= github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ= github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA= github.com/go-openapi/runtime v0.29.3 h1:h5twGaEqxtQg40ePiYm9vFFH1q06Czd7Ot6ufdK0w/Y= github.com/go-openapi/runtime v0.29.3/go.mod h1:8A1W0/L5eyNJvKciqZtvIVQvYO66NlB7INMSZ9bw/oI= github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= github.com/go-openapi/strfmt v0.26.0 h1:SDdQLyOEqu8W96rO1FRG1fuCtVyzmukky0zcD6gMGLU= github.com/go-openapi/strfmt v0.26.0/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y= github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU= github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA= github.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c= github.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk= github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc= github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U= github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo= github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw= github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY= github.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU= github.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14= github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= github.com/go-openapi/testify/enable/yaml/v2 v2.4.1 h1:NZOrZmIb6PTv5LTFxr5/mKV/FjbUzGE7E6gLz7vFoOQ= github.com/go-openapi/testify/enable/yaml/v2 v2.4.1/go.mod h1:r7dwsujEHawapMsxA69i+XMGZrQ5tRauhLAjV/sxg3Q= github.com/go-openapi/testify/v2 v2.4.1 h1:zB34HDKj4tHwyUQHrUkpV0Q0iXQ6dUCOQtIqn8hE6Iw= github.com/go-openapi/testify/v2 v2.4.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0= github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY= github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA= github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-training/helloworld v0.0.0-20200225145412-ba5f4379d78b h1:0pOrjn0UzTcHdhDVdxrH8LwM7QLnAp8qiUtwXM04JEE= github.com/go-training/helloworld v0.0.0-20200225145412-ba5f4379d78b/go.mod h1:hGGmX3bRUkYkc9aKA6mkUxi6d+f1GmZF1je0FlVTgwU= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/certificate-transparency-go v1.3.3 h1:hq/rSxztSkXN2tx/3jQqF6Xc0O565UQPdHrOWvZwybo= github.com/google/certificate-transparency-go v1.3.3/go.mod h1:iR17ZgSaXRzSa5qvjFl8TnVD5h8ky2JMVio+dzoKMgA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.21.3 h1:Xr+yt3VvwOOn/5nJzd7UoOhwPGiPkYW0zWDLLUXqAi4= github.com/google/go-containerregistry v0.21.3/go.mod h1:D5ZrJF1e6dMzvInpBPuMCX0FxURz7GLq2rV3Us9aPkc= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/trillian v1.7.2 h1:EPBxc4YWY4Ak8tcuhyFleY+zYlbCDCa4Sn24e1Ka8Js= github.com/google/trillian v1.7.2/go.mod h1:mfQJW4qRH6/ilABtPYNBerVJAJ/upxHLX81zxNQw05s= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ= github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM= github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= github.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0= github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM= github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM= github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= github.com/in-toto/attestation v1.1.2 h1:MBFn6lsMq6dptQZJBhalXTcWMb/aJy3V+GX3VYj/V1E= github.com/in-toto/attestation v1.1.2/go.mod h1:gYFddHMZj3DiQ0b62ltNi1Vj5rC879bTmBbrv9CRHpM= github.com/in-toto/in-toto-golang v0.10.0 h1:+s2eZQSK3WmWfYV85qXVSBfqgawi/5L02MaqA4o/tpM= github.com/in-toto/in-toto-golang v0.10.0/go.mod h1:wjT4RiyFlLWCmLUJjwB8oZcjaq7HA390aMJcD3xXgmg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7 h1:FWpSWRD8FbVkKQu8M1DM9jF5oXFLyE+XpisIYfdzbic= github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7/go.mod h1:BMxO138bOokdgt4UaxZiEfypcSHX0t6SIFimVP1oRfk= github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY= github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4= github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY= github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/letsencrypt/boulder v0.20251208.0 h1:rG1V+1Oiy8H7i6kSf85RwXeZZ8q2Vj65dbsSk88J7wI= github.com/letsencrypt/boulder v0.20251208.0/go.mod h1:Wi99CY9yzFg4yaHamFCBIScvY8KOcBUe1rlPjUZNTJM= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/moby/api v1.54.0 h1:7kbUgyiKcoBhm0UrWbdrMs7RX8dnwzURKVbZGy2GnL0= github.com/moby/moby/api v1.54.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= github.com/moby/moby/client v0.3.0 h1:UUGL5okry+Aomj3WhGt9Aigl3ZOxZGqR7XPo+RLPlKs= github.com/moby/moby/client v0.3.0/go.mod h1:HJgFbJRvogDQjbM8fqc1MCEm4mIAGMLjXbgwoZp6jCQ= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGqpgjJU3DYAZeI05A= github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq3K9hs5w6FpNMdUT//qR+zk= github.com/sassoftware/relic/v7 v7.6.2 h1:rS44Lbv9G9eXsukknS4mSjIAuuX+lMq/FnStgmZlUv4= github.com/sassoftware/relic/v7 v7.6.2/go.mod h1:kjmP0IBVkJZ6gXeAu35/KCEfca//+PKM6vTAsyDPY+k= github.com/secure-systems-lab/go-securesystemslib v0.10.0 h1:l+H5ErcW0PAehBNrBxoGv1jjNpGYdZ9RcheFkB2WI14= github.com/secure-systems-lab/go-securesystemslib v0.10.0/go.mod h1:MRKONWmRoFzPNQ9USRF9i1mc7MvAVvF1LlW8X5VWDvk= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= github.com/sigstore/cosign/v3 v3.0.5 h1:c1zPqjU+H4wmirgysC+AkWMg7a7fykyOYF/m+F1150I= github.com/sigstore/cosign/v3 v3.0.5/go.mod h1:ble1vMvJagCFyTIDkibCq6MIHiWDw00JNYl0f9rB4T4= github.com/sigstore/protobuf-specs v0.5.0 h1:F8YTI65xOHw70NrvPwJ5PhAzsvTnuJMGLkA4FIkofAY= github.com/sigstore/protobuf-specs v0.5.0/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc= github.com/sigstore/rekor v1.5.1 h1:Ca1egHRWRuDvXV4tZu9aXEXc3Gej9FG+HKeapV9OAMQ= github.com/sigstore/rekor v1.5.1/go.mod h1:gTLDuZuo3SyQCuZvKqwRPA79Qo/2rw39/WtLP/rZjUQ= github.com/sigstore/rekor-tiles/v2 v2.2.1 h1:UmV1CBQ3SjxxPGpFmwDoOhoIwiKpM2Qm1pU5tPGmvNk= github.com/sigstore/rekor-tiles/v2 v2.2.1/go.mod h1:z8n6l6oidpaLjjE6rJERuQqY9X38ulnHZCXyL+DEL7U= github.com/sigstore/sigstore v1.10.4 h1:ytOmxMgLdcUed3w1SbbZOgcxqwMG61lh1TmZLN+WeZE= github.com/sigstore/sigstore v1.10.4/go.mod h1:tDiyrdOref3q6qJxm2G+JHghqfmvifB7hw+EReAfnbI= github.com/sigstore/sigstore-go v1.1.4 h1:wTTsgCHOfqiEzVyBYA6mDczGtBkN7cM8mPpjJj5QvMg= github.com/sigstore/sigstore-go v1.1.4/go.mod h1:2U/mQOT9cjjxrtIUeKDVhL+sHBKsnWddn8URlswdBsg= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.4 h1:VZ+L6SKVWbLPHznIF0tBuO7qKMFdJiJMVwFKu9DlY5o= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.4/go.mod h1:Rstj47WpJym25il8j4jTL0BfikzP/9AhVD+DsBcYzZc= github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.4 h1:G7yOv8bxk3zIEEZyVCixPxtePIAm+t3ZWSaKRPzVw+o= github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.4/go.mod h1:hxJelB/bRItMYOzi6qD9xEKjse2QZcikh4TbysfdDHc= github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.4 h1:Qxt6dE4IwhJ6gIXmg2q4S/SeqEDSZ29nmfsv7Zb6LL4= github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.4/go.mod h1:hJVeNOwarqfyALjOwsf0OR8YA/A96NABucEaQumPr30= github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.4 h1:KVavYMPfSf5NryOl6VrZ9nRG3fXOOJOPp7Czk/YCPkM= github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.4/go.mod h1:J7CA1AaBkyK8dYq6EdQANhj+8oEcsA7PrIp088qgPiY= github.com/sigstore/timestamp-authority/v2 v2.0.5 h1:WT17MU4bNRvjRLlTvTO5gmrSIWJVbzwrNXgwsjB+53U= github.com/sigstore/timestamp-authority/v2 v2.0.5/go.mod h1:oV+Yy0GsfgNAeDZcv/WJjQE42wFtMTtuD85bPLAQk5M= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI= github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug= github.com/theupdateframework/go-tuf/v2 v2.4.1 h1:K6ewW064rKZCPkRo1W/CTbTtm/+IB4+coG1iNURAGCw= github.com/theupdateframework/go-tuf/v2 v2.4.1/go.mod h1:Nex2enPVYDFCklrnbTzl3OVwD7fgIAj0J5++z/rvCj8= github.com/tink-crypto/tink-go-awskms/v2 v2.1.0 h1:N9UxlsOzu5mttdjhxkDLbzwtEecuXmlxZVo/ds7JKJI= github.com/tink-crypto/tink-go-awskms/v2 v2.1.0/go.mod h1:PxSp9GlOkKL9rlybW804uspnHuO9nbD98V/fDX4uSis= github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0 h1:3B9i6XBXNTRspfkTC0asN5W0K6GhOSgcujNiECNRNb0= github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0/go.mod h1:jY5YN2BqD/KSCHM9SqZPIpJNG/u3zwfLXHgws4x2IRw= github.com/tink-crypto/tink-go-hcvault/v2 v2.4.0 h1:j+S+WKBQ5ya26A5EM/uXoVe+a2IaPQN8KgBJZ22cJ+4= github.com/tink-crypto/tink-go-hcvault/v2 v2.4.0/go.mod h1:OCKJIujnTzDq7f+73NhVs99oA2c1TR6nsOpuasYM6Yo= github.com/tink-crypto/tink-go/v2 v2.6.0 h1:+KHNBHhWH33Vn+igZWcsgdEPUxKwBMEe0QC60t388v4= github.com/tink-crypto/tink-go/v2 v2.6.0/go.mod h1:2WbBA6pfNsAfBwDCggboaHeB2X29wkU8XHtGwh2YIk8= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= github.com/transparency-dev/formats v0.1.0 h1:oL0zUFuYUjg8AbtjPMnIRDmjbaHo5jCjEWU5yaNuz0g= github.com/transparency-dev/formats v0.1.0/go.mod h1:d2FibUOHfCMdCe/+/rbKt1IPLBbPTDfwj46kt541/mU= github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A= github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns= github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q= github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg= github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU= github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 h1:RN3ifU8y4prNWeEnQp2kRRHz8UwonAEYZl8tUzHEXAk= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.step.sm/crypto v0.76.2 h1:JJ/yMcs/rmcCAwlo+afrHjq74XBFRTJw5B2y4Q4Z4c4= go.step.sm/crypto v0.76.2/go.mod h1:m6KlB/HzIuGFep0UWI5e0SYi38UxpoKeCg6qUaHV6/Q= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.269.0 h1:qDrTOxKUQ/P0MveH6a7vZ+DNHxJQjtGm/uvdbdGXCQg= google.golang.org/api v0.269.0/go.mod h1:N8Wpcu23Tlccl0zSHEkcAZQKDLdquxK+l9r2LkwAauE= google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c h1:OyQPd6I3pN/9gDxz6L13kYGJgqkpdrAohJRBeXyxlgI= google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c/go.mod h1:X2gu9Qwng7Nn009s/r3RUxqkzQNqOrAy79bluY7ojIg= google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8= k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= sigs.k8s.io/kind v0.31.0 h1:UcT4nzm+YM7YEbqiAKECk+b6dsvc/HRZZu9U0FolL1g= sigs.k8s.io/kind v0.31.0/go.mod h1:FSqriGaoTPruiXWfRnUXNykF8r2t+fHtK0P0m1AbGF8= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= ================================================ FILE: hack/boilerplate/boilerplate.go.txt ================================================ // Copyright 2023 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. ================================================ FILE: hack/boilerplate/boilerplate.sh.txt ================================================ #!/usr/bin/env bash # Copyright 2023 ko Build Authors All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ================================================ FILE: hack/presubmit.sh ================================================ #!/usr/bin/env bash # Copyright 2021 ko Build Authors All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -o errexit set -o nounset set -o pipefail PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" pushd "${PROJECT_ROOT}" trap popd EXIT # Verify that all source files are correctly formatted. find . -name "*.go" -exec gofmt -d -e -l {} + # Verify that generated Markdown docs are up-to-date. tmpdir=$(mktemp -d) go run cmd/help/main.go --dir "$tmpdir" diff -Naur -I '###### Auto generated' "$tmpdir" docs/reference/ ================================================ FILE: hack/tools.go ================================================ // Copyright 2021 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build tools // +build tools package hack import ( _ "github.com/go-training/helloworld" ) ================================================ FILE: hack/update-codegen.sh ================================================ #!/usr/bin/env bash # Copyright 2021 ko Build Authors All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -o errexit set -o nounset set -o pipefail PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" pushd ${PROJECT_ROOT} trap popd EXIT go mod tidy go run $PROJECT_ROOT/cmd/help/main.go --dir=$PROJECT_ROOT/docs/reference/ ================================================ FILE: hack/update-deps.sh ================================================ #!/usr/bin/env bash # Copyright 2018 ko Build Authors All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -o errexit set -o nounset set -o pipefail PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" pushd ${PROJECT_ROOT} trap popd EXIT go mod tidy ================================================ FILE: integration_test.sh ================================================ #!/bin/bash set -o errexit set -o nounset set -o pipefail ROOT_DIR=$(dirname "$0") pushd "$ROOT_DIR" ROOT_DIR="$(pwd)" echo "Moving GOPATH into /tmp/ to test modules behavior." export GOPATH="${GOPATH:-$(go env GOPATH)}" export ORIGINAL_GOPATH="$GOPATH" GOPATH="$(mktemp -d)" export GOPATH export CGO_ENABLED=0 GOARCH="${GOARCH:-$(go env GOARCH)}" pushd "$GOPATH" || exit 1 echo "Copying ko to temp gopath." mkdir -p "$GOPATH/src/github.com/google/ko" cp -r "$ROOT_DIR/"* "$GOPATH/src/github.com/google/ko/" pushd "$GOPATH/src/github.com/google/ko" || exit 1 echo "Building ko" RESULT="$(go build .)" echo "Beginning scenarios." FILTER="[^ ]local[^ ]*" echo "1. Test should create an image that outputs 'Hello World'." RESULT="$(./ko build --local --platform="linux/$GOARCH" "$GOPATH/src/github.com/google/ko/test" | grep "$FILTER" | xargs -I% docker run %)" if [[ "$RESULT" != *"Hello there"* ]]; then echo "Test FAILED. Saw $RESULT" && exit 1 else echo "Test PASSED" fi echo "2. Test knative 'KO_FLAGS' variable is ignored." # https://github.com/ko-build/ko/issues/1317 RESULT="$(KO_FLAGS="--platform=badvalue" ./ko build --local --platform="linux/$GOARCH" "$GOPATH/src/github.com/google/ko/test" | grep "$FILTER" | xargs -I% docker run %)" if [[ "$RESULT" != *"Hello there"* ]]; then echo "Test FAILED. Saw $RESULT" && exit 1 else echo "Test PASSED" fi echo "3. Linux capabilities." pushd test/build-configs || exit 1 # run as non-root user with net_bind_service cap granted docker_run_opts="--user 1 --cap-add=net_bind_service" RESULT="$(../../ko build --local --platform="linux/$GOARCH" ./caps/cmd | grep "$FILTER" | xargs -I% docker run $docker_run_opts %)" if [[ "$RESULT" != "No capabilities" ]]; then echo "Test FAILED. Saw '$RESULT' but expected 'No capabilities'. Docker 'cap-add' must have no effect unless matching capabilities are granted to the file." && exit 1 fi # build with a different config requesting net_bind_service file capability RESULT_WITH_FILE_CAPS="$(KO_CONFIG_PATH=caps.ko.yaml ../../ko build --local --platform="linux/$GOARCH" ./caps/cmd | grep "$FILTER" | xargs -I% docker run $docker_run_opts %)" if [[ "$RESULT_WITH_FILE_CAPS" != "Has capabilities"* ]]; then echo "Test FAILED. Saw '$RESULT_WITH_FILE_CAPS' but expected 'Has capabilities'. Docker 'cap-add' must work when matching capabilities are granted to the file." && exit 1 else echo "Test PASSED" fi popd || exit 1 popd || exit 1 popd || exit 1 export GOPATH="$ORIGINAL_GOPATH" ================================================ FILE: internal/sbom/sbom.go ================================================ // Copyright 2022 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package sbom import ( "bufio" "bytes" "fmt" "runtime/debug" "strings" "unicode" ) func modulePackageName(mod *debug.Module) string { return fmt.Sprintf("SPDXRef-Package-%s-%s", strings.ReplaceAll(mod.Path, "/", "."), mod.Version) } func goRef(mod *debug.Module) string { path := mod.Path // Try to lowercase the first 2 path elements to comply with spec // https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#golang p := strings.Split(path, "/") if len(p) > 2 { path = strings.Join( append( []string{strings.ToLower(p[0]), strings.ToLower(p[1])}, p[2:]..., ), "/", ) } return fmt.Sprintf("pkg:golang/%s@%s?type=module", path, mod.Version) } // massageGoVersionM massages the output of `go version -m` into a form that // can be consumed by ParseBuildInfo. // // `go version -m` adds a line at the beginning of its output, and tabs at the // beginning of every line, that ParseBuildInfo doesn't like. func massageGoVersionM(b []byte) ([]byte, error) { var out bytes.Buffer scanner := bufio.NewScanner(bytes.NewReader(b)) if !scanner.Scan() { // Input was malformed, and doesn't contain any newlines (it // may even be empty). This seems to happen on Windows // (https://github.com/ko-build/ko/issues/535) and in unit tests. // Just proceed with an empty output for now, and SBOMs will be empty. // TODO: This should be an error. return nil, nil } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("malformed input: %w", err) } for scanner.Scan() { // NOTE: debug.ParseBuildInfo relies on trailing tabs. line := strings.TrimLeftFunc(scanner.Text(), unicode.IsSpace) fmt.Fprintln(&out, line) } if err := scanner.Err(); err != nil { return nil, err } return out.Bytes(), nil } ================================================ FILE: internal/sbom/spdx.go ================================================ // Copyright 2021 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package sbom import ( "bytes" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "net/url" "runtime/debug" "strings" "time" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/types" specsv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sigstore/cosign/v3/pkg/oci" ) type qualifier struct { key string value string } // ociRef constructs a pURL for the OCI image according to: // https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#oci func ociRef(path string, imgDigest v1.Hash, qual ...qualifier) string { parts := strings.Split(path, "/") purl := fmt.Sprintf("pkg:oci/%s@%s", parts[len(parts)-1], imgDigest.String()) if num := len(qual); num > 0 { qs := make(url.Values, num) for _, q := range qual { qs.Add(q.key, q.value) } purl = purl + "?" + qs.Encode() } return purl } func h1ToSHA256(s string) string { if !strings.HasPrefix(s, "h1:") { return "" } b, err := base64.StdEncoding.DecodeString(s[3:]) if err != nil { return "" } return hex.EncodeToString(b) } const dateFormat = "2006-01-02T15:04:05Z" func GenerateImageSPDX(koVersion string, mod []byte, img oci.SignedImage) ([]byte, error) { var err error mod, err = massageGoVersionM(mod) if err != nil { return nil, err } bi, err := debug.ParseBuildInfo(string(mod)) if err != nil { return nil, err } imgDigest, err := img.Digest() if err != nil { return nil, err } cfg, err := img.ConfigFile() if err != nil { return nil, err } m, err := img.Manifest() if err != nil { return nil, err } doc, imageID := starterDocument(koVersion, cfg.Created.Time, imgDigest) // image -> main package -> transitive deps // -> base image doc.Packages = make([]Package, 0, 3+len(bi.Deps)) doc.Relationships = make([]Relationship, 0, 3+len(bi.Deps)) doc.Relationships = append(doc.Relationships, Relationship{ Element: "SPDXRef-DOCUMENT", Type: "DESCRIBES", Related: imageID, }) doc.Packages = append(doc.Packages, Package{ ID: imageID, Name: imgDigest.String(), // TODO: PackageSupplier: "Organization: " + bs.Main.Path DownloadLocation: NOASSERTION, FilesAnalyzed: false, // TODO: PackageHomePage: "https://" + bi.Main.Path, LicenseConcluded: NOASSERTION, LicenseDeclared: NOASSERTION, CopyrightText: NOASSERTION, PrimaryPurpose: "CONTAINER", ExternalRefs: []ExternalRef{{ Category: "PACKAGE-MANAGER", Type: "purl", Locator: ociRef("image", imgDigest, qualifier{ key: "mediaType", value: string(m.MediaType), }), }}, }) if err := addBaseImage(&doc, m.Annotations, imgDigest); err != nil { return nil, err } mainPackageID := modulePackageName(&bi.Main) doc.Relationships = append(doc.Relationships, Relationship{ Element: imageID, Type: "CONTAINS", Related: mainPackageID, }) doc.Packages = append(doc.Packages, Package{ Name: bi.Main.Path, ID: mainPackageID, // TODO: PackageSupplier: "Organization: " + bs.Main.Path DownloadLocation: "https://" + bi.Main.Path, FilesAnalyzed: false, // TODO: PackageHomePage: "https://" + bi.Main.Path, LicenseConcluded: NOASSERTION, LicenseDeclared: NOASSERTION, CopyrightText: NOASSERTION, ExternalRefs: []ExternalRef{{ Category: "PACKAGE-MANAGER", Type: "purl", Locator: goRef(&bi.Main), }}, }) for _, dep := range bi.Deps { depID := modulePackageName(dep) doc.Relationships = append(doc.Relationships, Relationship{ Element: mainPackageID, Type: "DEPENDS_ON", Related: depID, }) pkg := Package{ ID: depID, Name: dep.Path, Version: dep.Version, // TODO: PackageSupplier: "Organization: " + dep.Path DownloadLocation: fmt.Sprintf("https://proxy.golang.org/%s/@v/%s.zip", dep.Path, dep.Version), FilesAnalyzed: false, LicenseConcluded: NOASSERTION, LicenseDeclared: NOASSERTION, CopyrightText: NOASSERTION, ExternalRefs: []ExternalRef{{ Category: "PACKAGE-MANAGER", Type: "purl", Locator: goRef(dep), }}, } if dep.Sum != "" { pkg.Checksums = []Checksum{{ Algorithm: "SHA256", Value: h1ToSHA256(dep.Sum), }} } doc.Packages = append(doc.Packages, pkg) } var buf bytes.Buffer enc := json.NewEncoder(&buf) enc.SetIndent("", " ") if err := enc.Encode(doc); err != nil { return nil, err } return buf.Bytes(), nil } func extractDate(sii oci.SignedImageIndex) (*time.Time, error) { im, err := sii.IndexManifest() if err != nil { return nil, err } for _, desc := range im.Manifests { switch desc.MediaType { case types.OCIManifestSchema1, types.DockerManifestSchema2: si, err := sii.SignedImage(desc.Digest) if err != nil { return nil, err } cfg, err := si.ConfigFile() if err != nil { return nil, err } return &cfg.Created.Time, nil default: // We shouldn't need to handle nested indices, since we don't build // them, but if we do we will need to do some sort of recursion here. return nil, fmt.Errorf("unknown media type: %v", desc.MediaType) } } return nil, errors.New("unable to extract date, no imaged found") } func GenerateIndexSPDX(koVersion string, sii oci.SignedImageIndex) ([]byte, error) { indexDigest, err := sii.Digest() if err != nil { return nil, err } date, err := extractDate(sii) if err != nil { return nil, err } im, err := sii.IndexManifest() if err != nil { return nil, err } doc, indexID := starterDocument(koVersion, *date, indexDigest) doc.Packages = []Package{{ ID: indexID, Name: indexDigest.String(), DownloadLocation: NOASSERTION, FilesAnalyzed: false, LicenseConcluded: NOASSERTION, LicenseDeclared: NOASSERTION, CopyrightText: NOASSERTION, PrimaryPurpose: "CONTAINER", Checksums: []Checksum{{ Algorithm: strings.ToUpper(indexDigest.Algorithm), Value: indexDigest.Hex, }}, ExternalRefs: []ExternalRef{{ Category: "PACKAGE-MANAGER", Type: "purl", Locator: ociRef("index", indexDigest, qualifier{ key: "mediaType", value: string(im.MediaType), }), }}, }} if err := addBaseImage(&doc, im.Annotations, indexDigest); err != nil { return nil, err } for _, desc := range im.Manifests { switch desc.MediaType { case types.OCIManifestSchema1, types.DockerManifestSchema2: si, err := sii.SignedImage(desc.Digest) if err != nil { return nil, err } imageDigest, err := si.Digest() if err != nil { return nil, err } depID := ociPackageName(imageDigest) doc.Relationships = append(doc.Relationships, Relationship{ Element: ociPackageName(indexDigest), Type: "VARIANT_OF", Related: depID, }) qual := []qualifier{{ key: "mediaType", value: string(desc.MediaType), }, { key: "arch", value: desc.Platform.Architecture, }, { key: "os", value: desc.Platform.OS, }} if desc.Platform.Variant != "" { qual = append(qual, qualifier{ key: "variant", value: desc.Platform.Variant, }) } if desc.Platform.OSVersion != "" { qual = append(qual, qualifier{ key: "os-version", value: desc.Platform.OSVersion, }) } for _, feat := range desc.Platform.OSFeatures { qual = append(qual, qualifier{ key: "os-feature", value: feat, }) } doc.Packages = append(doc.Packages, Package{ ID: depID, Name: imageDigest.String(), Version: desc.Platform.String(), // TODO: PackageSupplier: "Organization: " + dep.Path DownloadLocation: NOASSERTION, FilesAnalyzed: false, LicenseConcluded: NOASSERTION, LicenseDeclared: NOASSERTION, CopyrightText: NOASSERTION, PrimaryPurpose: "CONTAINER", ExternalRefs: []ExternalRef{{ Category: "PACKAGE-MANAGER", Type: "purl", Locator: ociRef("image", imageDigest, qual...), }}, Checksums: []Checksum{{ Algorithm: strings.ToUpper(imageDigest.Algorithm), Value: imageDigest.Hex, }}, }) default: // We shouldn't need to handle nested indices, since we don't build // them, but if we do we will need to do some sort of recursion here. return nil, fmt.Errorf("unknown media type: %v", desc.MediaType) } } var buf bytes.Buffer enc := json.NewEncoder(&buf) enc.SetIndent("", " ") if err := enc.Encode(doc); err != nil { return nil, err } return buf.Bytes(), nil } func ociPackageName(d v1.Hash) string { return fmt.Sprintf("SPDXRef-Package-%s-%s", d.Algorithm, d.Hex) } func starterDocument(koVersion string, date time.Time, d v1.Hash) (Document, string) { digestID := ociPackageName(d) return Document{ ID: "SPDXRef-DOCUMENT", Version: Version, CreationInfo: CreationInfo{ Created: date.Format(dateFormat), Creators: []string{"Tool: ko " + koVersion}, }, DataLicense: "CC0-1.0", Name: "sbom-" + d.String(), Namespace: "http://spdx.org/spdxdocs/ko/" + d.String(), DocumentDescribes: []string{digestID}, }, digestID } func addBaseImage(doc *Document, annotations map[string]string, h v1.Hash) error { // Check for the base image annotation. base, ok := annotations[specsv1.AnnotationBaseImageName] if !ok { return nil } rawHash, ok := annotations[specsv1.AnnotationBaseImageDigest] if !ok { return nil } ref, err := name.ParseReference(base) if err != nil { return err } hash, err := v1.NewHash(rawHash) if err != nil { return err } digest := ref.Context().Digest(hash.String()) depID := ociPackageName(hash) doc.Relationships = append(doc.Relationships, Relationship{ Element: ociPackageName(h), Type: "DESCENDANT_OF", Related: depID, }) qual := []qualifier{{ key: "repository_url", value: ref.Context().Name(), }} if t, ok := ref.(name.Tag); ok { qual = append(qual, qualifier{ key: "tag", value: t.Identifier(), }) } doc.Packages = append(doc.Packages, Package{ ID: depID, Name: digest.String(), Version: ref.String(), // TODO: PackageSupplier: "Organization: " + dep.Path DownloadLocation: NOASSERTION, FilesAnalyzed: false, LicenseConcluded: NOASSERTION, LicenseDeclared: NOASSERTION, CopyrightText: NOASSERTION, ExternalRefs: []ExternalRef{{ Category: "PACKAGE-MANAGER", Type: "purl", Locator: ociRef("image", hash, qual...), }}, Checksums: []Checksum{{ Algorithm: strings.ToUpper(hash.Algorithm), Value: hash.Hex, }}, }) return nil } // Below this is forked from here: // https://github.com/kubernetes-sigs/bom/blob/main/pkg/spdx/json/v2.2.2/types.go /* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const ( NOASSERTION = "NOASSERTION" Version = "SPDX-2.3" ) type Document struct { ID string `json:"SPDXID"` Name string `json:"name"` Version string `json:"spdxVersion"` CreationInfo CreationInfo `json:"creationInfo"` DataLicense string `json:"dataLicense"` Namespace string `json:"documentNamespace"` DocumentDescribes []string `json:"documentDescribes,omitempty"` Files []File `json:"files,omitempty"` Packages []Package `json:"packages,omitempty"` Relationships []Relationship `json:"relationships,omitempty"` ExternalDocumentRefs []ExternalDocumentRef `json:"externalDocumentRefs,omitempty"` } type CreationInfo struct { Created string `json:"created"` // Date Creators []string `json:"creators,omitempty"` LicenseListVersion string `json:"licenseListVersion,omitempty"` } type Package struct { ID string `json:"SPDXID"` Name string `json:"name"` Version string `json:"versionInfo,omitempty"` FilesAnalyzed bool `json:"filesAnalyzed"` LicenseDeclared string `json:"licenseDeclared"` LicenseConcluded string `json:"licenseConcluded"` Description string `json:"description,omitempty"` DownloadLocation string `json:"downloadLocation"` Originator string `json:"originator,omitempty"` SourceInfo string `json:"sourceInfo,omitempty"` CopyrightText string `json:"copyrightText"` PrimaryPurpose string `json:"primaryPackagePurpose,omitempty"` HasFiles []string `json:"hasFiles,omitempty"` LicenseInfoFromFiles []string `json:"licenseInfoFromFiles,omitempty"` Checksums []Checksum `json:"checksums,omitempty"` ExternalRefs []ExternalRef `json:"externalRefs,omitempty"` VerificationCode *PackageVerificationCode `json:"packageVerificationCode,omitempty"` } type PackageVerificationCode struct { Value string `json:"packageVerificationCodeValue"` ExcludedFiles []string `json:"packageVerificationCodeExcludedFiles,omitempty"` } type File struct { ID string `json:"SPDXID"` Name string `json:"fileName"` CopyrightText string `json:"copyrightText"` NoticeText string `json:"noticeText,omitempty"` LicenseConcluded string `json:"licenseConcluded"` Description string `json:"description,omitempty"` FileTypes []string `json:"fileTypes,omitempty"` LicenseInfoInFile []string `json:"licenseInfoInFiles"` // List of licenses Checksums []Checksum `json:"checksums"` } type Checksum struct { Algorithm string `json:"algorithm"` Value string `json:"checksumValue"` } type ExternalRef struct { Category string `json:"referenceCategory"` Locator string `json:"referenceLocator"` Type string `json:"referenceType"` } type Relationship struct { Element string `json:"spdxElementId"` Type string `json:"relationshipType"` Related string `json:"relatedSpdxElement"` } type ExternalDocumentRef struct { Checksum Checksum `json:"checksum"` ExternalDocumentID string `json:"externalDocumentId"` SPDXDocument string `json:"spdxDocument"` } ================================================ FILE: main.go ================================================ // Copyright 2020 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:generate go run ./cmd/help/main.go -d docs/reference/ package main import ( "context" "os" "os/signal" "github.com/google/ko/pkg/commands" ) func main() { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) defer stop() if err := commands.Root.ExecuteContext(ctx); err != nil { os.Exit(1) } } ================================================ FILE: mkdocs.yml ================================================ site_name: 'ko: Easy Go Containers' site_url: https://ko.build repo_url: https://github.com/ko-build/ko edit_uri: edit/main/docs/ theme: name: material logo: images/favicon-96x96.png favicon: images/favicon-96x96.png custom_dir: docs/custom/ palette: # Palette toggle for automatic mode - media: "(prefers-color-scheme)" toggle: icon: material/brightness-auto name: Switch to light mode # Palette toggle for light mode - media: "(prefers-color-scheme: light)" primary: light blue toggle: icon: material/brightness-7 name: Switch to dark mode # Palette toggle for dark mode - media: "(prefers-color-scheme: dark)" scheme: slate primary: light blue toggle: icon: material/brightness-4 name: Switch to system preference nav: - index.md - install.md - get-started.md - configuration.md - deployment.md - community.md - Features: - features/multi-platform.md - features/sboms.md - features/k8s.md - features/static-assets.md - features/build-cache.md - features/debugging.md - Advanced: - advanced/go-packages.md - advanced/limitations.md - advanced/migrating-from-dockerfile.md - advanced/faq.md - advanced/terraform.md - advanced/lambda.md - advanced/linux-capabilities.md - advanced/root-ca-certificates.md - CLI Reference: - 'ko': reference/ko.md - 'ko apply': reference/ko_apply.md - 'ko build': reference/ko_build.md - 'ko create': reference/ko_create.md - 'ko delete': reference/ko_delete.md - 'ko login': reference/ko_login.md - 'ko resolve': reference/ko_resolve.md - 'ko run': reference/ko_run.md - 'ko version': reference/ko_version.md - Releases: "https://github.com/ko-build/ko/releases" plugins: - search - redirects: redirect_maps: 'repo.md': 'https://github.com/ko-build/ko' 'issues.md': 'https://github.com/ko-build/ko/issues' 'prs.md': 'https://github.com/ko-build/ko/pulls' 'releases.md': 'https://github.com/ko-build/ko/releases' 'godoc.md': 'https://pkg.go.dev/github.com/google/ko' 'terraform.md': 'https://github.com/ko-build/terraform-provider-ko' 'action.md': 'https://github.com/ko-build/setup-ko' 'slack.md': 'https://kubernetes.slack.com/archives/C01T7DTP65S' 'agenda.md': 'https://docs.google.com/document/d/1eQ67Qxwf1tkTv0yU_dw9bIRnlwJZz-5GXCRVOhqbgvU/edit' 'meet.md': 'meet.google.com/xvn-dzzk-wur' markdown_extensions: - pymdownx.highlight: anchor_linenums: true line_spans: __span pygments_lang_class: true - pymdownx.inlinehilite - pymdownx.snippets - pymdownx.superfences ================================================ FILE: pkg/build/build.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package build import ( "context" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/types" ) // Interface abstracts different methods for turning a supported importpath // reference into a v1.Image. type Interface interface { // QualifyImport turns relative importpath references into complete importpaths. // It also adds the ko scheme prefix if necessary. // E.g., "github.com/ko-build/ko/test" => "ko://github.com/ko-build/ko/test" // and "./test" => "ko://github.com/ko-build/ko/test" QualifyImport(string) (string, error) // IsSupportedReference determines whether the given reference is to an // importpath reference that Ko supports building, returning an error // if it is not. // TODO(mattmoor): Verify that some base repo: foo.io/bar can be suffixed with this reference and parsed. IsSupportedReference(string) error // Build turns the given importpath reference into a v1.Image containing the Go binary // (or a set of images as a v1.ImageIndex). Build(context.Context, string) (Result, error) } // Result represents the product of a Build. // This is generally one of: // - v1.Image (or oci.SignedImage), or // - v1.ImageIndex (or oci.SignedImageIndex) type Result interface { MediaType() (types.MediaType, error) Size() (int64, error) Digest() (v1.Hash, error) RawManifest() ([]byte, error) } // Assert that Image and ImageIndex implement Result. var _ Result = (v1.Image)(nil) var _ Result = (v1.ImageIndex)(nil) ================================================ FILE: pkg/build/cache.go ================================================ // Copyright 2021 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package build import ( "bytes" "context" "encoding/json" "fmt" "log" "os" "os/exec" "path/filepath" "strings" "sync" "github.com/google/go-containerregistry/pkg/logs" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/partial" ) type diffIDToDescriptor map[v1.Hash]v1.Descriptor type buildIDToDiffID map[string]v1.Hash type layerCache struct { buildToDiff map[string]buildIDToDiffID diffToDesc map[string]diffIDToDescriptor sync.Mutex } type layerFactory func() (v1.Layer, error) func (c *layerCache) get(ctx context.Context, file string, miss layerFactory) (v1.Layer, error) { if os.Getenv("KOCACHE") == "" { return miss() } // Cache hit. if diffid, desc, err := c.getMeta(ctx, file); err != nil { logs.Debug.Printf("getMeta(%q): %v", file, err) } else { return &lazyLayer{ diffid: *diffid, desc: *desc, buildLayer: miss, }, nil } // Cache miss. layer, err := miss() if err != nil { return nil, fmt.Errorf("miss(%q): %w", file, err) } if err := c.put(ctx, file, layer); err != nil { log.Printf("failed to cache metadata %s: %v", file, err) } return layer, nil } func (c *layerCache) getMeta(ctx context.Context, file string) (*v1.Hash, *v1.Descriptor, error) { buildid, err := getBuildID(ctx, file) if err != nil { return nil, nil, err } if buildid == "" { return nil, nil, fmt.Errorf("no buildid for %q", file) } // TODO: Implement better per-file locking. c.Lock() defer c.Unlock() btod, err := c.readBuildToDiff(file) if err != nil { return nil, nil, err } dtod, err := c.readDiffToDesc(file) if err != nil { return nil, nil, err } diffid, ok := btod[buildid] if !ok { return nil, nil, fmt.Errorf("no diffid for %q", buildid) } desc, ok := dtod[diffid] if !ok { return nil, nil, fmt.Errorf("no desc for %q", diffid) } return &diffid, &desc, nil } // Compute new layer metadata and cache it in-mem and on-disk. func (c *layerCache) put(ctx context.Context, file string, layer v1.Layer) error { buildid, err := getBuildID(ctx, file) if err != nil { return err } desc, err := partial.Descriptor(layer) if err != nil { return err } diffid, err := layer.DiffID() if err != nil { return err } btod, ok := c.buildToDiff[file] if !ok { btod = buildIDToDiffID{} } btod[buildid] = diffid dtod, ok := c.diffToDesc[file] if !ok { dtod = diffIDToDescriptor{} } dtod[diffid] = *desc // TODO: Implement better per-file locking. c.Lock() defer c.Unlock() btodf, err := os.OpenFile(filepath.Join(filepath.Dir(file), "buildid-to-diffid"), os.O_RDWR|os.O_CREATE, 0755) if err != nil { return fmt.Errorf("opening buildid-to-diffid: %w", err) } defer btodf.Close() dtodf, err := os.OpenFile(filepath.Join(filepath.Dir(file), "diffid-to-descriptor"), os.O_RDWR|os.O_CREATE, 0755) if err != nil { return fmt.Errorf("opening diffid-to-descriptor: %w", err) } defer dtodf.Close() enc := json.NewEncoder(btodf) enc.SetIndent("", " ") if err := enc.Encode(&btod); err != nil { return err } enc = json.NewEncoder(dtodf) enc.SetIndent("", " ") return enc.Encode(&dtod) } func (c *layerCache) readDiffToDesc(file string) (diffIDToDescriptor, error) { if dtod, ok := c.diffToDesc[file]; ok { return dtod, nil } dtodf, err := os.Open(filepath.Join(filepath.Dir(file), "diffid-to-descriptor")) if err != nil { return nil, fmt.Errorf("opening diffid-to-descriptor: %w", err) } defer dtodf.Close() var dtod diffIDToDescriptor if err := json.NewDecoder(dtodf).Decode(&dtod); err != nil { return nil, err } c.diffToDesc[file] = dtod return dtod, nil } func (c *layerCache) readBuildToDiff(file string) (buildIDToDiffID, error) { if btod, ok := c.buildToDiff[file]; ok { return btod, nil } btodf, err := os.Open(filepath.Join(filepath.Dir(file), "buildid-to-diffid")) if err != nil { return nil, fmt.Errorf("opening buildid-to-diffid: %w", err) } defer btodf.Close() var btod buildIDToDiffID if err := json.NewDecoder(btodf).Decode(&btod); err != nil { return nil, err } c.buildToDiff[file] = btod return btod, nil } func getBuildID(ctx context.Context, file string) (string, error) { gobin := getGoBinary() cmd := exec.CommandContext(ctx, gobin, "tool", "buildid", file) var output bytes.Buffer cmd.Stderr = &output cmd.Stdout = &output if err := cmd.Run(); err != nil { log.Printf("Unexpected error running \"go tool buildid %s\": %v\n%v", err, file, output.String()) return "", fmt.Errorf("go tool buildid %s: %w", file, err) } return strings.TrimSpace(output.String()), nil } ================================================ FILE: pkg/build/config.go ================================================ // Copyright 2021 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package build import "strings" // Note: The structs, types, and functions are based upon GoReleaser build // configuration to have a loosely compatible YAML configuration: // https://github.com/goreleaser/goreleaser/blob/master/pkg/config/config.go // StringArray is a wrapper for an array of strings. type StringArray []string // UnmarshalYAML is a custom unmarshaler that wraps strings in arrays. func (a *StringArray) UnmarshalYAML(unmarshal func(any) error) error { var strings []string if err := unmarshal(&strings); err != nil { var str string if err := unmarshal(&str); err != nil { return err } *a = []string{str} } else { *a = strings } return nil } // FlagArray is a wrapper for an array of strings. type FlagArray []string // UnmarshalYAML is a custom unmarshaler that wraps strings in arrays. func (a *FlagArray) UnmarshalYAML(unmarshal func(any) error) error { var flags []string if err := unmarshal(&flags); err != nil { var flagstr string if err := unmarshal(&flagstr); err != nil { return err } *a = strings.Fields(flagstr) } else { *a = flags } return nil } // Config contains the build configuration section. The name was changed from // the original GoReleaser name to match better with the ko naming. // // TODO: Introduce support for more fields where possible and where it makes // / sense for `ko`, for example ModTimestamp or GoBinary. type Config struct { // ID only serves as an identifier internally ID string `yaml:",omitempty"` // Dir is the directory out of which the build should be triggered Dir string `yaml:",omitempty"` // Main points to the main package, or the source file with the main // function, in which case only the package will be used for the importpath Main string `yaml:",omitempty"` // Ldflags and Flags will be used for the Go build command line arguments Ldflags StringArray `yaml:",omitempty"` Flags FlagArray `yaml:",omitempty"` // Env allows setting environment variables for `go build` Env []string `yaml:",omitempty"` // Other GoReleaser fields that are not supported or do not make sense // in the context of ko, for reference or for future use: // Goos []string `yaml:",omitempty"` // Goarch []string `yaml:",omitempty"` // Goarm []string `yaml:",omitempty"` // Gomips []string `yaml:",omitempty"` // Targets []string `yaml:",omitempty"` // Binary string `yaml:",omitempty"` // Lang string `yaml:",omitempty"` // Asmflags StringArray `yaml:",omitempty"` // Gcflags StringArray `yaml:",omitempty"` // ModTimestamp string `yaml:"mod_timestamp,omitempty"` // GoBinary string `yaml:",omitempty"` // extension: Linux capabilities to enable on the executable, applies // to Linux targets. LinuxCapabilities FlagArray `yaml:"linux_capabilities,omitempty"` } ================================================ FILE: pkg/build/doc.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package build defines methods for building a v1.Image reference from a // Go binary reference. package build ================================================ FILE: pkg/build/future.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package build import ( "sync" ) func newFuture(work func() (Result, error)) *future { // Create a channel on which to send the result. ch := make(chan *result) // Initiate the actual work, sending its result // along the above channel. go func() { img, err := work() ch <- &result{img: img, err: err} }() // Return a future for the above work. Callers should // call .Get() on this result (as many times as needed). // One of these calls will receive the result, store it, // and close the channel so that the rest of the callers // can consume it. return &future{ promise: ch, } } type result struct { img Result err error } type future struct { m sync.RWMutex result *result promise chan *result } // Get blocks on the result of the future. func (f *future) Get() (Result, error) { // Block on the promise of a result until we get one. result, ok := <-f.promise if ok { func() { f.m.Lock() defer f.m.Unlock() // If we got the result, then store it so that // others may access it. f.result = result // Close the promise channel so that others // are signaled that the result is available. close(f.promise) }() } f.m.RLock() defer f.m.RUnlock() return f.result.img, f.result.err } ================================================ FILE: pkg/build/future_test.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package build import ( "testing" "github.com/google/go-containerregistry/pkg/v1/random" ) func makeImage() (Result, error) { return random.Index(256, 8, 1) } func digest(t *testing.T, img Result) string { d, err := img.Digest() if err != nil { t.Fatalf("Digest() = %v", err) } return d.String() } func TestSameFutureSameImage(t *testing.T) { f := newFuture(makeImage) i1, err := f.Get() if err != nil { t.Errorf("Get() = %v", err) } d1 := digest(t, i1) i2, err := f.Get() if err != nil { t.Errorf("Get() = %v", err) } d2 := digest(t, i2) if d1 != d2 { t.Errorf("Got different digests %s and %s", d1, d2) } } func TestDiffFutureDiffImage(t *testing.T) { f1 := newFuture(makeImage) f2 := newFuture(makeImage) i1, err := f1.Get() if err != nil { t.Errorf("Get() = %v", err) } d1 := digest(t, i1) i2, err := f2.Get() if err != nil { t.Errorf("Get() = %v", err) } d2 := digest(t, i2) if d1 == d2 { t.Errorf("Got same digest %s, wanted different", d1) } } ================================================ FILE: pkg/build/gobuild.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package build import ( "archive/tar" "bufio" "bytes" "context" "errors" "fmt" gb "go/build" "io" "log" "maps" "os" "os/exec" "path" "path/filepath" "runtime" "strconv" "strings" "text/template" "time" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/tarball" "github.com/google/go-containerregistry/pkg/v1/types" "github.com/google/ko/internal/sbom" "github.com/google/ko/pkg/caps" "github.com/google/ko/pkg/internal/git" specsv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sigstore/cosign/v3/pkg/oci" ocimutate "github.com/sigstore/cosign/v3/pkg/oci/mutate" "github.com/sigstore/cosign/v3/pkg/oci/signed" "github.com/sigstore/cosign/v3/pkg/oci/static" ctypes "github.com/sigstore/cosign/v3/pkg/types" "golang.org/x/sync/errgroup" "golang.org/x/sync/semaphore" "golang.org/x/tools/go/packages" ) const ( defaultAppFilename = "ko-app" defaultGoBin = "go" // defaults to first go binary found in PATH goBinPathEnv = "KO_GO_PATH" // env lookup for optional relative or full go binary path ) // GetBase takes an importpath and returns a base image reference and base image (or index). type GetBase func(context.Context, string) (name.Reference, Result, error) // buildContext provides parameters for a builder function. type buildContext struct { creationTime v1.Time ip string dir string env []string flags []string ldflags []string platform v1.Platform } type builder func(context.Context, buildContext) (string, error) type sbomber func(context.Context, string, string, string, oci.SignedEntity, string) ([]byte, types.MediaType, error) type platformMatcher struct { spec []string platforms []v1.Platform } type gobuild struct { ctx context.Context getBase GetBase creationTime v1.Time kodataCreationTime v1.Time build builder sbom sbomber sbomDir string disableOptimizations bool trimpath bool buildConfigs map[string]Config defaultEnv []string defaultFlags []string defaultLdflags []string platformMatcher *platformMatcher dir string labels map[string]string annotations map[string]string user string debug bool semaphore *semaphore.Weighted cache *layerCache } // Option is a functional option for NewGo. type Option func(*gobuildOpener) error type gobuildOpener struct { ctx context.Context getBase GetBase creationTime v1.Time kodataCreationTime v1.Time build builder sbom sbomber sbomDir string disableOptimizations bool trimpath bool buildConfigs map[string]Config defaultEnv []string defaultFlags []string defaultLdflags []string platforms []string labels map[string]string annotations map[string]string user string dir string jobs int debug bool } func (gbo *gobuildOpener) Open() (Interface, error) { if gbo.getBase == nil { return nil, errors.New("a way of providing base images must be specified, see build.WithBaseImages") } matcher, err := parseSpec(gbo.platforms) if err != nil { return nil, err } if gbo.jobs == 0 { gbo.jobs = runtime.GOMAXPROCS(0) } if gbo.annotations == nil { gbo.annotations = map[string]string{} } return &gobuild{ ctx: gbo.ctx, getBase: gbo.getBase, user: gbo.user, creationTime: gbo.creationTime, kodataCreationTime: gbo.kodataCreationTime, build: gbo.build, sbom: gbo.sbom, sbomDir: gbo.sbomDir, disableOptimizations: gbo.disableOptimizations, trimpath: gbo.trimpath, buildConfigs: gbo.buildConfigs, defaultEnv: gbo.defaultEnv, defaultFlags: gbo.defaultFlags, defaultLdflags: gbo.defaultLdflags, labels: gbo.labels, annotations: gbo.annotations, dir: gbo.dir, debug: gbo.debug, platformMatcher: matcher, cache: &layerCache{ buildToDiff: map[string]buildIDToDiffID{}, diffToDesc: map[string]diffIDToDescriptor{}, }, semaphore: semaphore.NewWeighted(int64(gbo.jobs)), }, nil } // NewGo returns a build.Interface implementation that: // 1. builds go binaries named by importpath, // 2. containerizes the binary on a suitable base. // // The `dir` argument is the working directory for executing the `go` tool. // If `dir` is empty, the function uses the current process working directory. func NewGo(ctx context.Context, dir string, options ...Option) (Interface, error) { gbo := &gobuildOpener{ ctx: ctx, build: build, dir: dir, sbom: spdx("(none)"), } for _, option := range options { if err := option(gbo); err != nil { return nil, err } } return gbo.Open() } func (g *gobuild) qualifyLocalImport(importpath string) (string, error) { dir := filepath.Clean(g.dir) if dir == "." { dir = "" } cfg := &packages.Config{ Mode: packages.NeedName, Dir: dir, } pkgs, err := packages.Load(cfg, importpath) if err != nil { return "", err } if len(pkgs) != 1 { return "", fmt.Errorf("found %d local packages, expected 1", len(pkgs)) } return pkgs[0].PkgPath, nil } // QualifyImport implements build.Interface func (g *gobuild) QualifyImport(importpath string) (string, error) { if gb.IsLocalImport(importpath) { var err error importpath, err = g.qualifyLocalImport(importpath) if err != nil { return "", fmt.Errorf("qualifying local import %s: %w", importpath, err) } } if !strings.HasPrefix(importpath, StrictScheme) { importpath = StrictScheme + importpath } return importpath, nil } // IsSupportedReference implements build.Interface // // Only valid importpaths that provide commands (i.e., are "package main") are // supported. func (g *gobuild) IsSupportedReference(s string) error { ref := newRef(s) if !ref.IsStrict() { return errors.New("importpath does not start with ko://") } dir := filepath.Clean(g.dir) if dir == "." { dir = "" } pkgs, err := packages.Load(&packages.Config{Dir: dir, Mode: packages.NeedName}, ref.Path()) if err != nil { return fmt.Errorf("error loading package from %s: %w", ref.Path(), err) } if len(pkgs) != 1 { return fmt.Errorf("found %d local packages, expected 1", len(pkgs)) } if pkgs[0].Name != "main" { return errors.New("importpath is not `package main`") } return nil } func getGoarm(platform v1.Platform) (string, error) { if !strings.HasPrefix(platform.Variant, "v") { return "", fmt.Errorf("strange arm variant: %v", platform.Variant) } vs := strings.TrimPrefix(platform.Variant, "v") variant, err := strconv.Atoi(vs) if err != nil { return "", fmt.Errorf("cannot parse arm variant %q: %w", platform.Variant, err) } if variant >= 5 { // TODO(golang/go#29373): Allow for 8 in later go versions if this is fixed. if variant > 7 { vs = "7" } return vs, nil } return "", nil } func getGoBinary() string { if env := os.Getenv(goBinPathEnv); env != "" { return env } return defaultGoBin } func doesPlatformSupportDebugging(platform v1.Platform) bool { // Here's the list of supported platforms by Delve: // // https://github.com/go-delve/delve/blob/master/Documentation/faq.md#unsupportedplatforms // // For the time being, we'll support only linux/amd64 and linux/arm64. return platform.OS == "linux" && (platform.Architecture == "amd64" || platform.Architecture == "arm64") } func getDelve(ctx context.Context, platform v1.Platform) (string, error) { const delveCloneURL = "https://github.com/go-delve/delve.git" if platform.OS == "" || platform.Architecture == "" { return "", fmt.Errorf("platform os (%q) or arch (%q) is empty", platform.OS, platform.Architecture, ) } env, err := buildEnv(platform, os.Environ(), nil) if err != nil { return "", fmt.Errorf("could not create env for Delve build: %w", err) } tmpInstallDir, err := os.MkdirTemp("", "delve") if err != nil { return "", fmt.Errorf("could not create tmp dir for Delve installation: %w", err) } cloneDir := filepath.Join(tmpInstallDir, "delve") err = os.MkdirAll(cloneDir, 0755) if err != nil { return "", fmt.Errorf("making dir for delve clone: %w", err) } err = git.Clone(ctx, cloneDir, delveCloneURL) if err != nil { return "", fmt.Errorf("cloning delve repo: %w", err) } osArchDir := fmt.Sprintf("%s_%s", platform.OS, platform.Architecture) delveBinaryPath := filepath.Join(tmpInstallDir, "bin", osArchDir, "dlv") // install delve to tmp directory args := []string{ "build", "-trimpath", "-ldflags=-s -w", "-o", delveBinaryPath, "./cmd/dlv", } gobin := getGoBinary() cmd := exec.CommandContext(ctx, gobin, args...) cmd.Env = env cmd.Dir = cloneDir var output bytes.Buffer cmd.Stderr = &output cmd.Stdout = &output log.Printf("Building Delve for %s", platform) if err := cmd.Run(); err != nil { os.RemoveAll(tmpInstallDir) return "", fmt.Errorf("go build Delve: %w: %s", err, output.String()) } if _, err := os.Stat(delveBinaryPath); err != nil { return "", fmt.Errorf("could not find Delve binary at %q: %w", delveBinaryPath, err) } return delveBinaryPath, nil } func build(ctx context.Context, buildCtx buildContext) (string, error) { // Create the set of build arguments from the config flags/ldflags with any // template parameters applied. buildArgs, err := createBuildArgs(ctx, buildCtx) if err != nil { return "", err } args := make([]string, 0, 4+len(buildArgs)) args = append(args, "build") args = append(args, buildArgs...) tmpDir := "" if dir := os.Getenv("KOCACHE"); dir != "" { dirInfo, err := os.Stat(dir) if err != nil { if !os.IsNotExist(err) { return "", fmt.Errorf("could not stat KOCACHE: %w", err) } if err := os.MkdirAll(dir, os.ModePerm); err != nil && !os.IsExist(err) { return "", fmt.Errorf("could not create KOCACHE dir %s: %w", dir, err) } } else if !dirInfo.IsDir() { return "", fmt.Errorf("KOCACHE should be a directory, %s is not a directory", dir) } // TODO(#264): if KOCACHE is unset, default to filepath.Join(os.TempDir(), "ko"). tmpDir = filepath.Join(dir, "bin", buildCtx.ip, buildCtx.platform.String()) if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil { return "", fmt.Errorf("creating KOCACHE bin dir: %w", err) } } else { tmpDir, err = os.MkdirTemp("", "ko") if err != nil { return "", err } } file := filepath.Join(tmpDir, "out") args = append(args, "-o", file) args = append(args, buildCtx.ip) gobin := getGoBinary() cmd := exec.CommandContext(ctx, gobin, args...) cmd.Dir = buildCtx.dir cmd.Env = buildCtx.env var output bytes.Buffer cmd.Stderr = &output cmd.Stdout = &output log.Printf("Building %s for %s", buildCtx.ip, buildCtx.platform) if err := cmd.Run(); err != nil { if os.Getenv("KOCACHE") == "" { _ = os.RemoveAll(tmpDir) } return "", fmt.Errorf("go build: %w: %s", err, output.String()) } return file, nil } func goenv(ctx context.Context) (map[string]string, error) { gobin := getGoBinary() cmd := exec.CommandContext(ctx, gobin, "env") var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr if err := cmd.Run(); err != nil { return nil, fmt.Errorf("go env: %w: %s", err, stderr.String()) } env := make(map[string]string) scanner := bufio.NewScanner(bytes.NewReader(stdout.Bytes())) line := 0 for scanner.Scan() { line++ kv := strings.SplitN(scanner.Text(), "=", 2) if len(kv) != 2 { return nil, fmt.Errorf("go env: failed parsing line: %d", line) } key := strings.TrimSpace(kv[0]) value := strings.TrimSpace(kv[1]) // Unquote the value. Handle single or double quoted strings. if len(value) > 1 && ((value[0] == '\'' && value[len(value)-1] == '\'') || (value[0] == '"' && value[len(value)-1] == '"')) { value = value[1 : len(value)-1] } env[key] = value } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("go env: failed parsing: %w", err) } return env, nil } func goversionm(ctx context.Context, file string, appPath string, appFileName string, se oci.SignedEntity, dir string) ([]byte, types.MediaType, error) { gobin := getGoBinary() switch se.(type) { case oci.SignedImage: sbom := bytes.NewBuffer(nil) cmd := exec.CommandContext(ctx, gobin, "version", "-m", file) cmd.Stdout = sbom cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return nil, "", fmt.Errorf("go version -m %s: %w", file, err) } // In order to get deterministics SBOMs replace our randomized // file name with the path the app will get inside of the container. s := []byte(strings.Replace(sbom.String(), file, appPath, 1)) if err := writeSBOM(s, appFileName, dir, "go.version-m"); err != nil { return nil, "", fmt.Errorf("writing sbom: %w", err) } return s, "application/vnd.go.version-m", nil case oci.SignedImageIndex: return nil, "", nil default: return nil, "", fmt.Errorf("unrecognized type: %T", se) } } func spdx(version string) sbomber { return func(ctx context.Context, file string, appPath string, appFileName string, se oci.SignedEntity, dir string) ([]byte, types.MediaType, error) { switch obj := se.(type) { case oci.SignedImage: b, _, err := goversionm(ctx, file, appPath, "", obj, "") if err != nil { return nil, "", err } b, err = sbom.GenerateImageSPDX(version, b, obj) if err != nil { return nil, "", err } if err := writeSBOM(b, appFileName, dir, "spdx.json"); err != nil { return nil, "", err } return b, ctypes.SPDXJSONMediaType, nil case oci.SignedImageIndex: b, err := sbom.GenerateIndexSPDX(version, obj) if err != nil { return nil, "", err } if err := writeSBOM(b, appFileName, dir, "spdx.json"); err != nil { return nil, "", err } return b, ctypes.SPDXJSONMediaType, err default: return nil, "", fmt.Errorf("unrecognized type: %T", se) } } } func writeSBOM(sbom []byte, appFileName, dir, ext string) error { if dir != "" { sbomDir := filepath.Clean(dir) if err := os.MkdirAll(sbomDir, os.ModePerm); err != nil { return err } sbomPath := filepath.Join(sbomDir, appFileName+"."+ext) log.Printf("Writing SBOM to %s", sbomPath) return os.WriteFile(sbomPath, sbom, 0644) //nolint:gosec } return nil } // buildEnv creates the environment variables used by the `go build` command. // From `os/exec.Cmd`: If there are duplicate environment keys, only the last // value in the slice for each duplicate key is used. func buildEnv(platform v1.Platform, osEnv, buildEnv []string) ([]string, error) { // Default env env := []string{ "CGO_ENABLED=0", "GOOS=" + platform.OS, "GOARCH=" + platform.Architecture, } if platform.Variant != "" { switch platform.Architecture { case "arm": // See: https://pkg.go.dev/cmd/go#hdr-Environment_variables goarm, err := getGoarm(platform) if err != nil { return nil, fmt.Errorf("goarm failure: %w", err) } if goarm != "" { env = append(env, "GOARM="+goarm) } case "amd64": // See: https://tip.golang.org/doc/go1.18#amd64 env = append(env, "GOAMD64="+platform.Variant) } } env = append(env, osEnv...) env = append(env, buildEnv...) return env, nil } func appFilename(importpath string) string { base := filepath.Base(importpath) // If we fail to determine a good name from the importpath then use a // safe default. if base == "." || base == string(filepath.Separator) { return defaultAppFilename } return base } // userOwnerAndGroupSID is a magic value needed to make the binary executable // in a Windows container. // // owner: BUILTIN/Users group: BUILTIN/Users ($sddlValue="O:BUG:BU") const userOwnerAndGroupSID = "AQAAgBQAAAAkAAAAAAAAAAAAAAABAgAAAAAABSAAAAAhAgAAAQIAAAAAAAUgAAAAIQIAAA==" func tarBinary(name, binary string, platform *v1.Platform, opts *layerOptions) (*bytes.Buffer, error) { buf := bytes.NewBuffer(nil) tw := tar.NewWriter(buf) defer tw.Close() // Write the parent directories to the tarball archive. // For Windows, the layer must contain a Hives/ directory, and the root // of the actual filesystem goes in a Files/ directory. // For Linux, the binary goes into /ko-app/ dirs := []string{"ko-app"} if platform.OS == "windows" { dirs = []string{ "Hives", "Files", "Files/ko-app", } name = "Files" + name } for _, dir := range dirs { if err := tw.WriteHeader(&tar.Header{ Name: dir, Typeflag: tar.TypeDir, // Use a fixed Mode, so that this isn't sensitive to the directory and umask // under which it was created. Additionally, windows can only set 0222, // 0444, or 0666, none of which are executable. Mode: 0555, }); err != nil { return nil, fmt.Errorf("writing dir %q to tar: %w", dir, err) } } file, err := os.Open(binary) if err != nil { return nil, fmt.Errorf("opening binary: %w", err) } defer file.Close() stat, err := file.Stat() if err != nil { return nil, err } header := &tar.Header{ Name: name, Size: stat.Size(), Typeflag: tar.TypeReg, // Use a fixed Mode, so that this isn't sensitive to the directory and umask // under which it was created. Additionally, windows can only set 0222, // 0444, or 0666, none of which are executable. Mode: 0555, PAXRecords: map[string]string{}, } switch platform.OS { case "windows": // This magic value is for some reason needed for Windows to be // able to execute the binary. header.PAXRecords["MSWINDOWS.rawsd"] = userOwnerAndGroupSID case "linux": if opts.linuxCapabilities != nil { xattr, err := opts.linuxCapabilities.ToXattrBytes() if err != nil { return nil, fmt.Errorf("caps.FileCaps.ToXattrBytes: %w", err) } header.PAXRecords["SCHILY.xattr.security.capability"] = string(xattr) } } // write the header to the tarball archive if err := tw.WriteHeader(header); err != nil { return nil, fmt.Errorf("writing tar header: %w", err) } // copy the file data to the tarball if _, err := io.Copy(tw, file); err != nil { return nil, fmt.Errorf("copying file to tar: %w", err) } return buf, nil } func (g *gobuild) kodataPath(ref reference) (string, error) { dir := filepath.Clean(g.dir) if dir == "." { dir = "" } pkgs, err := packages.Load(&packages.Config{Dir: dir, Mode: packages.NeedFiles}, ref.Path()) if err != nil { return "", fmt.Errorf("error loading package from %s: %w", ref.Path(), err) } if len(pkgs) != 1 { return "", fmt.Errorf("found %d local packages, expected 1", len(pkgs)) } if len(pkgs[0].GoFiles) == 0 { return "", fmt.Errorf("package %s contains no Go files", pkgs[0]) } return filepath.Join(filepath.Dir(pkgs[0].GoFiles[0]), "kodata"), nil } // Where kodata lives in the image. const kodataRoot = "/var/run/ko" // writeDirToTar writes a directory header to the tar writer. func writeDirToTar(tw *tar.Writer, name string, modTime time.Time) error { return tw.WriteHeader(&tar.Header{ Name: name, Typeflag: tar.TypeDir, // Use a fixed Mode, so that this isn't sensitive to the directory and umask // under which it was created. Additionally, windows can only set 0222, // 0444, or 0666, none of which are executable. Mode: 0555, ModTime: modTime, }) } // writeFileToTar writes a file to the tar writer. func writeFileToTar(tw *tar.Writer, name, evalPath string, size int64, modTime time.Time, platform *v1.Platform) error { file, err := os.Open(evalPath) if err != nil { return fmt.Errorf("os.Open(%q): %w", evalPath, err) } defer file.Close() header := &tar.Header{ Name: name, Size: size, Typeflag: tar.TypeReg, // Use a fixed Mode, so that this isn't sensitive to the directory and umask // under which it was created. Additionally, windows can only set 0222, // 0444, or 0666, none of which are executable. Mode: 0555, ModTime: modTime, } if platform.OS == "windows" { // This magic value is for some reason needed for Windows to be // able to execute the binary. header.PAXRecords = map[string]string{ "MSWINDOWS.rawsd": userOwnerAndGroupSID, } } if err := tw.WriteHeader(header); err != nil { return fmt.Errorf("tar.Writer.WriteHeader(%q): %w", name, err) } if _, err := io.Copy(tw, file); err != nil { return fmt.Errorf("io.Copy(%q, %q): %w", name, evalPath, err) } return nil } // walkRecursive performs a filepath.Walk of the given root directory adding it // to the provided tar.Writer with root -> chroot. All symlinks are dereferenced, // which is what leads to recursion when we encounter a directory symlink. func walkRecursive(tw *tar.Writer, root, chroot string, creationTime v1.Time, platform *v1.Platform) error { return filepath.Walk(root, func(hostPath string, info os.FileInfo, err error) error { if hostPath == root { return nil } if err != nil { return fmt.Errorf("filepath.Walk(%q): %w", root, err) } newPath := path.Join(chroot, filepath.ToSlash(hostPath[len(root):])) // Handle directories: write header and let filepath.Walk recurse. if info.Mode().IsDir() { if err := writeDirToTar(tw, newPath, creationTime.Time); err != nil { return fmt.Errorf("writing dir %q to tar: %w", newPath, err) } return nil } // Don't chase symlinks on Windows, where cross-compiled symlink support is not possible. if platform.OS == "windows" { if info.Mode()&os.ModeSymlink != 0 { log.Println("skipping symlink in kodata for windows:", info.Name()) return nil } } evalPath, err := filepath.EvalSymlinks(hostPath) if err != nil { return fmt.Errorf("filepath.EvalSymlinks(%q): %w", hostPath, err) } // Get info of the symlink target. info, err = os.Stat(evalPath) if err != nil { return fmt.Errorf("os.Stat(%q): %w", evalPath, err) } // Symlink target is a directory: write header and recurse. if info.Mode().IsDir() { if err := writeDirToTar(tw, newPath, creationTime.Time); err != nil { return fmt.Errorf("writing dir %q to tar: %w", newPath, err) } return walkRecursive(tw, evalPath, newPath, creationTime, platform) } // Regular file (or symlink to file): write to tar. return writeFileToTar(tw, newPath, evalPath, info.Size(), creationTime.Time, platform) }) } func (g *gobuild) tarKoData(ref reference, platform *v1.Platform) (*bytes.Buffer, error) { buf := bytes.NewBuffer(nil) tw := tar.NewWriter(buf) defer tw.Close() root, err := g.kodataPath(ref) if err != nil { return nil, err } creationTime := g.kodataCreationTime // Write the parent directories to the tarball archive. // For Windows, the layer must contain a Hives/ directory, and the root // of the actual filesystem goes in a Files/ directory. // For Linux, kodata starts at /var/run/ko. chroot := kodataRoot dirs := []string{ "/var", "/var/run", "/var/run/ko", } if platform.OS == "windows" { chroot = "Files" + kodataRoot dirs = []string{ "Hives", "Files", "Files/var", "Files/var/run", "Files/var/run/ko", } } for _, dir := range dirs { if err := writeDirToTar(tw, dir, creationTime.Time); err != nil { return nil, fmt.Errorf("writing dir %q: %w", dir, err) } } return buf, walkRecursive(tw, root, chroot, creationTime, platform) } func createTemplateData(ctx context.Context, buildCtx buildContext) (map[string]any, error) { envVars := map[string]string{ "LDFLAGS": "", } for _, entry := range buildCtx.env { kv := strings.SplitN(entry, "=", 2) if len(kv) != 2 { return nil, fmt.Errorf("invalid environment variable entry: %q", entry) } envVars[kv[0]] = kv[1] } // Get the go environment. goEnv, err := goenv(ctx) if err != nil { return nil, err } // Override go env with any matching values from the environment variables. for k, v := range envVars { if _, ok := goEnv[k]; ok { goEnv[k] = v } } // Get the git information, if available. info, err := git.GetInfo(ctx, buildCtx.dir) if err != nil { log.Printf("%v", err) } // Use the creation time as the build date, if provided. date := buildCtx.creationTime.Time if date.IsZero() { date = time.Now() } return map[string]any{ "Env": envVars, "GoEnv": goEnv, "Git": info.TemplateValue(), "Date": date.Format(time.RFC3339), "Timestamp": date.UTC().Unix(), }, nil } func applyTemplating(list []string, data map[string]any) ([]string, error) { result := make([]string, 0, len(list)) for _, entry := range list { tmpl, err := template.New("argsTmpl").Option("missingkey=error").Parse(entry) if err != nil { return nil, err } var buf bytes.Buffer if err := tmpl.Execute(&buf, data); err != nil { return nil, err } result = append(result, buf.String()) } return result, nil } func createBuildArgs(ctx context.Context, buildCtx buildContext) ([]string, error) { var args []string data, err := createTemplateData(ctx, buildCtx) if err != nil { return nil, err } if len(buildCtx.flags) > 0 { flags, err := applyTemplating(buildCtx.flags, data) if err != nil { return nil, err } args = append(args, flags...) } if len(buildCtx.ldflags) > 0 { ldflags, err := applyTemplating(buildCtx.ldflags, data) if err != nil { return nil, err } args = append(args, fmt.Sprintf("-ldflags=%s", strings.Join(ldflags, " "))) } // Reject any flags that attempt to set --toolexec (with or // without =, with one or two -s) for _, a := range args { for _, d := range []string{"-", "--"} { if a == d+"toolexec" || strings.HasPrefix(a, d+"toolexec=") { return nil, fmt.Errorf("cannot set %s", a) } } } return args, nil } func (g *gobuild) configForImportPath(ip string) Config { config := g.buildConfigs[ip] // Apply defaultFlags before any flag manipulation (trimpath, gcflags, etc.) // so that the emptiness check works on the original per-build flags. if len(config.Flags) == 0 { config.Flags = g.defaultFlags } if g.trimpath { // The `-trimpath` flag removes file system paths from the resulting binary, to aid reproducibility. // Ref: https://pkg.go.dev/cmd/go#hdr-Compile_packages_and_dependencies config.Flags = append(config.Flags, "-trimpath") } if g.disableOptimizations { // Disable optimizations (-N) and inlining (-l). config.Flags = append(config.Flags, "-gcflags", "all=-N -l") } if config.ID != "" { log.Printf("Using build config %s for %s", config.ID, ip) } return config } func (g gobuild) useDebugging(platform v1.Platform) bool { return g.debug && doesPlatformSupportDebugging(platform) } func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, platform *v1.Platform) (oci.SignedImage, error) { if err := g.semaphore.Acquire(ctx, 1); err != nil { return nil, err } defer g.semaphore.Release(1) ref := newRef(refStr) // Layers should be typed to match the underlying image, since some // registries reject mixed-type layers. var layerMediaType types.MediaType mt, err := base.MediaType() if err != nil { return nil, err } switch mt { case types.OCIManifestSchema1: layerMediaType = types.OCILayer case types.DockerManifestSchema2: layerMediaType = types.DockerLayer } cf, err := base.ConfigFile() if err != nil { return nil, err } if platform == nil { if cf.OS == "" { cf.OS = "linux" } if cf.Architecture == "" { cf.Architecture = "amd64" } platform = &v1.Platform{ OS: cf.OS, Architecture: cf.Architecture, OSVersion: cf.OSVersion, } } if g.debug && !doesPlatformSupportDebugging(*platform) { log.Printf("image for platform %q will be built without debugging enabled because debugging is not supported for that platform", *platform) } if !g.platformMatcher.matches(platform) { return nil, fmt.Errorf("base image platform %q does not match desired platforms %v", platform, g.platformMatcher.platforms) } config := g.configForImportPath(ref.Path()) // Merge the system and build environment variables. env := config.Env if len(env) == 0 { // Use the default, if any. env = g.defaultEnv } env, err = buildEnv(*platform, os.Environ(), env) if err != nil { return nil, fmt.Errorf("could not create env for %s: %w", ref.Path(), err) } // Get the build flags (defaultFlags already applied in configForImportPath). flags := config.Flags // Get the build ldflags. ldflags := config.Ldflags if len(ldflags) == 0 { // Use the default, if any ldflags = g.defaultLdflags } // Do the build into a temporary file. file, err := g.build(ctx, buildContext{ creationTime: g.creationTime, ip: ref.Path(), dir: g.dir, env: env, flags: flags, ldflags: ldflags, platform: *platform, }) if err != nil { return nil, fmt.Errorf("build: %w", err) } if os.Getenv("KOCACHE") == "" { defer os.RemoveAll(filepath.Dir(file)) } var layers []mutate.Addendum // Create a layer from the kodata directory under this import path. dataLayerBuf, err := g.tarKoData(ref, platform) if err != nil { return nil, fmt.Errorf("tarring kodata: %w", err) } dataLayerBytes := dataLayerBuf.Bytes() dataLayer, err := tarball.LayerFromOpener(func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewBuffer(dataLayerBytes)), nil }, tarball.WithCompressedCaching, tarball.WithMediaType(layerMediaType)) if err != nil { return nil, err } layers = append(layers, mutate.Addendum{ Layer: dataLayer, History: v1.History{ Author: "ko", CreatedBy: "ko build " + ref.String(), Created: g.kodataCreationTime, Comment: "kodata contents, at $KO_DATA_PATH", }, }) appDir := "/ko-app" appFileName := appFilename(ref.Path()) appPath := path.Join(appDir, appFileName) var lo layerOptions lo.linuxCapabilities, err = caps.NewFileCaps(config.LinuxCapabilities...) if err != nil { return nil, fmt.Errorf("linux_capabilities: %w", err) } miss := func() (v1.Layer, error) { return buildLayer(appPath, file, platform, layerMediaType, &lo) } var binaryLayer v1.Layer switch { case lo.linuxCapabilities != nil: log.Printf("Some options prevent us from using layer cache") binaryLayer, err = miss() default: binaryLayer, err = g.cache.get(ctx, file, miss) } if err != nil { return nil, fmt.Errorf("cache.get(%q): %w", file, err) } layers = append(layers, mutate.Addendum{ Layer: binaryLayer, MediaType: layerMediaType, History: v1.History{ Author: "ko", Created: g.creationTime, CreatedBy: "ko build " + ref.String(), Comment: "go build output, at " + appPath, }, }) delvePath := "" // path for delve in image if g.useDebugging(*platform) { // get delve locally delveBinary, err := getDelve(ctx, *platform) if err != nil { return nil, fmt.Errorf("building Delve: %w", err) } defer os.RemoveAll(filepath.Dir(delveBinary)) delvePath = path.Join("/ko-app", filepath.Base(delveBinary)) // add layer with delve binary delveLayer, err := g.cache.get(ctx, delveBinary, func() (v1.Layer, error) { return buildLayer(delvePath, delveBinary, platform, layerMediaType, &lo) }) if err != nil { return nil, fmt.Errorf("cache.get(%q): %w", delveBinary, err) } layers = append(layers, mutate.Addendum{ Layer: delveLayer, MediaType: layerMediaType, History: v1.History{ Author: "ko", Created: g.creationTime, CreatedBy: "ko build " + ref.String(), Comment: "Delve debugger, at " + delvePath, }, }) } delveArgs := []string{ "exec", "--listen=:40000", "--headless", "--log", "--accept-multiclient", "--api-version=2", "--", } // Augment the base image with our application layer. withApp, err := mutate.Append(base, layers...) if err != nil { return nil, err } // Start from a copy of the base image's config file, and set // the entrypoint to our app. cfg, err := withApp.ConfigFile() if err != nil { return nil, err } cfg = cfg.DeepCopy() cfg.Config.Entrypoint = []string{appPath} cfg.Config.Cmd = nil if platform.OS == "windows" { appPath := `C:\ko-app\` + appFileName if g.debug { cfg.Config.Entrypoint = append([]string{"C:\\" + delvePath}, delveArgs...) cfg.Config.Entrypoint = append(cfg.Config.Entrypoint, appPath) } else { cfg.Config.Entrypoint = []string{appPath} } updatePath(cfg, `C:\ko-app`) cfg.Config.Env = append(cfg.Config.Env, `KO_DATA_PATH=C:\var\run\ko`) } else { if g.useDebugging(*platform) { cfg.Config.Entrypoint = append([]string{delvePath}, delveArgs...) cfg.Config.Entrypoint = append(cfg.Config.Entrypoint, appPath) } updatePath(cfg, appDir) cfg.Config.Env = append(cfg.Config.Env, "KO_DATA_PATH="+kodataRoot) } cfg.Author = "github.com/ko-build/ko" if cfg.Config.Labels == nil { cfg.Config.Labels = map[string]string{} } maps.Copy(cfg.Config.Labels, g.labels) if g.user != "" { cfg.Config.User = g.user } empty := v1.Time{} if g.creationTime != empty { cfg.Created = g.creationTime } image, err := mutate.ConfigFile(withApp, cfg) if err != nil { return nil, err } si := signed.Image(image) if g.sbom != nil { // Construct a path-safe encoding of platform. pf := strings.ReplaceAll(strings.ReplaceAll(platform.String(), "/", "-"), ":", "-") sbom, mt, err := g.sbom(ctx, file, appPath, fmt.Sprintf("%s-%s", appFileName, pf), si, g.sbomDir) if err != nil { return nil, err } f, err := static.NewFile(sbom, static.WithLayerMediaType(mt)) if err != nil { return nil, err } si, err = ocimutate.AttachFileToImage(si, "sbom", f) if err != nil { return nil, err } } return si, nil } // layerOptions captures additional options to apply when authoring layer type layerOptions struct { linuxCapabilities *caps.FileCaps } func buildLayer(appPath, file string, platform *v1.Platform, layerMediaType types.MediaType, opts *layerOptions) (v1.Layer, error) { // Construct a tarball with the binary and produce a layer. binaryLayerBuf, err := tarBinary(appPath, file, platform, opts) if err != nil { return nil, fmt.Errorf("tarring binary: %w", err) } binaryLayerBytes := binaryLayerBuf.Bytes() return tarball.LayerFromOpener(func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewBuffer(binaryLayerBytes)), nil }, tarball.WithCompressedCaching, tarball.WithMediaType(layerMediaType)) } // Append appPath to the PATH environment variable, if it exists. Otherwise, // set the PATH environment variable to appPath. func updatePath(cf *v1.ConfigFile, appPath string) { for i, env := range cf.Config.Env { parts := strings.SplitN(env, "=", 2) if len(parts) != 2 { // Expect environment variables to be in the form KEY=VALUE, so this is unexpected. continue } key, value := parts[0], parts[1] if key == "PATH" { value = fmt.Sprintf("%s:%s", value, appPath) cf.Config.Env[i] = "PATH=" + value return } } // If we get here, we never saw PATH. cf.Config.Env = append(cf.Config.Env, "PATH="+appPath) } // Build implements build.Interface func (g *gobuild) Build(ctx context.Context, s string) (Result, error) { // Determine the appropriate base image for this import path. // We use the overall gobuild.ctx because the Build ctx gets cancelled // early, and we lazily use the ctx within ggcr's remote package. baseRef, base, err := g.getBase(g.ctx, s) if err != nil { return nil, fmt.Errorf("fetching base image: %w", err) } // Determine what kind of base we have and if we should publish an image or an index. mt, err := base.MediaType() if err != nil { return nil, err } // Annotate the base image we pass to the build function with // annotations indicating the digest (and possibly tag) of the // base image. This will be inherited by the image produced. if mt != types.DockerManifestList { baseDigest, err := base.Digest() if err != nil { return nil, err } annotations := maps.Clone(g.annotations) annotations[specsv1.AnnotationBaseImageDigest] = baseDigest.String() annotations[specsv1.AnnotationBaseImageName] = baseRef.Name() base = mutate.Annotations(base, annotations).(Result) } switch mt { case types.OCIImageIndex, types.DockerManifestList: baseIndex, ok := base.(v1.ImageIndex) if !ok { return nil, fmt.Errorf("failed to interpret base as index: %v", base) } return g.buildAll(ctx, s, baseRef, baseIndex) case types.OCIManifestSchema1, types.DockerManifestSchema2: baseImage, ok := base.(v1.Image) if !ok { return nil, fmt.Errorf("failed to interpret base as image: %v", base) } return g.buildOne(ctx, s, baseImage, nil) default: return nil, fmt.Errorf("base image media type: %s", mt) } } func (g *gobuild) buildAll(ctx context.Context, ref string, baseRef name.Reference, baseIndex v1.ImageIndex) (Result, error) { im, err := baseIndex.IndexManifest() if err != nil { return nil, err } matches := make([]v1.Descriptor, 0) for _, desc := range im.Manifests { // Nested index is pretty rare. We could support this in theory, but return an error for now. if desc.MediaType != types.OCIManifestSchema1 && desc.MediaType != types.DockerManifestSchema2 { return nil, fmt.Errorf("%q has unexpected mediaType %q in base for %q", desc.Digest, desc.MediaType, ref) } if g.platformMatcher.matches(desc.Platform) { matches = append(matches, desc) } } if len(matches) == 0 { return nil, errors.New("no matching platforms in base image index") } if len(matches) == 1 { // Filters resulted in a single matching platform; just produce // a single-platform image. img, err := baseIndex.Image(matches[0].Digest) if err != nil { return nil, fmt.Errorf("error getting matching image from index: %w", err) } annotations := maps.Clone(g.annotations) // Decorate the image with the ref of the index, and the matching // platform's digest. annotations[specsv1.AnnotationBaseImageDigest] = matches[0].Digest.String() annotations[specsv1.AnnotationBaseImageName] = baseRef.Name() img = mutate.Annotations(img, annotations).(v1.Image) return g.buildOne(ctx, ref, img, matches[0].Platform) } annotations := maps.Clone(g.annotations) annotations[specsv1.AnnotationBaseImageName] = baseRef.Name() baseDigest, _ := baseIndex.Digest() annotations[specsv1.AnnotationBaseImageDigest] = baseDigest.String() // Build an image for each matching platform from the base and append // it to a new index to produce the result. We use the indices to // preserve the base image ordering here. errg, gctx := errgroup.WithContext(ctx) adds := make([]ocimutate.IndexAddendum, len(matches)) for i, desc := range matches { errg.Go(func() error { baseImage, err := baseIndex.Image(desc.Digest) if err != nil { return err } annotations := maps.Clone(g.annotations) // Decorate the image with the ref of the index, and the matching // platform's digest. The ref of the index encodes the critical // repository information for fetching the base image's digest, but // we leave `name` pointing at the index's full original ref to that // folks could conceivably check for updates to the index over time. // While the `digest` doesn't give us enough information to check // for changes with a simple HEAD (because we need the full index // manifest to get the per-architecture image), that optimization // mainly matters for DockerHub where HEAD's are exempt from rate // limiting. However, in practice, the way DockerHub updates the // indices for official images is to rebuild per-arch images and // replace the per-arch images in the existing index, so an index // with N manifest receives N updates. If we only record the digest // of the index here, then we cannot tell when the index updates are // no-ops for us because we didn't record the digest of the actual // image we used, and we would potentially end up doing Nx more work // than we really need to do. annotations[specsv1.AnnotationBaseImageDigest] = desc.Digest.String() annotations[specsv1.AnnotationBaseImageName] = baseRef.Name() baseImage = mutate.Annotations(baseImage, annotations).(v1.Image) img, err := g.buildOne(gctx, ref, baseImage, desc.Platform) if err != nil { return err } adds[i] = ocimutate.IndexAddendum{ Add: img, Descriptor: v1.Descriptor{ URLs: desc.URLs, MediaType: desc.MediaType, Annotations: desc.Annotations, Platform: desc.Platform, }, } return nil }) } if err := errg.Wait(); err != nil { return nil, err } baseType, err := baseIndex.MediaType() if err != nil { return nil, err } idx := ocimutate.AppendManifests( mutate.Annotations( mutate.IndexMediaType(empty.Index, baseType), annotations).(v1.ImageIndex), adds...) if g.sbom != nil { ref := newRef(ref) appFileName := appFilename(ref.Path()) sbom, mt, err := g.sbom(ctx, "", "", fmt.Sprintf("%s-index", appFileName), idx, g.sbomDir) if err != nil { return nil, err } if sbom != nil { f, err := static.NewFile(sbom, static.WithLayerMediaType(mt)) if err != nil { return nil, err } idx, err = ocimutate.AttachFileToImageIndex(idx, "sbom", f) if err != nil { return nil, err } } } return idx, nil } func parseSpec(spec []string) (*platformMatcher, error) { // Don't bother parsing "all". // Empty slice should never happen because we default to linux/amd64 (or GOOS/GOARCH). if len(spec) == 0 || spec[0] == "all" { return &platformMatcher{spec: spec}, nil } platforms := make([]v1.Platform, 0) for _, s := range spec { p, err := v1.ParsePlatform(s) if err != nil { return nil, err } platforms = append(platforms, *p) } return &platformMatcher{spec: spec, platforms: platforms}, nil } func (pm *platformMatcher) matches(base *v1.Platform) bool { // Strip out manifests with "unknown/unknown" platform, which Docker uses // to store provenance attestations. if base != nil && (base.OS == "unknown" || base.Architecture == "unknown") { return false } if len(pm.spec) > 0 && pm.spec[0] == "all" { return true } // Don't build anything without a platform field unless "all". Unclear what we should do here. if base == nil { return false } for _, p := range pm.platforms { if p.OS != "" && base.OS != p.OS { continue } if p.Architecture != "" && base.Architecture != p.Architecture { continue } if p.Variant != "" && base.Variant != p.Variant { continue } // Windows is... weird. Windows base images use osversion to // communicate what Windows version is used, which matters for image // selection at runtime. // // Windows osversions include the usual major/minor/patch version // components, as well as an incrementing "build number" which can // change when new Windows base images are released. // // In order to avoid having to match the entire osversion including the // incrementing build number component, we allow matching a platform // that only matches the first three osversion components, only for // Windows images. // // If the X.Y.Z components don't match (or aren't formed as we expect), // the platform doesn't match. Only if X.Y.Z matches and the extra // build number component doesn't, do we consider the platform to // match. // // Ref: https://docs.microsoft.com/en-us/virtualization/windowscontainers/deploy-containers/version-compatibility?tabs=windows-server-2022%2Cwindows-10-21H1#build-number-new-release-of-windows if p.OSVersion != "" && p.OSVersion != base.OSVersion { if p.OS != "windows" { // osversion mismatch is only possibly allowed when os == windows. continue } if pcount, bcount := strings.Count(p.OSVersion, "."), strings.Count(base.OSVersion, "."); pcount == 2 && bcount == 3 { if p.OSVersion != base.OSVersion[:strings.LastIndex(base.OSVersion, ".")] { // If requested osversion is X.Y.Z and potential match is X.Y.Z.A, all of X.Y.Z must match. // Any other form of these osversions are not a match. continue } } else { // Partial osversion matching only allows X.Y.Z to match X.Y.Z.A. continue } } return true } return false } ================================================ FILE: pkg/build/gobuild_test.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package build import ( "archive/tar" "context" "errors" "fmt" "io" "os" "path" "path/filepath" "runtime" "strconv" "strings" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/random" "github.com/google/go-containerregistry/pkg/v1/types" "github.com/google/ko/pkg/internal/gittesting" specsv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sigstore/cosign/v3/pkg/oci" "github.com/stretchr/testify/require" ) func repoRootDir() (string, error) { _, filename, _, ok := runtime.Caller(0) if !ok { return "", fmt.Errorf("could not get current filename") } basepath := filepath.Dir(filename) repoDir := filepath.Join(basepath, "..", "..") return filepath.Rel(basepath, repoDir) } func TestGoBuildQualifyImport(t *testing.T) { base, err := random.Image(1024, 1) if err != nil { t.Fatalf("random.Image() = %v", err) } repoDir, err := repoRootDir() if err != nil { t.Fatalf("could not get Git repository root directory") } tests := []struct { description string rawImportpath string dir string qualifiedImportpath string expectError bool }{ { description: "strict qualified import path", rawImportpath: "ko://github.com/google/ko", dir: "", qualifiedImportpath: "ko://github.com/google/ko", expectError: false, }, { description: "strict qualified import path in subdirectory of go.mod", rawImportpath: "ko://github.com/google/ko/test", dir: "", qualifiedImportpath: "ko://github.com/google/ko/test", expectError: false, }, { description: "non-strict qualified import path", rawImportpath: "github.com/google/ko", dir: "", qualifiedImportpath: "ko://github.com/google/ko", expectError: false, }, { description: "non-strict local import path in repository root directory", rawImportpath: "./test", dir: repoDir, qualifiedImportpath: "ko://github.com/google/ko/test", expectError: false, }, { description: "non-strict local import path in subdirectory", rawImportpath: ".", dir: filepath.Join(repoDir, "test"), qualifiedImportpath: "ko://github.com/google/ko/test", expectError: false, }, { description: "non-existent non-strict local import path", rawImportpath: "./does-not-exist", dir: "/", qualifiedImportpath: "should return error", expectError: true, }, } for _, test := range tests { t.Run(test.description, func(t *testing.T) { ng, err := NewGo(context.Background(), test.dir, WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return nil, base, nil })) if err != nil { t.Fatalf("NewGo() = %v", err) } gotImportpath, err := ng.QualifyImport(test.rawImportpath) if err != nil && test.expectError { return } if err != nil && !test.expectError { t.Errorf("QualifyImport(dir=%q)(%q) was error (%v), want nil error", test.dir, test.rawImportpath, err) } if err == nil && test.expectError { t.Errorf("QualifyImport(dir=%q)(%q) was nil error, want non-nil error", test.dir, test.rawImportpath) } if gotImportpath != test.qualifiedImportpath { t.Errorf("QualifyImport(dir=%q)(%q) = (%q, nil), want (%q, nil)", test.dir, test.rawImportpath, gotImportpath, test.qualifiedImportpath) } }) } } var baseRef = name.MustParseReference("all.your/base") func TestGoBuildIsSupportedRef(t *testing.T) { base, err := random.Image(1024, 3) if err != nil { t.Fatalf("random.Image() = %v", err) } ng, err := NewGo(context.Background(), "", WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return nil, base, nil })) if err != nil { t.Fatalf("NewGo() = %v", err) } // Supported import paths. for _, importpath := range []string{ "ko://github.com/google/ko", // ko can build itself. } { t.Run(importpath, func(t *testing.T) { if err := ng.IsSupportedReference(importpath); err != nil { t.Errorf("IsSupportedReference(%q) = (%v), want nil", importpath, err) } }) } // Unsupported import paths. for _, importpath := range []string{ "ko://github.com/google/ko/pkg/build", // not a command. "ko://github.com/google/ko/pkg/nonexistent", // does not exist. } { t.Run(importpath, func(t *testing.T) { if err := ng.IsSupportedReference(importpath); err == nil { t.Errorf("IsSupportedReference(%v) = nil, want error", importpath) } }) } } func TestGoBuildIsSupportedRefWithModules(t *testing.T) { base, err := random.Image(1024, 3) if err != nil { t.Fatalf("random.Image() = %v", err) } opts := []Option{ WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }), } ng, err := NewGo(context.Background(), "", opts...) if err != nil { t.Fatalf("NewGo() = %v", err) } // Supported import paths. for _, importpath := range []string{ "ko://github.com/google/ko/test", // ko can build the test package. "ko://github.com/go-training/helloworld", // ko can build commands in dependent modules } { t.Run(importpath, func(t *testing.T) { if err := ng.IsSupportedReference(importpath); err != nil { t.Errorf("IsSupportedReference(%q) = (%v), want nil", err, importpath) } }) } // Unsupported import paths. for _, importpath := range []string{ "ko://github.com/google/ko/pkg/build", // not a command. "ko://github.com/google/ko/pkg/nonexistent", // does not exist. "ko://github.com/google/go-github", // not in this module. } { t.Run(importpath, func(t *testing.T) { if err := ng.IsSupportedReference(importpath); err == nil { t.Errorf("IsSupportedReference(%v) = nil, want error", importpath) } }) } } func TestBuildEnv(t *testing.T) { tests := []struct { description string platform v1.Platform osEnv []string buildEnv []string expectedEnvs map[string]string }{{ description: "defaults", platform: v1.Platform{ OS: "linux", Architecture: "amd64", }, expectedEnvs: map[string]string{ "GOOS": "linux", "GOARCH": "amd64", "CGO_ENABLED": "0", }, }, { description: "build override a system value", osEnv: []string{"CGO_ENABLED=0"}, buildEnv: []string{"CGO_ENABLED=1"}, expectedEnvs: map[string]string{ "CGO_ENABLED": "1", }, }, { description: "override an envvar and add an envvar", osEnv: []string{"CGO_ENABLED=0"}, buildEnv: []string{"CGO_ENABLED=1", "GOPRIVATE=git.internal.example.com,source.developers.google.com"}, expectedEnvs: map[string]string{ "CGO_ENABLED": "1", "GOPRIVATE": "git.internal.example.com,source.developers.google.com", }, }, { description: "arm variant", platform: v1.Platform{ Architecture: "arm", Variant: "v7", }, expectedEnvs: map[string]string{ "GOARCH": "arm", "GOARM": "7", }, }, { // GOARM is ignored for arm64. description: "arm64 variant", platform: v1.Platform{ Architecture: "arm64", Variant: "v8", }, expectedEnvs: map[string]string{ "GOARCH": "arm64", "GOARM": "", }, }, { description: "amd64 variant", platform: v1.Platform{ Architecture: "amd64", Variant: "v3", }, expectedEnvs: map[string]string{ "GOARCH": "amd64", "GOAMD64": "v3", }, }} for _, test := range tests { t.Run(test.description, func(t *testing.T) { env, err := buildEnv(test.platform, test.osEnv, test.buildEnv) if err != nil { t.Fatalf("unexpected error running buildEnv(): %v", err) } envs := map[string]string{} for _, e := range env { split := strings.SplitN(e, "=", 2) envs[split[0]] = split[1] } for key, val := range test.expectedEnvs { if envs[key] != val { t.Errorf("buildEnv(): expected %s=%s, got %s=%s", key, val, key, envs[key]) } } }) } } func TestGoEnv(t *testing.T) { goVars, err := goenv(context.TODO()) require.NoError(t, err) // Just check some basic values. require.Equal(t, runtime.GOOS, goVars["GOOS"]) require.Equal(t, runtime.GOARCH, goVars["GOARCH"]) } func TestCreateTemplateData(t *testing.T) { t.Run("empty creation time", func(t *testing.T) { params, err := createTemplateData(context.TODO(), buildContext{dir: t.TempDir()}) require.NoError(t, err) // Make sure the date was set to time.Now(). actualDateStr := params["Date"].(string) actualDate, err := time.Parse(time.RFC3339, actualDateStr) require.NoError(t, err) if time.Since(actualDate) > time.Minute { t.Fatalf("expected date to be now, but was %v", actualDate) } // Check the timestamp. actualTimestampSec := params["Timestamp"].(int64) actualTimestamp := time.Unix(actualTimestampSec, 0).UTC() expectedTimestamp := actualDate.Truncate(time.Second).UTC() require.Equal(t, expectedTimestamp, actualTimestamp) }) t.Run("creation time", func(t *testing.T) { // Create a reference time for use as a creation time. expectedTime, err := time.Parse(time.RFC3339, "2012-11-01T22:08:00Z") require.NoError(t, err) params, err := createTemplateData(context.TODO(), buildContext{ creationTime: v1.Time{Time: expectedTime}, dir: t.TempDir(), }) require.NoError(t, err) // Check the date. actualDateStr := params["Date"].(string) actualDate, err := time.Parse(time.RFC3339, actualDateStr) require.NoError(t, err) require.Equal(t, expectedTime, actualDate) // Check the timestamp. actualTimestampSec := params["Timestamp"].(int64) actualTimestamp := time.Unix(actualTimestampSec, 0).UTC() require.Equal(t, expectedTime, actualTimestamp) }) t.Run("no git available", func(t *testing.T) { dir := t.TempDir() params, err := createTemplateData(context.TODO(), buildContext{dir: dir}) require.NoError(t, err) gitParams := params["Git"].(map[string]any) require.Equal(t, "", gitParams["Branch"]) require.Equal(t, "", gitParams["Tag"]) require.Equal(t, "", gitParams["ShortCommit"]) require.Equal(t, "", gitParams["FullCommit"]) require.Equal(t, "clean", gitParams["TreeState"]) }) t.Run("git", func(t *testing.T) { // Create a fake git structure under the test temp dir. const fakeGitURL = "git@github.com:foo/bar.git" dir := t.TempDir() gittesting.GitInit(t, dir) gittesting.GitRemoteAdd(t, dir, fakeGitURL) gittesting.GitCommit(t, dir, "commit1") gittesting.GitTag(t, dir, "v0.0.1") params, err := createTemplateData(context.TODO(), buildContext{dir: dir}) require.NoError(t, err) gitParams := params["Git"].(map[string]any) require.Equal(t, "main", gitParams["Branch"]) require.Equal(t, "v0.0.1", gitParams["Tag"]) require.Equal(t, "clean", gitParams["TreeState"]) }) t.Run("env", func(t *testing.T) { params, err := createTemplateData(context.TODO(), buildContext{ dir: t.TempDir(), env: []string{"FOO=bar"}, }) require.NoError(t, err) vars := params["Env"].(map[string]string) require.Equal(t, "bar", vars["FOO"]) }) t.Run("bad env", func(t *testing.T) { _, err := createTemplateData(context.TODO(), buildContext{ dir: t.TempDir(), env: []string{"bad var"}, }) require.Error(t, err) }) t.Run("default go env", func(t *testing.T) { params, err := createTemplateData(context.TODO(), buildContext{dir: t.TempDir()}) require.NoError(t, err) vars := params["GoEnv"].(map[string]string) require.Equal(t, runtime.GOOS, vars["GOOS"]) require.Equal(t, runtime.GOARCH, vars["GOARCH"]) }) t.Run("env overrides go env", func(t *testing.T) { params, err := createTemplateData(context.TODO(), buildContext{ dir: t.TempDir(), env: []string{ "GOOS=testgoos", "GOARCH=testgoarch", }, }) require.NoError(t, err) vars := params["GoEnv"].(map[string]string) require.Equal(t, "testgoos", vars["GOOS"]) require.Equal(t, "testgoarch", vars["GOARCH"]) }) } func TestBuildConfig(t *testing.T) { tests := []struct { description string options []Option importpath string expectConfig Config }{ { description: "minimal options", options: []Option{ WithBaseImages(nilGetBase), }, }, { description: "trimpath flag", options: []Option{ WithBaseImages(nilGetBase), WithTrimpath(true), }, expectConfig: Config{ Flags: FlagArray{"-trimpath"}, }, }, { description: "no trimpath flag", options: []Option{ WithBaseImages(nilGetBase), WithTrimpath(false), }, }, { description: "build config and trimpath", options: []Option{ WithBaseImages(nilGetBase), WithConfig(map[string]Config{ "example.com/foo": { Flags: FlagArray{"-v"}, }, }), WithTrimpath(true), }, importpath: "example.com/foo", expectConfig: Config{ Flags: FlagArray{"-v", "-trimpath"}, }, }, { description: "no trimpath overridden by build config flag", options: []Option{ WithBaseImages(nilGetBase), WithConfig(map[string]Config{ "example.com/bar": { Flags: FlagArray{"-trimpath"}, }, }), WithTrimpath(false), }, importpath: "example.com/bar", expectConfig: Config{ Flags: FlagArray{"-trimpath"}, }, }, { description: "disable optimizations", options: []Option{ WithBaseImages(nilGetBase), WithDisabledOptimizations(), }, expectConfig: Config{ Flags: FlagArray{"-gcflags", "all=-N -l"}, }, }, { description: "defaultFlags applied when no per-build flags", options: []Option{ WithBaseImages(nilGetBase), WithDefaultFlags([]string{"-v", "-tags", "netgo"}), }, expectConfig: Config{ Flags: FlagArray{"-v", "-tags", "netgo"}, }, }, { description: "defaultFlags applied with trimpath", options: []Option{ WithBaseImages(nilGetBase), WithDefaultFlags([]string{"-v"}), WithTrimpath(true), }, expectConfig: Config{ Flags: FlagArray{"-v", "-trimpath"}, }, }, { description: "per-build flags override defaultFlags", options: []Option{ WithBaseImages(nilGetBase), WithConfig(map[string]Config{ "example.com/foo": { Flags: FlagArray{"-race"}, }, }), WithDefaultFlags([]string{"-v"}), WithTrimpath(true), }, importpath: "example.com/foo", expectConfig: Config{ Flags: FlagArray{"-race", "-trimpath"}, }, }, } for _, test := range tests { t.Run(test.description, func(t *testing.T) { i, err := NewGo(context.Background(), "", test.options...) if err != nil { t.Fatalf("NewGo(): unexpected error: %+v", err) } gb, ok := i.(*gobuild) if !ok { t.Fatal("NewGo() did not return *gobuild{} as expected") } config := gb.configForImportPath(test.importpath) if diff := cmp.Diff(test.expectConfig, config, cmpopts.EquateEmpty(), cmpopts.SortSlices(func(x, y string) bool { return x < y })); diff != "" { t.Errorf("%T differ (-got, +want): %s", test.expectConfig, diff) } }) } } func nilGetBase(context.Context, string) (name.Reference, Result, error) { return nil, nil, nil } const wantSBOM = "This is our fake SBOM" // A helper method we use to substitute for the default "build" method. func fauxSBOM(context.Context, string, string, string, oci.SignedEntity, string) ([]byte, types.MediaType, error) { return []byte(wantSBOM), "application/vnd.garbage", nil } // A helper method we use to substitute for the default "build" method. func writeTempFile(_ context.Context, buildCtx buildContext) (string, error) { tmpDir, err := os.MkdirTemp("", "ko") if err != nil { return "", err } file, err := os.CreateTemp(tmpDir, "out") if err != nil { return "", err } defer file.Close() if _, err := file.WriteString(filepath.ToSlash(buildCtx.ip)); err != nil { return "", err } return file.Name(), nil } func TestGoBuildNoKoData(t *testing.T) { baseLayers := int64(3) base, err := random.Image(1024, baseLayers) if err != nil { t.Fatalf("random.Image() = %v", err) } importpath := "github.com/google/ko" creationTime := v1.Time{Time: time.Unix(5000, 0)} ng, err := NewGo( context.Background(), "", WithCreationTime(creationTime), WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }), withBuilder(writeTempFile), withSBOMber(fauxSBOM), WithPlatforms("all"), ) if err != nil { t.Fatalf("NewGo() = %v", err) } result, err := ng.Build(context.Background(), StrictScheme+importpath) if err != nil { t.Fatalf("Build() = %v", err) } img, ok := result.(v1.Image) if !ok { t.Fatalf("Build() not an Image: %T", result) } ls, err := img.Layers() if err != nil { t.Fatalf("Layers() = %v", err) } // Check that we have the expected number of layers. t.Run("check layer count", func(t *testing.T) { // We get a layer for the go binary and a layer for the kodata/ if got, want := int64(len(ls)), baseLayers+2; got != want { t.Fatalf("len(Layers()) = %v, want %v", got, want) } }) // Check that rebuilding the image again results in the same image digest. t.Run("check determinism", func(t *testing.T) { result2, err := ng.Build(context.Background(), StrictScheme+importpath) if err != nil { t.Fatalf("Build() = %v", err) } d1, err := result.Digest() if err != nil { t.Fatalf("Digest() = %v", err) } d2, err := result2.Digest() if err != nil { t.Fatalf("Digest() = %v", err) } if d1 != d2 { t.Errorf("Digest mismatch: %s != %s", d1, d2) } }) // Check that the entrypoint of the image is configured to invoke our Go application t.Run("check entrypoint", func(t *testing.T) { cfg, err := img.ConfigFile() if err != nil { t.Errorf("ConfigFile() = %v", err) } entrypoint := cfg.Config.Entrypoint if got, want := len(entrypoint), 1; got != want { t.Errorf("len(entrypoint) = %v, want %v", got, want) } if got, want := entrypoint[0], "/ko-app/ko"; got != want { t.Errorf("entrypoint = %v, want %v", got, want) } }) t.Run("check creation time", func(t *testing.T) { cfg, err := img.ConfigFile() if err != nil { t.Errorf("ConfigFile() = %v", err) } actual := cfg.Created if actual.Time != creationTime.Time { t.Errorf("created = %v, want %v", actual, creationTime) } }) } func validateImage(t *testing.T, img oci.SignedImage, baseLayers int64, creationTime v1.Time, checkAnnotations bool, expectSBOM bool) { t.Helper() ls, err := img.Layers() if err != nil { t.Fatalf("Layers() = %v", err) } // Check that we have the expected number of layers. t.Run("check layer count", func(t *testing.T) { // We get a layer for the go binary and a layer for the kodata/ if got, want := int64(len(ls)), baseLayers+2; got != want { t.Fatalf("len(Layers()) = %v, want %v", got, want) } }) t.Run("check app layer contents", func(t *testing.T) { dataLayer := ls[baseLayers] if _, err := dataLayer.Digest(); err != nil { t.Errorf("Digest() = %v", err) } // We don't check the data layer here because it includes a symlink of refs and // will produce a distinct hash each time we commit something. r, err := dataLayer.Uncompressed() if err != nil { t.Errorf("Uncompressed() = %v", err) } defer r.Close() tr := tar.NewReader(r) if _, err := tr.Next(); errors.Is(err, io.EOF) { t.Errorf("Layer contained no files") } }) // Check that the kodata layer contains the expected data (even though it was a symlink // outside kodata). t.Run("check kodata", func(t *testing.T) { dataLayer := ls[baseLayers] r, err := dataLayer.Uncompressed() if err != nil { t.Errorf("Uncompressed() = %v", err) } defer r.Close() found := false tr := tar.NewReader(r) for { header, err := tr.Next() if errors.Is(err, io.EOF) { break } else if err != nil { t.Errorf("Next() = %v", err) continue } if header.Name != path.Join(kodataRoot, "kenobi") { continue } found = true body, err := io.ReadAll(tr) if err != nil { t.Errorf("ReadAll() = %v", err) } else if want, got := "Hello there\n", string(body); got != want { t.Errorf("ReadAll() = %v, wanted %v", got, want) } } if !found { t.Error("Didn't find expected file in tarball") } }) // Check that directory headers are written for subdirectories in kodata. t.Run("check kodata directory headers", func(t *testing.T) { dataLayer := ls[baseLayers] r, err := dataLayer.Uncompressed() if err != nil { t.Fatalf("Uncompressed() = %v", err) } defer r.Close() expectedDir := path.Join(kodataRoot, "subdir") expectedFile := path.Join(kodataRoot, "subdir", "file.txt") foundDir := false foundFile := false tr := tar.NewReader(r) for { header, err := tr.Next() if errors.Is(err, io.EOF) { break } else if err != nil { t.Errorf("Next() = %v", err) continue } if header.Name == expectedDir { foundDir = true if header.Typeflag != tar.TypeDir { t.Errorf("expected %q to be a directory (typeflag %d), got typeflag %d", expectedDir, tar.TypeDir, header.Typeflag) } } if header.Name == expectedFile { foundFile = true if header.Typeflag != tar.TypeReg { t.Errorf("expected %q to be a regular file (typeflag %d), got typeflag %d", expectedFile, tar.TypeReg, header.Typeflag) } } } if !foundDir { t.Errorf("directory header for %q not found in tarball", expectedDir) } if !foundFile { t.Errorf("file %q not found in tarball", expectedFile) } }) // Check that the entrypoint of the image is configured to invoke our Go application t.Run("check entrypoint", func(t *testing.T) { cfg, err := img.ConfigFile() if err != nil { t.Errorf("ConfigFile() = %v", err) } entrypoint := cfg.Config.Entrypoint if got, want := len(entrypoint), 1; got != want { t.Errorf("len(entrypoint) = %v, want %v", got, want) } if got, want := entrypoint[0], "/ko-app/test"; got != want { t.Errorf("entrypoint = %v, want %v", got, want) } }) // Check that the environment contains the KO_DATA_PATH environment variable. t.Run("check KO_DATA_PATH env var", func(t *testing.T) { cfg, err := img.ConfigFile() if err != nil { t.Errorf("ConfigFile() = %v", err) } found := false for _, entry := range cfg.Config.Env { if entry == "KO_DATA_PATH="+kodataRoot { found = true } } if !found { t.Error("Didn't find KO_DATA_PATH.") } }) // Check that PATH contains the directory of the produced binary. t.Run("check PATH env var", func(t *testing.T) { cfg, err := img.ConfigFile() if err != nil { t.Errorf("ConfigFile() = %v", err) } found := false for _, envVar := range cfg.Config.Env { if after, ok := strings.CutPrefix(envVar, "PATH="); ok { pathValue := after pathEntries := strings.SplitSeq(pathValue, ":") for pathEntry := range pathEntries { if pathEntry == "/ko-app" { found = true } } } } if !found { t.Error("Didn't find entrypoint in PATH.") } }) t.Run("check creation time", func(t *testing.T) { cfg, err := img.ConfigFile() if err != nil { t.Errorf("ConfigFile() = %v", err) } actual := cfg.Created if actual.Time != creationTime.Time { t.Errorf("created = %v, want %v", actual, creationTime) } }) t.Run("check annotations", func(t *testing.T) { if !checkAnnotations { t.Skip("skipping annotations check") } mf, err := img.Manifest() if err != nil { t.Fatalf("Manifest() = %v", err) } t.Logf("Got annotations: %v", mf.Annotations) if _, found := mf.Annotations[specsv1.AnnotationBaseImageDigest]; !found { t.Errorf("image annotations did not contain base image digest") } want := baseRef.Name() if got := mf.Annotations[specsv1.AnnotationBaseImageName]; got != want { t.Errorf("base image ref; got %q, want %q", got, want) } }) if expectSBOM { t.Run("checking for SBOM", func(t *testing.T) { f, err := img.Attachment("sbom") if err != nil { t.Fatalf("Attachment() = %v", err) } b, err := f.Payload() if err != nil { t.Fatalf("Payload() = %v", err) } t.Logf("Got SBOM: %v", string(b)) if string(b) != wantSBOM { t.Errorf("got SBOM %s, wanted %s", string(b), wantSBOM) } }) } else { t.Run("checking for no SBOM", func(t *testing.T) { f, err := img.Attachment("sbom") if err == nil { b, err := f.Payload() if err != nil { t.Fatalf("Payload() = %v", err) } t.Fatalf("Attachment() = %v, wanted error", string(b)) } }) } } func TestGoBuild(t *testing.T) { baseLayers := int64(3) base, err := random.Image(1024, baseLayers) if err != nil { t.Fatalf("random.Image() = %v", err) } importpath := "github.com/google/ko" creationTime := v1.Time{Time: time.Unix(5000, 0)} ng, err := NewGo( context.Background(), "", WithCreationTime(creationTime), WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }), withBuilder(writeTempFile), withSBOMber(fauxSBOM), WithLabel("foo", "bar"), WithLabel("hello", "world"), WithAnnotation("fizz", "buzz"), WithAnnotation("goodbye", "world"), WithUser("1234:1234"), WithPlatforms("all"), ) if err != nil { t.Fatalf("NewGo() = %v", err) } result, err := ng.Build(context.Background(), StrictScheme+filepath.Join(importpath, "test")) if err != nil { t.Fatalf("Build() = %v", err) } img, ok := result.(oci.SignedImage) if !ok { t.Fatalf("Build() not a SignedImage: %T", result) } validateImage(t, img, baseLayers, creationTime, true, true) // Check that rebuilding the image again results in the same image digest. t.Run("check determinism", func(t *testing.T) { result2, err := ng.Build(context.Background(), StrictScheme+filepath.Join(importpath, "test")) if err != nil { t.Fatalf("Build() = %v", err) } d1, err := result.Digest() if err != nil { t.Fatalf("Digest() = %v", err) } d2, err := result2.Digest() if err != nil { t.Fatalf("Digest() = %v", err) } if d1 != d2 { t.Errorf("Digest mismatch: %s != %s", d1, d2) } }) t.Run("check labels", func(t *testing.T) { cfg, err := img.ConfigFile() if err != nil { t.Fatalf("ConfigFile() = %v", err) } want := map[string]string{ "foo": "bar", "hello": "world", } got := cfg.Config.Labels if d := cmp.Diff(got, want); d != "" { t.Fatalf("Labels diff (-got,+want): %s", d) } }) t.Run("check annotations", func(t *testing.T) { baseDigest, err := base.Digest() if err != nil { t.Fatalf("base.Digest() = %v", err) } man, err := img.Manifest() if err != nil { t.Fatalf("Manifest() = %v", err) } want := map[string]string{ specsv1.AnnotationBaseImageName: baseRef.Name(), specsv1.AnnotationBaseImageDigest: baseDigest.String(), "fizz": "buzz", "goodbye": "world", } got := man.Annotations if d := cmp.Diff(got, want); d != "" { t.Fatalf("Annotations diff (-got,+want): %s", d) } }) t.Run("check user", func(t *testing.T) { cfg, err := img.ConfigFile() if err != nil { t.Fatalf("ConfigFile() = %v", err) } want := "1234:1234" got := cfg.Config.User if got != want { t.Fatalf("User: %s != %s", want, got) } }) } func TestGoBuild_Defaults(t *testing.T) { baseLayers := int64(3) base, err := random.Image(1024, baseLayers) if err != nil { t.Fatalf("random.Image() = %v", err) } importpath := "github.com/google/ko" creationTime := v1.Time{Time: time.Unix(5000, 0)} var buildCtx buildContext ng, err := NewGo( context.Background(), "", WithCreationTime(creationTime), WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }), withBuilder(func(_ context.Context, b buildContext) (string, error) { buildCtx = b return "", errors.New("fake build error") }), withSBOMber(fauxSBOM), WithPlatforms("all"), WithDefaultEnv([]string{"FOO=foo", "BAR=bar"}), WithDefaultFlags([]string{"-v"}), WithDefaultLdflags([]string{"-s"}), WithConfig(map[string]Config{ "github.com/google/ko/test": {}, }), ) require.NoError(t, err) // Build and capture the buildContext. _, err = ng.Build(context.Background(), StrictScheme+filepath.Join(importpath, "test")) require.ErrorContains(t, err, "fake build error") require.Equal(t, []string{"-v"}, buildCtx.flags) require.Equal(t, []string{"-s"}, buildCtx.ldflags) envVars := make(map[string]string) for _, val := range buildCtx.env { kv := strings.SplitN(val, "=", 2) envVars[kv[0]] = kv[1] } require.Equal(t, "foo", envVars["FOO"]) require.Equal(t, "bar", envVars["BAR"]) } func TestGoBuild_ConfigOverrideDefaults(t *testing.T) { baseLayers := int64(3) base, err := random.Image(1024, baseLayers) if err != nil { t.Fatalf("random.Image() = %v", err) } importpath := "github.com/google/ko" creationTime := v1.Time{Time: time.Unix(5000, 0)} var buildCtx buildContext ng, err := NewGo( context.Background(), "", WithCreationTime(creationTime), WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }), withBuilder(func(_ context.Context, b buildContext) (string, error) { buildCtx = b return "", errors.New("fake build error") }), withSBOMber(fauxSBOM), WithPlatforms("all"), WithDefaultEnv([]string{"FOO=foo", "BAR=bar"}), WithDefaultFlags([]string{"-v"}), WithDefaultLdflags([]string{"-s"}), WithConfig(map[string]Config{ "github.com/google/ko/test": { Env: StringArray{"FOO=baz"}, Flags: FlagArray{"-trimpath"}, Ldflags: StringArray{"-w"}, }, }), ) require.NoError(t, err) // Build and capture the buildContext. _, err = ng.Build(context.Background(), StrictScheme+filepath.Join(importpath, "test")) require.ErrorContains(t, err, "fake build error") require.Equal(t, []string{"-trimpath"}, buildCtx.flags) require.Equal(t, []string{"-w"}, buildCtx.ldflags) envVars := make(map[string]string) for _, val := range buildCtx.env { kv := strings.SplitN(val, "=", 2) envVars[kv[0]] = kv[1] } require.Equal(t, "baz", envVars["FOO"]) require.Equal(t, "", envVars["BAR"]) } func TestGoBuildWithKOCACHE(t *testing.T) { now := time.Now() // current local time sec := now.Unix() tmpDir := t.TempDir() koCacheDir := filepath.Join(tmpDir, strconv.FormatInt(sec, 10)) t.Setenv("KOCACHE", koCacheDir) baseLayers := int64(3) base, err := random.Image(1024, baseLayers) if err != nil { t.Fatalf("random.Image() = %v", err) } importpath := "github.com/google/ko" creationTime := v1.Time{Time: time.Unix(5000, 0)} ng, err := NewGo( context.Background(), "", WithCreationTime(creationTime), WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }), WithPlatforms("all"), ) if err != nil { t.Fatalf("NewGo() = %v", err) } _, err = ng.Build(context.Background(), StrictScheme+filepath.Join(importpath, "test")) if err != nil { t.Fatalf("Build() = %v", err) } t.Run("check KOCACHE exists", func(t *testing.T) { _, err := os.Stat(koCacheDir) if os.IsNotExist(err) { t.Fatalf("KOCACHE directory %s should be exists= %v", koCacheDir, err) } }) } func TestGoBuildWithoutSBOM(t *testing.T) { baseLayers := int64(3) base, err := random.Image(1024, baseLayers) if err != nil { t.Fatalf("random.Image() = %v", err) } importpath := "github.com/google/ko" creationTime := v1.Time{Time: time.Unix(5000, 0)} ng, err := NewGo( context.Background(), "", WithCreationTime(creationTime), WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }), withBuilder(writeTempFile), withSBOMber(fauxSBOM), WithLabel("foo", "bar"), WithLabel("hello", "world"), WithDisabledSBOM(), WithPlatforms("all"), ) if err != nil { t.Fatalf("NewGo() = %v", err) } result, err := ng.Build(context.Background(), StrictScheme+filepath.Join(importpath, "test")) if err != nil { t.Fatalf("Build() = %v", err) } img, ok := result.(oci.SignedImage) if !ok { t.Fatalf("Build() not a SignedImage: %T", result) } validateImage(t, img, baseLayers, creationTime, true, false) } func TestGoBuildIndex(t *testing.T) { baseLayers := int64(3) images := int64(2) base, err := random.Index(1024, baseLayers, images) if err != nil { t.Fatalf("random.Image() = %v", err) } importpath := "github.com/google/ko" creationTime := v1.Time{Time: time.Unix(5000, 0)} ng, err := NewGo( context.Background(), "", WithCreationTime(creationTime), WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }), WithPlatforms("all"), withBuilder(writeTempFile), withSBOMber(fauxSBOM), ) if err != nil { t.Fatalf("NewGo() = %v", err) } result, err := ng.Build(context.Background(), StrictScheme+filepath.Join(importpath, "test")) if err != nil { t.Fatalf("Build() = %v", err) } idx, ok := result.(oci.SignedImageIndex) if !ok { t.Fatalf("Build() not a SignedImageIndex: %T", result) } im, err := idx.IndexManifest() if err != nil { t.Fatalf("IndexManifest() = %v", err) } for _, desc := range im.Manifests { img, err := idx.SignedImage(desc.Digest) if err != nil { t.Fatalf("idx.Image(%s) = %v", desc.Digest, err) } validateImage(t, img, baseLayers, creationTime, false, true) } if want, got := images, int64(len(im.Manifests)); want != got { t.Fatalf("len(Manifests()) = %v, want %v", got, want) } // Check that rebuilding the image again results in the same image digest. t.Run("check determinism", func(t *testing.T) { result2, err := ng.Build(context.Background(), StrictScheme+filepath.Join(importpath, "test")) if err != nil { t.Fatalf("Build() = %v", err) } d1, err := result.Digest() if err != nil { t.Fatalf("Digest() = %v", err) } d2, err := result2.Digest() if err != nil { t.Fatalf("Digest() = %v", err) } if d1 != d2 { t.Errorf("Digest mismatch: %s != %s", d1, d2) } }) } func TestNestedIndex(t *testing.T) { baseLayers := int64(3) images := int64(2) base, err := random.Index(1024, baseLayers, images) if err != nil { t.Fatalf("random.Image() = %v", err) } importpath := "github.com/google/ko" nestedBase := mutate.AppendManifests(empty.Index, mutate.IndexAddendum{Add: base}) creationTime := v1.Time{Time: time.Unix(5000, 0)} ng, err := NewGo( context.Background(), "", WithCreationTime(creationTime), WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, nestedBase, nil }), withBuilder(writeTempFile), withSBOMber(fauxSBOM), ) if err != nil { t.Fatalf("NewGo() = %v", err) } _, err = ng.Build(context.Background(), StrictScheme+filepath.Join(importpath, "test")) if err == nil { t.Fatal("Build() expected err") } if !strings.Contains(err.Error(), "unexpected mediaType") { t.Errorf("Build() expected unexpected mediaType error, got: %s", err) } } func TestGoarm(t *testing.T) { // From golang@sha256:1ba0da74b20aad52b091877b0e0ece503c563f39e37aa6b0e46777c4d820a2ae // and made up invalid cases. for _, tc := range []struct { platform v1.Platform variant string err bool }{{ platform: v1.Platform{ Architecture: "arm", OS: "linux", Variant: "vnot-a-number", }, err: true, }, { platform: v1.Platform{ Architecture: "arm", OS: "linux", Variant: "wrong-prefix", }, err: true, }, { platform: v1.Platform{ Architecture: "arm64", OS: "linux", Variant: "v3", }, variant: "", }, { platform: v1.Platform{ Architecture: "arm", OS: "linux", Variant: "v5", }, variant: "5", }, { platform: v1.Platform{ Architecture: "arm", OS: "linux", Variant: "v7", }, variant: "7", }, { platform: v1.Platform{ Architecture: "arm64", OS: "linux", Variant: "v8", }, variant: "7", }, } { variant, err := getGoarm(tc.platform) if tc.err { if err == nil { t.Errorf("getGoarm(%v) expected err", tc.platform) } continue } if err != nil { t.Fatalf("getGoarm failed for %v: %v", tc.platform, err) } if got, want := variant, tc.variant; got != want { t.Errorf("wrong variant for %v: want %q got %q", tc.platform, want, got) } } } func TestMatchesPlatformSpec(t *testing.T) { for _, tc := range []struct { platform *v1.Platform spec []string result bool err bool }{{ platform: nil, spec: []string{"all"}, result: true, }, { platform: nil, spec: []string{"linux/amd64"}, result: false, }, { platform: &v1.Platform{ Architecture: "amd64", OS: "linux", }, spec: []string{"all"}, result: true, }, { platform: &v1.Platform{ Architecture: "amd64", OS: "windows", }, spec: []string{"linux"}, result: false, }, { platform: &v1.Platform{ Architecture: "arm64", OS: "linux", Variant: "v3", }, spec: []string{"linux/amd64", "linux/arm64"}, result: true, }, { platform: &v1.Platform{ Architecture: "arm64", OS: "linux", Variant: "v3", }, spec: []string{"linux/amd64", "linux/arm64/v4"}, result: false, }, { platform: &v1.Platform{ Architecture: "arm64", OS: "linux", Variant: "v3", }, spec: []string{"linux/amd64", "linux/arm64/v3/z5"}, err: true, }, { spec: []string{}, platform: &v1.Platform{ Architecture: "amd64", OS: "linux", }, result: false, }, { // Exact match w/ osversion spec: []string{"windows/amd64:10.0.17763.1234"}, platform: &v1.Platform{ OS: "windows", Architecture: "amd64", OSVersion: "10.0.17763.1234", }, result: true, }, { // OSVersion partial match using relaxed semantics. spec: []string{"windows/amd64:10.0.17763"}, platform: &v1.Platform{ OS: "windows", Architecture: "amd64", OSVersion: "10.0.17763.1234", }, result: true, }, { // Not windows and osversion isn't exact match. spec: []string{"linux/amd64:10.0.17763"}, platform: &v1.Platform{ OS: "linux", Architecture: "amd64", OSVersion: "10.0.17763.1234", }, result: false, }, { // Not matching X.Y.Z spec: []string{"windows/amd64:10"}, platform: &v1.Platform{ OS: "windows", Architecture: "amd64", OSVersion: "10.0.17763.1234", }, result: false, }, { // Requirement is more specific. spec: []string{"windows/amd64:10.0.17763.1234"}, platform: &v1.Platform{ OS: "windows", Architecture: "amd64", OSVersion: "10.0.17763", // this won't happen in the wild, but it shouldn't match. }, result: false, }, { // Requirement is not specific enough. spec: []string{"windows/amd64:10.0.17763.1234"}, platform: &v1.Platform{ OS: "windows", Architecture: "amd64", OSVersion: "10.0.17763.1234.5678", // this won't happen in the wild, but it shouldn't match. }, result: false, }, { // Even --platform=all does not match unknown/unknown. platform: &v1.Platform{Architecture: "unknown", OS: "unknown"}, spec: []string{"all"}, result: false, }} { pm, err := parseSpec(tc.spec) if tc.err { if err == nil { t.Errorf("parseSpec(%v, %q) expected err", tc.platform, tc.spec) } continue } if err != nil { t.Fatalf("parseSpec failed for %v %q: %v", tc.platform, tc.spec, err) } matches := pm.matches(tc.platform) if got, want := matches, tc.result; got != want { t.Errorf("wrong result for %v %q: want %t got %t", tc.platform, tc.spec, want, got) } } } func TestGoBuildConsistentMediaTypes(t *testing.T) { for _, c := range []struct { desc string mediaType, layerMediaType types.MediaType }{{ desc: "docker types", mediaType: types.DockerManifestSchema2, layerMediaType: types.DockerLayer, }, { desc: "oci types", mediaType: types.OCIManifestSchema1, layerMediaType: types.OCILayer, }} { t.Run(c.desc, func(t *testing.T) { base := mutate.MediaType(empty.Image, c.mediaType) l, err := random.Layer(10, c.layerMediaType) if err != nil { t.Fatal(err) } base, err = mutate.AppendLayers(base, l) if err != nil { t.Fatal(err) } ng, err := NewGo( context.Background(), "", WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }), withBuilder(writeTempFile), withSBOMber(fauxSBOM), WithPlatforms("all"), ) if err != nil { t.Fatalf("NewGo() = %v", err) } importpath := "github.com/google/ko" result, err := ng.Build(context.Background(), StrictScheme+importpath) if err != nil { t.Fatalf("Build() = %v", err) } img, ok := result.(v1.Image) if !ok { t.Fatalf("Build() not an Image: %T", result) } ls, err := img.Layers() if err != nil { t.Fatalf("Layers() = %v", err) } for i, l := range ls { gotMT, err := l.MediaType() if err != nil { t.Fatal(err) } if gotMT != c.layerMediaType { t.Errorf("layer %d: got mediaType %q, want %q", i, gotMT, c.layerMediaType) } } gotMT, err := img.MediaType() if err != nil { t.Fatal(err) } if gotMT != c.mediaType { t.Errorf("got image mediaType %q, want %q", gotMT, c.layerMediaType) } }) } } func TestDebugger(t *testing.T) { base, err := random.Image(1024, 3) if err != nil { t.Fatalf("random.Image() = %v", err) } importpath := "github.com/google/ko" ng, err := NewGo( context.Background(), "", WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }), WithPlatforms("linux/amd64"), WithDebugger(), ) if err != nil { t.Fatalf("NewGo() = %v", err) } result, err := ng.Build(context.Background(), StrictScheme+importpath) if err != nil { t.Fatalf("Build() = %v", err) } img, ok := result.(v1.Image) if !ok { t.Fatalf("Build() not an Image: %T", result) } // Check that the entrypoint of the image is not overwritten cfg, err := img.ConfigFile() if err != nil { t.Errorf("ConfigFile() = %v", err) } gotEntrypoint := cfg.Config.Entrypoint wantEntrypoint := []string{ "/ko-app/dlv", "exec", "--listen=:40000", "--headless", "--log", "--accept-multiclient", "--api-version=2", "--", "/ko-app/ko", } if got, want := len(gotEntrypoint), len(wantEntrypoint); got != want { t.Fatalf("len(entrypoint) = %v, want %v", got, want) } for i := range wantEntrypoint { if got, want := gotEntrypoint[i], wantEntrypoint[i]; got != want { t.Errorf("entrypoint[%d] = %v, want %v", i, got, want) } } } ================================================ FILE: pkg/build/gobuilds.go ================================================ // Copyright 2021 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package build import ( "context" "fmt" gb "go/build" "path" "path/filepath" "strings" ) type gobuilds struct { // Map of fully qualified import path to go builder with config builders map[string]builderWithConfig // Default go builder used if there's no matching build config. defaultBuilder Interface // workingDirectory is typically ".", but it may be a different value if ko is embedded as a library. workingDirectory string } // builderWithConfig is not an imaginative name. type builderWithConfig struct { builder Interface config Config } // NewGobuilds returns a build.Interface that can dispatch to builders based on matching the import path to a build config in .ko.yaml. func NewGobuilds(ctx context.Context, workingDirectory string, buildConfigs map[string]Config, opts ...Option) (Interface, error) { if workingDirectory == "" { workingDirectory = "." } defaultBuilder, err := NewGo(ctx, workingDirectory, opts...) if err != nil { return nil, fmt.Errorf("could not create default go builder: %w", err) } g := &gobuilds{ builders: map[string]builderWithConfig{}, defaultBuilder: defaultBuilder, workingDirectory: workingDirectory, } for importpath, buildConfig := range buildConfigs { builderDirectory := path.Join(workingDirectory, buildConfig.Dir) builder, err := NewGo(ctx, builderDirectory, opts...) if err != nil { return nil, fmt.Errorf("could not create go builder for config (%q): %w", importpath, err) } g.builders[importpath] = builderWithConfig{ builder: builder, config: buildConfig, } } return g, nil } // QualifyImport implements build.Interface func (g *gobuilds) QualifyImport(importpath string) (string, error) { b := g.builder(importpath) if b.config.Dir != "" { var err error importpath, err = relativePath(b.config.Dir, importpath) if err != nil { return "", err } } return b.builder.QualifyImport(importpath) } // IsSupportedReference implements build.Interface func (g *gobuilds) IsSupportedReference(importpath string) error { return g.builder(importpath).builder.IsSupportedReference(importpath) } // Build implements build.Interface func (g *gobuilds) Build(ctx context.Context, importpath string) (Result, error) { return g.builder(importpath).builder.Build(ctx, importpath) } // builder selects a go builder for the provided import path. // The `importpath` argument can be either local (e.g., `./cmd/foo`) or not (e.g., `example.com/app/cmd/foo`). func (g *gobuilds) builder(importpath string) builderWithConfig { importpath = strings.TrimPrefix(importpath, StrictScheme) if len(g.builders) == 0 { return builderWithConfig{ builder: g.defaultBuilder, } } // first, try to find go builder by fully qualified import path if builderWithConfig, exists := g.builders[importpath]; exists { return builderWithConfig } // second, try to find go builder by local path for _, builderWithConfig := range g.builders { // Match go builder by trying to resolve the local path to a fully qualified import path. If successful, we have a winner. relPath, err := relativePath(builderWithConfig.config.Dir, importpath) if err != nil { // Cannot determine a relative path. Move on and try the next go builder. continue } _, err = builderWithConfig.builder.QualifyImport(relPath) if err != nil { // There's an error turning the local path into a fully qualified import path. Move on and try the next go builder. continue } return builderWithConfig } // fall back to default go builder return builderWithConfig{ builder: g.defaultBuilder, } } // relativePath takes as input a local import path, and returns a path relative to the base directory. // // For example, given the following inputs: // - baseDir: "app" // - importpath: "./app/cmd/foo // The output is: "./cmd/foo" // // If the input is a not a local import path as determined by go/build.IsLocalImport(), the input is returned unchanged. // // If the import path is _not_ a subdirectory of baseDir, the result is an error. func relativePath(baseDir string, importpath string) (string, error) { // Return input unchanged if the import path is a fully qualified import path if !gb.IsLocalImport(importpath) { return importpath, nil } relPath, err := filepath.Rel(baseDir, importpath) if err != nil { return "", fmt.Errorf("cannot determine relative path of baseDir (%q) and local path (%q): %w", baseDir, importpath, err) } if strings.HasPrefix(relPath, "..") { // TODO Is this assumption correct? return "", fmt.Errorf("import path (%q) must be a subdirectory of build config directory (%q)", importpath, baseDir) } if !strings.HasPrefix(relPath, ".") && relPath != "." { relPath = "./" + relPath // ensure go/build.IsLocalImport() interprets this as a local path } return relPath, nil } ================================================ FILE: pkg/build/gobuilds_test.go ================================================ // Copyright 2021 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package build import ( "context" "testing" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/random" ) func Test_gobuilds(t *testing.T) { base, err := random.Image(1024, 1) if err != nil { t.Fatalf("random.Image() = %v", err) } baseRef := name.MustParseReference("all.your/base") opts := []Option{ WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }), withBuilder(writeTempFile), withSBOMber(fauxSBOM), WithPlatforms("all"), } tests := []struct { description string workingDirectory string buildConfigs map[string]Config opts []Option nilDefaultBuilder bool // set to true if you want to test build config and don't want the test to fall back to the default builder importpath string }{ { description: "default builder used when no build configs provided", opts: opts, importpath: "github.com/google/ko", }, { description: "match build config using fully qualified import path", workingDirectory: "../..", buildConfigs: map[string]Config{ "github.com/google/ko/test": { ID: "build-config-0", Dir: "test", }, }, nilDefaultBuilder: true, opts: opts, importpath: "github.com/google/ko/test", }, { description: "match build config using ko scheme-prefixed fully qualified import path", workingDirectory: "../..", buildConfigs: map[string]Config{ "github.com/google/ko/test": { ID: "build-config-1", Dir: "test", }, }, nilDefaultBuilder: true, opts: opts, importpath: "ko://github.com/google/ko/test", }, { description: "find build config by resolving local import path to fully qualified import path", workingDirectory: "../../test", buildConfigs: map[string]Config{ "github.com/google/ko/test": { ID: "build-config-2", }, }, nilDefaultBuilder: true, opts: opts, importpath: ".", }, { description: "find build config by matching local import path to build config directory", workingDirectory: "../..", buildConfigs: map[string]Config{ "github.com/google/ko/tes12t": { ID: "build-config-3", Dir: "test", }, }, nilDefaultBuilder: true, opts: opts, importpath: "./test", }, } for _, test := range tests { t.Run(test.description, func(t *testing.T) { ctx := context.Background() bi, err := NewGobuilds(ctx, test.workingDirectory, test.buildConfigs, test.opts...) if err != nil { t.Fatalf("NewGobuilds(): unexpected error: %v", err) } gbs := bi.(*gobuilds) if test.nilDefaultBuilder { gbs.defaultBuilder = nil } qualifiedImportpath, err := gbs.QualifyImport(test.importpath) if err != nil { t.Fatalf("gobuilds.QualifyImport(%s): unexpected error: %v", test.importpath, err) } if err = gbs.IsSupportedReference(qualifiedImportpath); err != nil { t.Fatalf("gobuilds.IsSupportedReference(%s): unexpected error: %v", qualifiedImportpath, err) } result, err := gbs.Build(ctx, qualifiedImportpath) if err != nil { t.Fatalf("gobuilds.Build(%s): unexpected error = %v", qualifiedImportpath, err) } if result == nil { t.Fatalf("gobuilds.Build(%s): expected non-nil result", qualifiedImportpath) } }) } } func Test_relativePath(t *testing.T) { tests := []struct { description string baseDir string importpath string want string wantErr bool }{ { description: "all empty string", }, { description: "all current directory", baseDir: ".", importpath: ".", want: ".", }, { description: "fully qualified import path without ko prefix", baseDir: "also-any-value-because-it-is-ignored", importpath: "example.com/app/cmd/foo", want: "example.com/app/cmd/foo", }, { description: "fully qualified import path with ko prefix", baseDir: "also-any-value-because-it-is-ignored", importpath: "ko://example.com/app/cmd/foo", want: "ko://example.com/app/cmd/foo", }, { description: "importpath is local subdirectory", baseDir: "foo", importpath: "./foo/bar", want: "./bar", }, { description: "importpath is same local directory", baseDir: "foo/bar", importpath: "./foo/bar", want: ".", }, { description: "importpath is not subdirectory or same local directory", baseDir: "foo", importpath: "./bar", wantErr: true, }, } for _, test := range tests { t.Run(test.description, func(t *testing.T) { got, err := relativePath(test.baseDir, test.importpath) if (err != nil) != test.wantErr { t.Errorf("relativePath() error = %v, wantErr %v", err, test.wantErr) return } if got != test.want { t.Errorf("relativePath() got = %v, want %v", got, test.want) } }) } } ================================================ FILE: pkg/build/layer.go ================================================ // Copyright 2020 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package build import ( "io" "sync" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/types" ) type lazyLayer struct { diffid v1.Hash desc v1.Descriptor sync.Once buildLayer func() (v1.Layer, error) layer v1.Layer err error } // All this info is cached by previous builds. func (l *lazyLayer) Digest() (v1.Hash, error) { return l.desc.Digest, nil } func (l *lazyLayer) DiffID() (v1.Hash, error) { return l.diffid, nil } func (l *lazyLayer) Size() (int64, error) { return l.desc.Size, nil } func (l *lazyLayer) MediaType() (types.MediaType, error) { return l.desc.MediaType, nil } // This is only called if the registry doesn't have this blob already. func (l *lazyLayer) Compressed() (io.ReadCloser, error) { layer, err := l.compute() if err != nil { return nil, err } return layer.Compressed() } // This should never actually be called but we need it to impl v1.Layer. func (l *lazyLayer) Uncompressed() (io.ReadCloser, error) { layer, err := l.compute() if err != nil { return nil, err } return layer.Uncompressed() } func (l *lazyLayer) compute() (v1.Layer, error) { l.Do(func() { l.layer, l.err = l.buildLayer() }) return l.layer, l.err } ================================================ FILE: pkg/build/limit.go ================================================ // Copyright 2019 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package build import ( "context" "golang.org/x/sync/semaphore" ) // Limiter composes with another Interface to limit the number of concurrent builds. type Limiter struct { Builder Interface semaphore *semaphore.Weighted } // Limiter implements Interface var _ Interface = (*Recorder)(nil) // QualifyImport implements Interface func (l *Limiter) QualifyImport(ip string) (string, error) { return l.Builder.QualifyImport(ip) } // IsSupportedReference implements Interface func (l *Limiter) IsSupportedReference(ip string) error { return l.Builder.IsSupportedReference(ip) } // Build implements Interface func (l *Limiter) Build(ctx context.Context, ip string) (Result, error) { if err := l.semaphore.Acquire(ctx, 1); err != nil { return nil, err } defer l.semaphore.Release(1) return l.Builder.Build(ctx, ip) } // NewLimiter returns a new builder that only allows n concurrent builds of b. // // Deprecated: Obsoleted by WithJobs option. func NewLimiter(b Interface, n int) *Limiter { return &Limiter{ Builder: b, semaphore: semaphore.NewWeighted(int64(n)), } } ================================================ FILE: pkg/build/limit_test.go ================================================ // Copyright 2019 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package build import ( "context" "testing" "time" "golang.org/x/sync/errgroup" ) type sleeper struct{} var _ Interface = (*sleeper)(nil) // QualifyImport implements Interface func (*sleeper) QualifyImport(ip string) (string, error) { return ip, nil } // IsSupportedReference implements Interface func (*sleeper) IsSupportedReference(_ string) error { return nil } // Build implements Interface func (*sleeper) Build(_ context.Context, _ string) (Result, error) { time.Sleep(50 * time.Millisecond) return nil, nil } func TestLimiter(t *testing.T) { b := NewLimiter(&sleeper{}, 2) start := time.Now() g, _ := errgroup.WithContext(context.TODO()) for i := 0; i <= 10; i++ { g.Go(func() error { _, _ = b.Build(context.Background(), "whatever") return nil }) } g.Wait() // 50 ms * 10 builds / 2 concurrency = ~250ms if time.Now().Before(start.Add(250 * time.Millisecond)) { t.Fatal("Too many builds") } } ================================================ FILE: pkg/build/options.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package build import ( "strings" v1 "github.com/google/go-containerregistry/pkg/v1" ) // WithBaseImages is a functional option for overriding the base images // that are used for different images. func WithBaseImages(gb GetBase) Option { return func(gbo *gobuildOpener) error { gbo.getBase = gb return nil } } // WithCreationTime is a functional option for overriding the creation // time given to images. func WithCreationTime(t v1.Time) Option { return func(gbo *gobuildOpener) error { gbo.creationTime = t return nil } } // WithKoDataCreationTime is a functional option for overriding the creation // time given to the files in the kodata directory. func WithKoDataCreationTime(t v1.Time) Option { return func(gbo *gobuildOpener) error { gbo.kodataCreationTime = t return nil } } // WithDisabledOptimizations is a functional option for disabling optimizations // when compiling. func WithDisabledOptimizations() Option { return func(gbo *gobuildOpener) error { gbo.disableOptimizations = true return nil } } // WithDisabledSBOM is a functional option for disabling SBOM generation. func WithDisabledSBOM() Option { return func(gbo *gobuildOpener) error { gbo.sbom = nil return nil } } // WithTrimpath is a functional option that controls whether the `-trimpath` // flag is added to `go build`. func WithTrimpath(v bool) Option { return func(gbo *gobuildOpener) error { gbo.trimpath = v return nil } } // WithConfig is a functional option for providing GoReleaser Build influenced // build settings for importpaths. // // Set a fully qualified importpath (e.g. github.com/my-user/my-repo/cmd/app) // as the mapping key for the respective Config. func WithConfig(buildConfigs map[string]Config) Option { return func(gbo *gobuildOpener) error { gbo.buildConfigs = buildConfigs return nil } } // WithDefaultEnv is a functional option for providing a default set of environment // variables across all builds. func WithDefaultEnv(env []string) Option { return func(gbo *gobuildOpener) error { gbo.defaultEnv = env return nil } } // WithDefaultFlags is a functional option for providing a default set of flags across all builds. func WithDefaultFlags(flags []string) Option { return func(gbo *gobuildOpener) error { gbo.defaultFlags = flags return nil } } // WithDefaultLdflags is a functional option for providing a default set of ldflags across all builds. func WithDefaultLdflags(ldflags []string) Option { return func(gbo *gobuildOpener) error { gbo.defaultLdflags = ldflags return nil } } // WithPlatforms is a functional option for building certain platforms for // multi-platform base images. To build everything from the base, use "all", // otherwise use a list of platform specs, i.e.: // // platform = [/[/]] // allowed = "all" | []string{platform[,platform]*} // // Note: a string of comma-separated platforms (i.e. "platform[,platform]*") // has been deprecated and only exist for backwards compatibility reasons, // which will be removed in the future. func WithPlatforms(platforms ...string) Option { return func(gbo *gobuildOpener) error { if len(platforms) == 1 { // TODO: inform users that they are using deprecated flow? platforms = strings.Split(platforms[0], ",") } gbo.platforms = platforms return nil } } // WithLabel is a functional option for adding labels to built images. func WithLabel(k, v string) Option { return func(gbo *gobuildOpener) error { if gbo.labels == nil { gbo.labels = map[string]string{} } gbo.labels[k] = v return nil } } // WithAnnotation is a functional option for adding annotations to built manifests func WithAnnotation(k, v string) Option { return func(gbo *gobuildOpener) error { if gbo.annotations == nil { gbo.annotations = map[string]string{} } gbo.annotations[k] = v return nil } } // WithUser is a functional option for overriding the user in the image config. func WithUser(user string) Option { return func(gbo *gobuildOpener) error { gbo.user = user return nil } } // withBuilder is a functional option for overriding the way go binaries // are built. func withBuilder(b builder) Option { return func(gbo *gobuildOpener) error { gbo.build = b return nil } } // WithSPDX is a functional option to direct ko to use // SPDX for SBOM format. func WithSPDX(version string) Option { return func(gbo *gobuildOpener) error { gbo.sbom = spdx(version) return nil } } // withSBOMber is a functional option for overriding the way SBOMs // are generated. func withSBOMber(sbom sbomber) Option { return func(gbo *gobuildOpener) error { gbo.sbom = sbom return nil } } // WithJobs limits the number of concurrent builds. func WithJobs(jobs int) Option { return func(gbo *gobuildOpener) error { gbo.jobs = jobs return nil } } // WithSBOMDir is a functional option for overriding the directory func WithSBOMDir(dir string) Option { return func(gbo *gobuildOpener) error { gbo.sbomDir = dir return nil } } func WithDebugger() Option { return func(gbo *gobuildOpener) error { gbo.debug = true return nil } } ================================================ FILE: pkg/build/recorder.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package build import ( "context" "sync" ) // Recorder composes with another Interface to record the built import paths. type Recorder struct { m sync.Mutex ImportPaths []string Builder Interface } // Recorder implements Interface var _ Interface = (*Recorder)(nil) // QualifyImport implements Interface func (r *Recorder) QualifyImport(ip string) (string, error) { return r.Builder.QualifyImport(ip) } // IsSupportedReference implements Interface func (r *Recorder) IsSupportedReference(ip string) error { return r.Builder.IsSupportedReference(ip) } // Build implements Interface func (r *Recorder) Build(ctx context.Context, ip string) (Result, error) { func() { r.m.Lock() defer r.m.Unlock() r.ImportPaths = append(r.ImportPaths, ip) }() return r.Builder.Build(ctx, ip) } ================================================ FILE: pkg/build/recorder_test.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package build import ( "context" "testing" "github.com/google/go-cmp/cmp" ) type fake struct { isr func(string) error b func(string) (Result, error) } var _ Interface = (*fake)(nil) // QualifyImport implements Interface func (r *fake) QualifyImport(ip string) (string, error) { return ip, nil } // IsSupportedReference implements Interface func (r *fake) IsSupportedReference(ip string) error { return r.isr(ip) } // Build implements Interface func (r *fake) Build(_ context.Context, ip string) (Result, error) { return r.b(ip) } func TestISRPassThrough(t *testing.T) { tests := []struct { name string input string }{{ name: "empty string", }, { name: "non-empty string", input: "asdf asdf asdf", }} for _, test := range tests { t.Run(test.name, func(t *testing.T) { called := false inner := &fake{ isr: func(ip string) error { called = true if ip != test.input { t.Errorf("ISR = %v, wanted %v", ip, test.input) } return nil }, } rec := &Recorder{ Builder: inner, } rec.IsSupportedReference(test.input) if !called { t.Error("IsSupportedReference wasn't called, wanted called") } }) } } func TestBuildRecording(t *testing.T) { tests := []struct { name string inputs []string }{{ name: "no calls", }, { name: "one call", inputs: []string{ "github.com/foo/bar", }, }, { name: "two calls", inputs: []string{ "github.com/foo/bar", "github.com/foo/baz", }, }, { name: "duplicates", inputs: []string{ "github.com/foo/bar", "github.com/foo/baz", "github.com/foo/bar", "github.com/foo/baz", }, }} for _, test := range tests { t.Run(test.name, func(t *testing.T) { inner := &fake{ b: func(_ string) (Result, error) { return nil, nil }, } rec := &Recorder{ Builder: inner, } for _, in := range test.inputs { rec.Build(context.Background(), in) } if diff := cmp.Diff(test.inputs, rec.ImportPaths); diff != "" { t.Errorf("Build (-want, +got): %s", diff) } }) } } ================================================ FILE: pkg/build/shared.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package build import ( "context" "sync" ) // Caching wraps a builder implementation in a layer that shares build results // for the same inputs using a simple "future" implementation. Cached results // may be invalidated by calling Invalidate with the same input passed to Build. type Caching struct { inner Interface m sync.Mutex results map[string]*future } // Caching implements Interface var _ Interface = (*Caching)(nil) // NewCaching wraps the provided build.Interface in an implementation that // shares build results for a given path until the result has been invalidated. func NewCaching(inner Interface) (*Caching, error) { return &Caching{ inner: inner, results: make(map[string]*future), }, nil } // Build implements Interface func (c *Caching) Build(ctx context.Context, ip string) (Result, error) { f := func() *future { // Lock the map of futures. c.m.Lock() defer c.m.Unlock() // If a future for "ip" exists, then return it. f, ok := c.results[ip] if ok { return f } // Otherwise create and record a future for a Build of "ip". f = newFuture(func() (Result, error) { return c.inner.Build(ctx, ip) }) c.results[ip] = f return f }() return f.Get() } // QualifyImport implements Interface func (c *Caching) QualifyImport(ip string) (string, error) { return c.inner.QualifyImport(ip) } // IsSupportedReference implements Interface func (c *Caching) IsSupportedReference(ip string) error { return c.inner.IsSupportedReference(ip) } // Invalidate removes an import path's cached results. func (c *Caching) Invalidate(ip string) { c.m.Lock() defer c.m.Unlock() delete(c.results, ip) } ================================================ FILE: pkg/build/shared_test.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package build import ( "context" "testing" "time" "github.com/google/go-containerregistry/pkg/v1/random" ) type slowbuild struct { sleep time.Duration } // slowbuild implements Interface var _ Interface = (*slowbuild)(nil) func (sb *slowbuild) QualifyImport(ip string) (string, error) { return ip, nil } func (sb *slowbuild) IsSupportedReference(string) error { return nil } func (sb *slowbuild) Build(context.Context, string) (Result, error) { time.Sleep(sb.sleep) return random.Index(256, 8, 3) } func TestCaching(t *testing.T) { duration := 100 * time.Millisecond ip := "foo" sb := &slowbuild{duration} cb, _ := NewCaching(sb) if err := cb.IsSupportedReference(ip); err != nil { t.Errorf("ISR(%q) = (%v), wanted nil", err, ip) } previousDigest := "not-a-digest" // Each iteration, we test that the first build is slow and subsequent // builds are fast and return the same image. Then we invalidate the // cache and iterate. for range 3 { start := time.Now() img1, err := cb.Build(context.Background(), ip) if err != nil { t.Errorf("Build() = %v", err) } end := time.Now() elapsed := end.Sub(start) if elapsed < duration { t.Errorf("Elapsed time %v, wanted >= %s", elapsed, duration) } d1 := digest(t, img1) if d1 == previousDigest { t.Errorf("Got same digest as previous iteration, wanted different: %v", d1) } previousDigest = d1 start = time.Now() img2, err := cb.Build(context.Background(), ip) if err != nil { t.Errorf("Build() = %v", err) } end = time.Now() elapsed = end.Sub(start) if elapsed >= duration { t.Errorf("Elapsed time %v, wanted < %s", elapsed, duration) } d2 := digest(t, img2) if d1 != d2 { t.Error("Got different images, wanted same") } cb.Invalidate(ip) } } ================================================ FILE: pkg/build/strict.go ================================================ // Copyright 2020 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package build import "strings" // StrictScheme is a prefix that can be placed on import paths that users // think MUST be supported references. const StrictScheme = "ko://" type reference struct { strict bool path string } func newRef(s string) reference { return reference{ strict: strings.HasPrefix(s, StrictScheme), path: strings.TrimPrefix(s, StrictScheme), } } func (r reference) IsStrict() bool { return r.strict } func (r reference) Path() string { return r.path } func (r reference) String() string { if r.IsStrict() { return StrictScheme + r.Path() } return r.Path() } ================================================ FILE: pkg/build/strict_test.go ================================================ // Copyright 2020 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package build import "testing" func TestStrictReference(t *testing.T) { tests := []struct { name string input string strict bool path string }{{ name: "loose", input: "github.com/foo/bar", strict: false, path: "github.com/foo/bar", }, { name: "strict", input: "ko://github.com/foo/bar", strict: true, path: "github.com/foo/bar", }} for _, test := range tests { t.Run(test.name, func(t *testing.T) { ref := newRef(test.input) if got, want := ref.IsStrict(), test.strict; got != want { t.Errorf("got: %v, want: %v", got, want) } if got, want := ref.Path(), test.path; got != want { t.Errorf("got: %v, want: %v", got, want) } if got, want := ref.String(), test.input; got != want { t.Errorf("got: %v, want: %v", got, want) } }) } } ================================================ FILE: pkg/caps/caps.go ================================================ // Copyright 2024 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package caps implements a subset of Linux capabilities handling // relevant in the context of authoring container images. package caps import ( "bytes" "encoding/binary" "fmt" "strconv" "strings" ) // Mask captures a set of Linux capabilities type Mask uint64 // Parse text representation of a single Linux capability. // // It accepts all variations recognized by Docker's --cap-add, such as // 'chown', 'cap_chown', and 'CHOWN'. Additionally, we allow numeric // values, e.g. '42' to support future capabilities that are not yet // known to us. func Parse(s string) (Mask, error) { if index, err := strconv.ParseUint(s, 10, 6); err == nil { return 1 << index, nil } name := strings.ToUpper(s) if name == "ALL" { return allKnownCaps(), nil } name = strings.TrimPrefix(name, "CAP_") if index, ok := nameToIndex[name]; ok { return 1 << index, nil } return 0, fmt.Errorf("unknown capability: %#v", s) } func allKnownCaps() Mask { var mask Mask for _, index := range nameToIndex { mask |= 1 << index } return mask } var nameToIndex = map[string]int{ "CHOWN": 0, "DAC_OVERRIDE": 1, "DAC_READ_SEARCH": 2, "FOWNER": 3, "FSETID": 4, "KILL": 5, "SETGID": 6, "SETUID": 7, "SETPCAP": 8, "LINUX_IMMUTABLE": 9, "NET_BIND_SERVICE": 10, "NET_BROADCAST": 11, "NET_ADMIN": 12, "NET_RAW": 13, "IPC_LOCK": 14, "IPC_OWNER": 15, "SYS_MODULE": 16, "SYS_RAWIO": 17, "SYS_CHROOT": 18, "SYS_PTRACE": 19, "SYS_PACCT": 20, "SYS_ADMIN": 21, "SYS_BOOT": 22, "SYS_NICE": 23, "SYS_RESOURCE": 24, "SYS_TIME": 25, "SYS_TTY_CONFIG": 26, "MKNOD": 27, "LEASE": 28, "AUDIT_WRITE": 29, "AUDIT_CONTROL": 30, "SETFCAP": 31, "MAC_OVERRIDE": 32, "MAC_ADMIN": 33, "SYSLOG": 34, "WAKE_ALARM": 35, "BLOCK_SUSPEND": 36, "AUDIT_READ": 37, "PERFMON": 38, "BPF": 39, "CHECKPOINT_RESTORE": 40, } // Flags alter certain aspects of capabilities handling type Flags uint32 const ( // FlagEffective causes all of the new permitted capabilities to be // also raised in the effective set diring execve(2) FlagEffective Flags = 1 ) // XattrBytes encodes capabilities in the format of // security.capability extended filesystem attribute. This is how Linux // tracks file capabilities internally. func XattrBytes(permitted, inheritable Mask, flags Flags) ([]byte, error) { // Underlying data layout as defined by Linux kernel (vfs_ns_cap_data) type vfsNsCapData struct { MagicEtc uint32 Data [2]struct { Permitted uint32 Inheritable uint32 } } const vfsCapRevision2 = 0x02000000 data := vfsNsCapData{MagicEtc: vfsCapRevision2 | uint32(flags)} data.Data[0].Permitted = uint32(permitted) data.Data[0].Inheritable = uint32(inheritable) data.Data[1].Permitted = uint32(permitted >> 32) data.Data[1].Inheritable = uint32(inheritable >> 32) buf := &bytes.Buffer{} if err := binary.Write(buf, binary.LittleEndian, data); err != nil { return nil, err } return buf.Bytes(), nil } // FileCaps encodes Linux file capabilities type FileCaps struct { permitted, inheritable Mask flags Flags } // NewFileCaps produces file capabilities object from a list of string // terms. A term is either a single capability name (added as permitted) // or a cap_from_text(3) clause. func NewFileCaps(terms ...string) (*FileCaps, error) { var permitted, inheritable, effective Mask for _, term := range terms { var caps, actionList string if index := strings.IndexAny(term, "+-="); index != -1 { caps, actionList = term[:index], term[index:] } else { mask, err := Parse(term) if err != nil { return nil, err } permitted |= mask continue } // Handling cap_from_text(3) syntax, e.g. cap1,cap2=pie if caps == "" && actionList[0] == '=' { caps = "all" } var mask, mask2 Mask for capname := range strings.SplitSeq(caps, ",") { m, err := Parse(capname) if err != nil { return nil, fmt.Errorf("%#v: %w", term, err) } mask |= m } for _, c := range actionList { switch c { case '+': mask2 = ^Mask(0) case '-': mask2 = ^mask case '=': mask2 = ^Mask(0) permitted &= ^mask inheritable &= ^mask effective &= ^mask case 'p': permitted = (permitted | mask) & mask2 case 'i': inheritable = (inheritable | mask) & mask2 case 'e': effective = (effective | mask) & mask2 default: return nil, fmt.Errorf("%#v: unknown flag '%c'", term, c) } } } if permitted != 0 || inheritable != 0 { var flags Flags if effective != 0 { flags = FlagEffective } return &FileCaps{permitted: permitted, inheritable: inheritable, flags: flags}, nil } return nil, nil } // ToXattrBytes encodes capabilities in the format of // security.capability extended filesystem attribute. func (fc *FileCaps) ToXattrBytes() ([]byte, error) { return XattrBytes(fc.permitted, fc.inheritable, fc.flags) } ================================================ FILE: pkg/caps/caps_dd_test.go ================================================ // Generated file, do not edit. // Copyright 2024 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package caps var ddTests = []ddTest{ {permitted: "chown", inheritable: "", effective: false, res: "AAAAAgEAAAAAAAAAAAAAAAAAAAA="}, {permitted: "chown", inheritable: "", effective: true, res: "AQAAAgEAAAAAAAAAAAAAAAAAAAA="}, {permitted: "", inheritable: "chown", effective: false, res: "AAAAAgAAAAABAAAAAAAAAAAAAAA="}, {permitted: "chown", inheritable: "chown", effective: true, res: "AQAAAgEAAAABAAAAAAAAAAAAAAA="}, {permitted: "dac_override", inheritable: "dac_override", effective: true, res: "AQAAAgIAAAACAAAAAAAAAAAAAAA="}, {permitted: "dac_read_search", inheritable: "dac_read_search", effective: true, res: "AQAAAgQAAAAEAAAAAAAAAAAAAAA="}, {permitted: "fowner", inheritable: "fowner", effective: true, res: "AQAAAggAAAAIAAAAAAAAAAAAAAA="}, {permitted: "fsetid", inheritable: "fsetid", effective: true, res: "AQAAAhAAAAAQAAAAAAAAAAAAAAA="}, {permitted: "kill", inheritable: "kill", effective: true, res: "AQAAAiAAAAAgAAAAAAAAAAAAAAA="}, {permitted: "setgid", inheritable: "setgid", effective: true, res: "AQAAAkAAAABAAAAAAAAAAAAAAAA="}, {permitted: "setuid", inheritable: "setuid", effective: true, res: "AQAAAoAAAACAAAAAAAAAAAAAAAA="}, {permitted: "setpcap", inheritable: "setpcap", effective: true, res: "AQAAAgABAAAAAQAAAAAAAAAAAAA="}, {permitted: "linux_immutable", inheritable: "linux_immutable", effective: true, res: "AQAAAgACAAAAAgAAAAAAAAAAAAA="}, {permitted: "net_bind_service", inheritable: "net_bind_service", effective: true, res: "AQAAAgAEAAAABAAAAAAAAAAAAAA="}, {permitted: "net_broadcast", inheritable: "net_broadcast", effective: true, res: "AQAAAgAIAAAACAAAAAAAAAAAAAA="}, {permitted: "net_admin", inheritable: "net_admin", effective: true, res: "AQAAAgAQAAAAEAAAAAAAAAAAAAA="}, {permitted: "net_raw", inheritable: "net_raw", effective: true, res: "AQAAAgAgAAAAIAAAAAAAAAAAAAA="}, {permitted: "ipc_lock", inheritable: "ipc_lock", effective: true, res: "AQAAAgBAAAAAQAAAAAAAAAAAAAA="}, {permitted: "ipc_owner", inheritable: "ipc_owner", effective: true, res: "AQAAAgCAAAAAgAAAAAAAAAAAAAA="}, {permitted: "sys_module", inheritable: "sys_module", effective: true, res: "AQAAAgAAAQAAAAEAAAAAAAAAAAA="}, {permitted: "sys_rawio", inheritable: "sys_rawio", effective: true, res: "AQAAAgAAAgAAAAIAAAAAAAAAAAA="}, {permitted: "sys_chroot", inheritable: "sys_chroot", effective: true, res: "AQAAAgAABAAAAAQAAAAAAAAAAAA="}, {permitted: "sys_ptrace", inheritable: "sys_ptrace", effective: true, res: "AQAAAgAACAAAAAgAAAAAAAAAAAA="}, {permitted: "sys_pacct", inheritable: "sys_pacct", effective: true, res: "AQAAAgAAEAAAABAAAAAAAAAAAAA="}, {permitted: "sys_admin", inheritable: "sys_admin", effective: true, res: "AQAAAgAAIAAAACAAAAAAAAAAAAA="}, {permitted: "sys_boot", inheritable: "sys_boot", effective: true, res: "AQAAAgAAQAAAAEAAAAAAAAAAAAA="}, {permitted: "sys_nice", inheritable: "sys_nice", effective: true, res: "AQAAAgAAgAAAAIAAAAAAAAAAAAA="}, {permitted: "sys_resource", inheritable: "sys_resource", effective: true, res: "AQAAAgAAAAEAAAABAAAAAAAAAAA="}, {permitted: "sys_time", inheritable: "sys_time", effective: true, res: "AQAAAgAAAAIAAAACAAAAAAAAAAA="}, {permitted: "sys_tty_config", inheritable: "sys_tty_config", effective: true, res: "AQAAAgAAAAQAAAAEAAAAAAAAAAA="}, {permitted: "mknod", inheritable: "mknod", effective: true, res: "AQAAAgAAAAgAAAAIAAAAAAAAAAA="}, {permitted: "lease", inheritable: "lease", effective: true, res: "AQAAAgAAABAAAAAQAAAAAAAAAAA="}, {permitted: "audit_write", inheritable: "audit_write", effective: true, res: "AQAAAgAAACAAAAAgAAAAAAAAAAA="}, {permitted: "audit_control", inheritable: "audit_control", effective: true, res: "AQAAAgAAAEAAAABAAAAAAAAAAAA="}, {permitted: "setfcap", inheritable: "setfcap", effective: true, res: "AQAAAgAAAIAAAACAAAAAAAAAAAA="}, {permitted: "mac_override", inheritable: "mac_override", effective: true, res: "AQAAAgAAAAAAAAAAAQAAAAEAAAA="}, {permitted: "mac_admin", inheritable: "mac_admin", effective: true, res: "AQAAAgAAAAAAAAAAAgAAAAIAAAA="}, {permitted: "syslog", inheritable: "syslog", effective: true, res: "AQAAAgAAAAAAAAAABAAAAAQAAAA="}, {permitted: "wake_alarm", inheritable: "wake_alarm", effective: true, res: "AQAAAgAAAAAAAAAACAAAAAgAAAA="}, {permitted: "block_suspend", inheritable: "block_suspend", effective: true, res: "AQAAAgAAAAAAAAAAEAAAABAAAAA="}, {permitted: "audit_read", inheritable: "audit_read", effective: true, res: "AQAAAgAAAAAAAAAAIAAAACAAAAA="}, {permitted: "perfmon", inheritable: "perfmon", effective: true, res: "AQAAAgAAAAAAAAAAQAAAAEAAAAA="}, {permitted: "bpf", inheritable: "bpf", effective: true, res: "AQAAAgAAAAAAAAAAgAAAAIAAAAA="}, {permitted: "checkpoint_restore", inheritable: "checkpoint_restore", effective: true, res: "AQAAAgAAAAAAAAAAAAEAAAABAAA="}, } ================================================ FILE: pkg/caps/caps_test.go ================================================ // Copyright 2024 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package caps import ( "encoding/base64" "fmt" "testing" ) func TestParse(t *testing.T) { tests := []struct { arg string res Mask mustFail bool }{ {arg: "chown", res: 1}, {arg: "cap_chown", res: 1}, {arg: "cAp_cHoWn", res: 1}, {arg: "unknown", mustFail: true}, {arg: "63", res: 1 << 63}, {arg: "64", mustFail: true}, {arg: "all", res: allKnownCaps()}, } for _, tc := range tests { t.Run(tc.arg, func(t *testing.T) { mask, err := Parse(tc.arg) if err == nil && tc.mustFail { t.Fatal("invalid input accepted") } if err != nil && !tc.mustFail { t.Fatal(err) } if mask != tc.res { t.Fatalf("unexpected result: %x", mask) } }) } } //go:generate ./gen.sh type ddTest struct { permitted, inheritable string effective bool res string } func TestDd(t *testing.T) { for _, test := range ddTests { label := fmt.Sprintf("%s,%s,%v", test.permitted, test.inheritable, test.effective) t.Run(label, func(t *testing.T) { var permitted, inheritable Mask var flags Flags if test.permitted != "" { mask, err := Parse(test.permitted) if err != nil { t.Fatal(err) } permitted = mask } if test.inheritable != "" { mask, err := Parse(test.inheritable) if err != nil { t.Fatal(err) } inheritable = mask } if test.effective { flags = FlagEffective } res, err := XattrBytes(permitted, inheritable, flags) if err != nil { t.Fatal(err) } resBase64 := make([]byte, base64.StdEncoding.EncodedLen(len(res))) base64.StdEncoding.Encode(resBase64, res) if string(resBase64) != test.res { t.Fatalf("expected %s, result %s", test.res, resBase64) } }) } } ================================================ FILE: pkg/caps/gen.sh ================================================ #!/usr/bin/env bash # Copyright 2024 ko Build Authors All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # This script assigns different capabilities to files and captures # resulting xattr blobs for testing (generates caps_dd_test.go). # # It has to be run on a reasonably recent Linux to ensure that the full # set of capabilities is supported. Setting capabilities requires # privileges; the script assumes paswordless sudo is available. set -o errexit set -o nounset set -o pipefail shopt -s inherit_errexit # capblob CAP_STRING # Obtain base64-encoded value of the underlying xattr that implemens # specified capabilities, setcap syntax. # Example: capblob cap_chown=eip capblob() { f=$(mktemp) sudo -n setcap $1 $f getfattr -n security.capability --absolute-names --only-values $f | base64 rm $f } ( license=$(sed -e '/^$/,$d' caps.go) echo "// Generated file, do not edit." echo "" echo "$license" echo "" echo "package caps" echo "var ddTests = []ddTest{" res=$(capblob cap_chown=p) echo "{permitted: \"chown\", inheritable: \"\", effective: false, res: \"$res\"}," res=$(capblob cap_chown=ep) echo "{permitted: \"chown\", inheritable: \"\", effective: true, res: \"$res\"}," res=$(capblob cap_chown=i) echo "{permitted: \"\", inheritable: \"chown\", effective: false, res: \"$res\"}," CAPS="chown dac_override dac_read_search fowner fsetid kill setgid setuid setpcap linux_immutable net_bind_service net_broadcast net_admin net_raw ipc_lock ipc_owner sys_module sys_rawio sys_chroot sys_ptrace sys_pacct sys_admin sys_boot sys_nice sys_resource sys_time sys_tty_config mknod lease audit_write audit_control setfcap mac_override mac_admin syslog wake_alarm block_suspend audit_read perfmon bpf checkpoint_restore" for cap in $CAPS; do res=$(capblob cap_$cap=eip) echo "{permitted: \"$cap\", inheritable: \"$cap\", effective: true, res: \"$res\"}," done echo "}" ) > caps_dd_test.go gofmt -w -s ./caps_dd_test.go ================================================ FILE: pkg/caps/new_file_caps_test.go ================================================ // Copyright 2024 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package caps import ( "reflect" "strings" "testing" ) func TestNewFileCaps(t *testing.T) { tests := []struct { args []string res *FileCaps mustFail bool }{ {}, { args: []string{"chown", "dac_override", "dac_read_search"}, res: &FileCaps{permitted: 7}, }, { args: []string{"chown,dac_override,dac_read_search=p"}, res: &FileCaps{permitted: 7}, }, { args: []string{"chown,dac_override,dac_read_search=i"}, res: &FileCaps{inheritable: 7}, }, { args: []string{"chown,dac_override,dac_read_search=e"}, }, { args: []string{"chown,dac_override,dac_read_search=pe"}, res: &FileCaps{permitted: 7, flags: FlagEffective}, }, { args: []string{"=pe"}, res: &FileCaps{permitted: allKnownCaps(), flags: FlagEffective}, }, { args: []string{"chown=ie", "chown=p"}, res: &FileCaps{permitted: 1}, }, { args: []string{"chown=ie", "chown="}, }, { args: []string{"chown=ie", "chown+p"}, res: &FileCaps{permitted: 1, inheritable: 1, flags: FlagEffective}, }, { args: []string{"chown=pie", "dac_override,chown-p"}, res: &FileCaps{inheritable: 1, flags: FlagEffective}, }, {args: []string{"chown,=pie"}, mustFail: true}, {args: []string{"-pie"}, mustFail: true}, {args: []string{"+pie"}, mustFail: true}, {args: []string{"="}}, } for _, tc := range tests { label := strings.Join(tc.args, ":") t.Run(label, func(t *testing.T) { res, err := NewFileCaps(tc.args...) if tc.mustFail && err == nil { t.Fatal("didn't fail") } if !tc.mustFail && err != nil { t.Fatalf("unexpectedly failed: %v", err) } if !reflect.DeepEqual(res, tc.res) { t.Fatalf("got %v expected %v", res, tc.res) } }) } } ================================================ FILE: pkg/commands/apply.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package commands import ( "errors" "fmt" "os" "os/exec" "github.com/google/ko/pkg/commands/options" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" ) // addApply augments our CLI surface with apply. func addApply(topLevel *cobra.Command) { po := &options.PublishOptions{} fo := &options.FilenameOptions{} so := &options.SelectorOptions{} bo := &options.BuildOptions{} apply := &cobra.Command{ Use: "apply -f FILENAME", Short: "Apply the input files with image references resolved to built/pushed image digests.", Long: `This sub-command finds import path references within the provided files, builds them into Go binaries, containerizes them, publishes them, and then feeds the resulting yaml into "kubectl apply".`, Example: ` # Build and publish import path references to a Docker # Registry as: # ${KO_DOCKER_REPO}/- # Then, feed the resulting yaml into "kubectl apply". # When KO_DOCKER_REPO is ko.local, it is the same as if # --local was passed. ko apply -f config/ # Build and publish import path references to a Docker # Registry preserving import path names as: # ${KO_DOCKER_REPO}/ # Then, feed the resulting yaml into "kubectl apply". ko apply --preserve-import-paths -f config/ # Build and publish import path references to a Docker # daemon as: # ko.local/ # Then, feed the resulting yaml into "kubectl apply". ko apply --local -f config/ # Apply from stdin: cat config.yaml | ko apply -f - # Any flags passed after '--' are passed to 'kubectl apply' directly: ko apply -f config -- --namespace=foo --kubeconfig=cfg.yaml `, RunE: func(cmd *cobra.Command, args []string) error { if err := options.Validate(po, bo); err != nil { return fmt.Errorf("validating options: %w", err) } if !isKubectlAvailable() { return errors.New("error: kubectl is not available. kubectl must be installed to use ko apply") } ctx := cmd.Context() bo.InsecureRegistry = po.InsecureRegistry builder, err := makeBuilder(ctx, bo) if err != nil { return fmt.Errorf("error creating builder: %w", err) } publisher, err := makePublisher(po) if err != nil { return fmt.Errorf("error creating publisher: %w", err) } defer publisher.Close() // Issue a "kubectl apply" command reading from stdin, // to which we will pipe the resolved files, and any // remaining flags passed after '--'. kubectlCmd := exec.CommandContext(ctx, "kubectl", append([]string{"apply", "-f", "-"}, args...)...) //nolint:gosec // Pass through our environment kubectlCmd.Env = os.Environ() // Pass through our std{out,err} and make our resolved buffer stdin. kubectlCmd.Stderr = os.Stderr kubectlCmd.Stdout = os.Stdout // Wire up kubectl stdin to resolveFilesToWriter. stdin, err := kubectlCmd.StdinPipe() if err != nil { return fmt.Errorf("error piping to 'kubectl apply': %w", err) } // Make sure builds are cancelled if kubectl apply fails. g, ctx := errgroup.WithContext(ctx) g.Go(func() error { // kubectl buffers data before starting to apply it, which // can lead to resources being created more slowly than desired. // In the case of --watch, it can lead to resources not being // applied at all until enough iteration has occurred. To work // around this, we prime the stream with a bunch of empty objects // which kubectl will discard. // See https://github.com/google/go-containerregistry/pull/348 for range 1000 { stdin.Write([]byte("---\n")) } // Once primed kick things off. return ResolveFilesToWriter(ctx, builder, publisher, fo, so, stdin) }) g.Go(func() error { // Run it. if err := kubectlCmd.Run(); err != nil { return fmt.Errorf("error executing 'kubectl apply': %w", err) } return nil }) return g.Wait() }, } options.AddPublishArg(apply, po) options.AddFileArg(apply, fo) options.AddSelectorArg(apply, so) options.AddBuildOptions(apply, bo) topLevel.AddCommand(apply) } ================================================ FILE: pkg/commands/build.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package commands import ( "fmt" "github.com/google/ko/pkg/commands/options" "github.com/spf13/cobra" ) // addBuild augments our CLI surface with build. func addBuild(topLevel *cobra.Command) { po := &options.PublishOptions{} bo := &options.BuildOptions{} build := &cobra.Command{ Use: "build IMPORTPATH...", Short: "Build and publish container images from the given importpaths.", Long: `This sub-command builds the provided import paths into Go binaries, containerizes them, and publishes them.`, Aliases: []string{"publish"}, Example: ` # Build and publish import path references to a Docker Registry as: # ${KO_DOCKER_REPO}/- # When KO_DOCKER_REPO is ko.local, it is the same as if --local and # --preserve-import-paths were passed. # If the import path is not provided, the current working directory is the # default. ko build github.com/foo/bar/cmd/baz github.com/foo/bar/cmd/blah # Build and publish a relative import path as: # ${KO_DOCKER_REPO}/- # When KO_DOCKER_REPO is ko.local, it is the same as if --local and # --preserve-import-paths were passed. ko build ./cmd/blah # Build and publish a relative import path as: # ${KO_DOCKER_REPO}/ # When KO_DOCKER_REPO is ko.local, it is the same as if --local was passed. ko build --preserve-import-paths ./cmd/blah # Build and publish import path references to a Docker daemon as: # ko.local/ # This always preserves import paths. ko build --local github.com/foo/bar/cmd/baz github.com/foo/bar/cmd/blah`, RunE: func(cmd *cobra.Command, args []string) error { if err := options.Validate(po, bo); err != nil { return fmt.Errorf("validating options: %w", err) } if len(args) == 0 { // Build the current directory by default. args = []string{"."} } ctx := cmd.Context() bo.InsecureRegistry = po.InsecureRegistry builder, err := makeBuilder(ctx, bo) if err != nil { return fmt.Errorf("error creating builder: %w", err) } publisher, err := makePublisher(po) if err != nil { return fmt.Errorf("error creating publisher: %w", err) } defer publisher.Close() images, err := publishImages(ctx, args, publisher, builder) if err != nil { return fmt.Errorf("failed to publish images: %w", err) } for _, img := range images { fmt.Println(img) } return nil }, } options.AddPublishArg(build, po) options.AddBuildOptions(build, bo) topLevel.AddCommand(build) } ================================================ FILE: pkg/commands/cache.go ================================================ // Copyright 2023 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package commands import ( "bytes" "context" "fmt" "io" "os" "path/filepath" "sync" "github.com/google/go-containerregistry/pkg/logs" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/layout" "github.com/google/go-containerregistry/pkg/v1/match" "github.com/google/go-containerregistry/pkg/v1/partial" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/types" "github.com/google/ko/pkg/build" ) type imageCache struct { // In memory cache sync.Map // On disk mu sync.Mutex p *layout.Path // Over the network puller *remote.Puller } func newCache(puller *remote.Puller) (*imageCache, error) { cache := &imageCache{ puller: puller, } if kc := os.Getenv("KOCACHE"); kc != "" { path := filepath.Join(kc, "img") p, err := layout.FromPath(path) if err != nil { p, err = layout.Write(path, empty.Index) if err != nil { return cache, err } } cache.p = &p } return cache, nil } func (i *imageCache) get(ctx context.Context, ref name.Reference, missFunc baseFactory) (build.Result, error) { if v, ok := i.cache.Load(ref.String()); ok { logs.Debug.Printf("cache hit: %s", ref.String()) return v.(build.Result), nil } var ( once sync.Once missResult build.Result missErr error ) miss := func(ctx context.Context, ref name.Reference) (build.Result, error) { once.Do(func() { missResult, missErr = missFunc(ctx, ref) }) return missResult, missErr } if i.p != nil { key := "" if _, ok := ref.(name.Digest); ok { key = ref.Identifier() } else { logs.Debug.Printf("cache miss due to tag: %s", ref.String()) result, err := miss(ctx, ref) if err != nil { return result, err } dig, err := result.Digest() if err != nil { return result, err } key = dig.String() } // Use a pretty broad lock on the on-disk cache to avoid races. i.mu.Lock() defer i.mu.Unlock() ii, err := i.p.ImageIndex() if err != nil { return nil, fmt.Errorf("loading cache index: %w", err) } h, err := v1.NewHash(key) if err != nil { return nil, err } descs, err := partial.FindManifests(ii, match.Digests(h)) if err != nil { return nil, err } if len(descs) != 0 { logs.Debug.Printf("cache hit: %s", ref.String()) desc := descs[0] var br build.Result if desc.MediaType.IsIndex() { idx, err := ii.ImageIndex(h) if err != nil { return nil, err } br, err = i.newLazyIndex(ref, idx, missFunc) if err != nil { return nil, err } } else { img, err := ii.Image(h) if err != nil { return nil, err } br, err = i.newLazyImage(ref, img, missFunc) if err != nil { return nil, err } } i.cache.Store(ref.String(), br) return br, nil } } logs.Debug.Printf("cache miss: %s", ref.String()) result, err := miss(ctx, ref) if err != nil { return result, err } if i.p != nil { logs.Debug.Printf("cache store: %s", ref.String()) desc, err := partial.Descriptor(result) if err != nil { return result, err } manifest, err := result.RawManifest() if err != nil { return result, err } if err := i.p.WriteBlob(desc.Digest, io.NopCloser(bytes.NewReader(manifest))); err != nil { return result, err } if _, ok := result.(v1.ImageIndex); ok { result = &lazyIndex{ ref: ref, desc: *desc, manifest: manifest, miss: missFunc, cache: i, } } else if img, ok := result.(v1.Image); ok { cf, err := img.RawConfigFile() if err != nil { return result, err } id, err := img.ConfigName() if err != nil { return result, err } if err := i.p.WriteBlob(id, io.NopCloser(bytes.NewReader(cf))); err != nil { return result, err } result = &lazyImage{ ref: ref, desc: *desc, manifest: manifest, config: cf, id: id, miss: missFunc, cache: i, } } if err := i.p.AppendDescriptor(*desc); err != nil { return result, err } } i.cache.Store(ref.String(), result) return result, nil } func (i *imageCache) newLazyIndex(ref name.Reference, idx v1.ImageIndex, missFunc baseFactory) (*lazyIndex, error) { desc, err := partial.Descriptor(idx) if err != nil { return nil, err } manifest, err := idx.RawManifest() if err != nil { return nil, err } return &lazyIndex{ ref: ref, desc: *desc, manifest: manifest, miss: missFunc, cache: i, }, nil } func (i *imageCache) newLazyImage(ref name.Reference, img v1.Image, missFunc baseFactory) (*lazyImage, error) { desc, err := partial.Descriptor(img) if err != nil { return nil, err } manifest, err := img.RawManifest() if err != nil { return nil, err } cf, err := img.RawConfigFile() if err != nil { return nil, err } id, err := img.ConfigName() if err != nil { return nil, err } return &lazyImage{ ref: ref, desc: *desc, manifest: manifest, config: cf, id: id, miss: missFunc, cache: i, }, nil } type lazyIndex struct { ref name.Reference desc v1.Descriptor manifest []byte miss baseFactory cache *imageCache } func (i *lazyIndex) MediaType() (types.MediaType, error) { return i.desc.MediaType, nil } func (i *lazyIndex) Digest() (v1.Hash, error) { return i.desc.Digest, nil } func (i *lazyIndex) Size() (int64, error) { return i.desc.Size, nil } func (i *lazyIndex) IndexManifest() (*v1.IndexManifest, error) { return v1.ParseIndexManifest(bytes.NewReader(i.manifest)) } func (i *lazyIndex) RawManifest() ([]byte, error) { return i.manifest, nil } func (i *lazyIndex) Image(h v1.Hash) (v1.Image, error) { br, err := i.cache.get(context.TODO(), i.ref.Context().Digest(h.String()), i.miss) if err != nil { return nil, fmt.Errorf("Image(%q): %w", h.String(), err) } img, ok := br.(v1.Image) if !ok { return nil, fmt.Errorf("Image(%q) is a type %T", h.String(), br) } return img, nil } func (i *lazyIndex) ImageIndex(h v1.Hash) (v1.ImageIndex, error) { br, err := i.cache.get(context.TODO(), i.ref.Context().Digest(h.String()), i.miss) if err != nil { return nil, err } ii, ok := br.(v1.ImageIndex) if !ok { return nil, fmt.Errorf("ImageIndex(%q) is a type %T", h.String(), br) } return ii, nil } type lazyImage struct { ref name.Reference desc v1.Descriptor manifest []byte config []byte id v1.Hash miss baseFactory cache *imageCache } // Layers returns the ordered collection of filesystem layers that comprise this image. // The order of the list is oldest/base layer first, and most-recent/top layer last. func (i *lazyImage) Layers() ([]v1.Layer, error) { m, err := i.Manifest() if err != nil { return nil, err } layers := make([]v1.Layer, 0, len(m.Layers)) for _, desc := range m.Layers { diffid, err := partial.BlobToDiffID(i, desc.Digest) if err != nil { return nil, err } layers = append(layers, &lazyLayer{ ref: i.ref.Context().Digest(desc.Digest.String()), desc: desc, diffid: diffid, miss: i.miss, cache: i.cache, }) } return layers, nil } func (i *lazyImage) MediaType() (types.MediaType, error) { return i.desc.MediaType, nil } func (i *lazyImage) Digest() (v1.Hash, error) { return i.desc.Digest, nil } func (i *lazyImage) Size() (int64, error) { return i.desc.Size, nil } func (i *lazyImage) ConfigName() (v1.Hash, error) { return i.id, nil } func (i *lazyImage) ConfigFile() (*v1.ConfigFile, error) { return v1.ParseConfigFile(bytes.NewReader(i.config)) } func (i *lazyImage) RawConfigFile() ([]byte, error) { return i.config, nil } func (i *lazyImage) Manifest() (*v1.Manifest, error) { return v1.ParseManifest(bytes.NewReader(i.manifest)) } func (i *lazyImage) RawManifest() ([]byte, error) { return i.manifest, nil } func (i *lazyImage) LayerByDigest(h v1.Hash) (v1.Layer, error) { if h == i.id { return partial.ConfigLayer(i) } layers, err := i.Layers() if err != nil { return nil, err } for _, layer := range layers { digest, err := layer.Digest() if err != nil { return nil, err } if digest == h { return layer, nil } } return nil, fmt.Errorf("could not find layer %q in lazyImage %q", h.String(), i.ref) } func (i *lazyImage) LayerByDiffID(h v1.Hash) (v1.Layer, error) { d, err := partial.DiffIDToBlob(i, h) if err != nil { return nil, err } return i.LayerByDigest(d) } type lazyLayer struct { ref name.Digest desc v1.Descriptor diffid v1.Hash miss baseFactory cache *imageCache } func (l *lazyLayer) Digest() (v1.Hash, error) { return l.desc.Digest, nil } func (l *lazyLayer) DiffID() (v1.Hash, error) { return l.diffid, nil } func (l *lazyLayer) Size() (int64, error) { return l.desc.Size, nil } func (l *lazyLayer) MediaType() (types.MediaType, error) { return l.desc.MediaType, nil } func (l *lazyLayer) Compressed() (io.ReadCloser, error) { if rc, err := l.cache.p.Blob(l.desc.Digest); err == nil { return rc, nil } rl, err := l.cache.puller.Layer(context.TODO(), l.ref) if err != nil { return nil, err } // Note that we intentionally don't cache this because it will slow down cases where registry has it. return rl.Compressed() } func (l *lazyLayer) Uncompressed() (io.ReadCloser, error) { cl, err := partial.CompressedToLayer(l) if err != nil { return nil, err } return cl.Uncompressed() } ================================================ FILE: pkg/commands/commands.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package commands import ( "os/exec" "github.com/spf13/cobra" ) // AddKubeCommands augments our CLI surface with a passthru delete command, and an apply // command that realizes the promise of ko, as outlined here: // // https://github.com/google/go-containerregistry/issues/80 func AddKubeCommands(topLevel *cobra.Command) { addDelete(topLevel) addVersion(topLevel) addCreate(topLevel) addApply(topLevel) addResolve(topLevel) addBuild(topLevel) addRun(topLevel) } // check if kubectl is installed func isKubectlAvailable() bool { if _, err := exec.LookPath("kubectl"); err != nil { return false } return true } ================================================ FILE: pkg/commands/config.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package commands import ( "context" "fmt" "io" "log" "os" "strconv" "strings" "time" ecr "github.com/awslabs/amazon-ecr-credential-helper/ecr-login" "github.com/chrismellard/docker-credential-acr-env/pkg/credhelper" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/authn/github" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/daemon" "github.com/google/go-containerregistry/pkg/v1/google" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/ko/pkg/build" "github.com/google/ko/pkg/commands/options" "github.com/google/ko/pkg/publish" ) var ( amazonKeychain authn.Keychain = authn.NewKeychainFromHelper(ecr.NewECRHelper(ecr.WithLogger(io.Discard))) azureKeychain authn.Keychain = authn.NewKeychainFromHelper(credhelper.NewACRCredentialsHelper()) keychain = authn.NewMultiKeychain( amazonKeychain, authn.DefaultKeychain, google.Keychain, github.Keychain, azureKeychain, ) ) // getBaseImage returns a function that determines the base image for a given import path. func getBaseImage(bo *options.BuildOptions) build.GetBase { userAgent := ua() if bo.UserAgent != "" { userAgent = bo.UserAgent } ropt := []remote.Option{ remote.WithAuthFromKeychain(keychain), remote.WithUserAgent(userAgent), } puller, err := remote.NewPuller(ropt...) if err != nil { // This can't really happen. panic(err) } ropt = append(ropt, remote.Reuse(puller)) cache, err := newCache(puller) if err != nil { log.Printf("Image cache init failed: %v", err) } fetch := func(ctx context.Context, ref name.Reference) (build.Result, error) { ropt = append(ropt, remote.WithContext(ctx)) desc, err := remote.Get(ref, ropt...) if err != nil { return nil, err } if desc.MediaType.IsIndex() { return desc.ImageIndex() } return desc.Image() } return func(ctx context.Context, s string) (name.Reference, build.Result, error) { s = strings.TrimPrefix(s, build.StrictScheme) // Viper configuration file keys are case insensitive, and are // returned as all lowercase. This means that import paths with // uppercase must be normalized for matching here, e.g. // github.com/GoogleCloudPlatform/foo/cmd/bar // comes through as: // github.com/googlecloudplatform/foo/cmd/bar baseImage, ok := bo.BaseImageOverrides[strings.ToLower(s)] if !ok || baseImage == "" { baseImage = bo.BaseImage } var nameOpts []name.Option if bo.InsecureRegistry { nameOpts = append(nameOpts, name.Insecure) } ref, err := name.ParseReference(baseImage, nameOpts...) if err != nil { return nil, nil, fmt.Errorf("parsing base image (%q): %w", baseImage, err) } var result build.Result // For ko.local, look in the daemon. if ref.Context().RegistryStr() == publish.LocalDomain { result, err = daemon.Image(ref) if err != nil { return nil, nil, fmt.Errorf("loading %s from daemon: %w", ref, err) } } else { result, err = cache.get(ctx, ref, fetch) if err != nil { // We don't expect this to fail, usually, but the cache should also not be fatal. // Log it so people can complain about it and we can fix the cache. log.Printf("cache.get(%q) failed with %v", ref.String(), err) result, err = fetch(ctx, ref) if err != nil { return nil, nil, fmt.Errorf("pulling %s: %w", ref, err) } } } if _, ok := ref.(name.Digest); ok { log.Printf("Using base %s for %s", ref, s) } else { dig, err := result.Digest() if err != nil { return ref, result, err } log.Printf("Using base %s@%s for %s", ref, dig, s) } return ref, result, nil } } func getTimeFromEnv(env string) (*v1.Time, error) { epoch := os.Getenv(env) if epoch == "" { return nil, nil } seconds, err := strconv.ParseInt(epoch, 10, 64) if err != nil { return nil, fmt.Errorf("the environment variable %s should be the number of seconds since January 1st 1970, 00:00 UTC, got: %w", env, err) } return &v1.Time{Time: time.Unix(seconds, 0)}, nil } func getCreationTime() (*v1.Time, error) { return getTimeFromEnv("SOURCE_DATE_EPOCH") } func getKoDataCreationTime() (*v1.Time, error) { return getTimeFromEnv("KO_DATA_DATE_EPOCH") } type baseFactory func(context.Context, name.Reference) (build.Result, error) ================================================ FILE: pkg/commands/config_test.go ================================================ // Copyright 2021 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package commands import ( "context" "fmt" "testing" "github.com/google/go-containerregistry/pkg/crane" "github.com/google/ko/pkg/commands/options" ) func TestOverrideDefaultBaseImageUsingBuildOption(t *testing.T) { namespace := "base" s, err := registryServerWithImage(namespace) if err != nil { t.Fatalf("could not create test registry server: %v", err) } defer s.Close() baseImage := fmt.Sprintf("%s/%s", s.Listener.Addr().String(), namespace) wantDigest, err := crane.Digest(baseImage) if err != nil { t.Fatalf("crane.Digest(%s): %v", baseImage, err) } wantImage := fmt.Sprintf("%s@%s", baseImage, wantDigest) bo := &options.BuildOptions{ BaseImage: wantImage, Platforms: []string{"all"}, } baseFn := getBaseImage(bo) _, res, err := baseFn(context.Background(), "ko://example.com/helloworld") if err != nil { t.Fatalf("getBaseImage(): %v", err) } digest, err := res.Digest() if err != nil { t.Fatalf("res.Digest(): %v", err) } gotDigest := digest.String() if gotDigest != wantDigest { t.Errorf("got digest %s, wanted %s", gotDigest, wantDigest) } } ================================================ FILE: pkg/commands/create.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package commands import ( "errors" "fmt" "os" "os/exec" "github.com/google/ko/pkg/commands/options" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" ) // addCreate augments our CLI surface with apply. func addCreate(topLevel *cobra.Command) { po := &options.PublishOptions{} fo := &options.FilenameOptions{} so := &options.SelectorOptions{} bo := &options.BuildOptions{} create := &cobra.Command{ Use: "create -f FILENAME", Short: "Create the input files with image references resolved to built/pushed image digests.", Long: `This sub-command finds import path references within the provided files, builds them into Go binaries, containerizes them, publishes them, and then feeds the resulting yaml into "kubectl create".`, Example: ` # Build and publish import path references to a Docker # Registry as: # ${KO_DOCKER_REPO}/- # Then, feed the resulting yaml into "kubectl create". # When KO_DOCKER_REPO is ko.local, it is the same as if # --local was passed. ko create -f config/ # Build and publish import path references to a Docker # Registry preserving import path names as: # ${KO_DOCKER_REPO}/ # Then, feed the resulting yaml into "kubectl create". ko create --preserve-import-paths -f config/ # Build and publish import path references to a Docker # daemon as: # ko.local/ # Then, feed the resulting yaml into "kubectl create". ko create --local -f config/ # Create from stdin: cat config.yaml | ko create -f - # Any flags passed after '--' are passed to 'kubectl apply' directly: ko apply -f config -- --namespace=foo --kubeconfig=cfg.yaml `, RunE: func(cmd *cobra.Command, args []string) error { if err := options.Validate(po, bo); err != nil { return fmt.Errorf("validating options: %w", err) } if !isKubectlAvailable() { return errors.New("error: kubectl is not available. kubectl must be installed to use ko create") } ctx := cmd.Context() bo.InsecureRegistry = po.InsecureRegistry builder, err := makeBuilder(ctx, bo) if err != nil { return fmt.Errorf("error creating builder: %w", err) } publisher, err := makePublisher(po) if err != nil { return fmt.Errorf("error creating publisher: %w", err) } defer publisher.Close() // Issue a "kubectl create" command reading from stdin, // to which we will pipe the resolved files, and any // remaining flags passed after '--'. kubectlCmd := exec.CommandContext(ctx, "kubectl", append([]string{"create", "-f", "-"}, args...)...) //nolint:gosec // Pass through our environment kubectlCmd.Env = os.Environ() // Pass through our std{out,err} and make our resolved buffer stdin. kubectlCmd.Stderr = os.Stderr kubectlCmd.Stdout = os.Stdout // Wire up kubectl stdin to resolveFilesToWriter. stdin, err := kubectlCmd.StdinPipe() if err != nil { return fmt.Errorf("error piping to 'kubectl create': %w", err) } // Make sure builds are cancelled if kubectl create fails. g, ctx := errgroup.WithContext(ctx) g.Go(func() error { // kubectl buffers data before starting to create it, which // can lead to resources being created more slowly than desired. // In the case of --watch, it can lead to resources not being // applied at all until enough iteration has occurred. To work // around this, we prime the stream with a bunch of empty objects // which kubectl will discard. // See https://github.com/google/go-containerregistry/pull/348 for range 1000 { stdin.Write([]byte("---\n")) } // Once primed kick things off. return ResolveFilesToWriter(ctx, builder, publisher, fo, so, stdin) }) g.Go(func() error { // Run it. if err := kubectlCmd.Run(); err != nil { return fmt.Errorf("error executing 'kubectl create': %w", err) } return nil }) return g.Wait() }, } options.AddPublishArg(create, po) options.AddFileArg(create, fo) options.AddSelectorArg(create, so) options.AddBuildOptions(create, bo) topLevel.AddCommand(create) } ================================================ FILE: pkg/commands/delete.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package commands import ( "errors" "os" "os/exec" "github.com/spf13/cobra" ) // runCmd is suitable for use with cobra.Command's Run field. type runCmd func(*cobra.Command, []string) error // passthru returns a runCmd that simply passes our CLI arguments // through to a binary named command. func passthru(command string) runCmd { return func(cmd *cobra.Command, _ []string) error { if !isKubectlAvailable() { return errors.New("error: kubectl is not available. kubectl must be installed to use ko delete") } ctx := cmd.Context() // Start building a command line invocation by passing // through our arguments to command's CLI. //nolint:gosec // We actively want to pass arguments through, so this is fine. ecmd := exec.CommandContext(ctx, command, os.Args[1:]...) // Pass through our environment ecmd.Env = os.Environ() // Pass through our stdfoo ecmd.Stderr = os.Stderr ecmd.Stdout = os.Stdout ecmd.Stdin = os.Stdin // Run it. return ecmd.Run() } } // addDelete augments our CLI surface with publish. func addDelete(topLevel *cobra.Command) { topLevel.AddCommand(&cobra.Command{ Use: "delete", Short: `See "kubectl help delete" for detailed usage.`, RunE: passthru("kubectl"), // We ignore unknown flags to avoid importing everything Go exposes // from our commands. FParseErrWhitelist: cobra.FParseErrWhitelist{ UnknownFlags: true, }, }) } ================================================ FILE: pkg/commands/options/build.go ================================================ // Copyright 2019 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package options import ( "errors" "fmt" "os" "path/filepath" "reflect" "github.com/go-viper/mapstructure/v2" "github.com/google/go-containerregistry/pkg/name" "github.com/google/ko/pkg/build" "github.com/spf13/cobra" "github.com/spf13/viper" "golang.org/x/tools/go/packages" ) const ( // configDefaultBaseImage is the default base image if not specified in .ko.yaml. configDefaultBaseImage = "cgr.dev/chainguard/static:latest" ) // BuildOptions represents options for the ko builder. type BuildOptions struct { // BaseImage enables setting the default base image programmatically. // If non-empty, this takes precedence over the value in `.ko.yaml`. BaseImage string // BaseImageOverrides stores base image overrides for import paths. BaseImageOverrides map[string]string // DefaultPlatforms defines the default platforms when Platforms is not explicitly defined DefaultPlatforms []string // DefaultEnv defines the default environment when per-build value is not explicitly defined. DefaultEnv []string // DefaultFlags defines the default flags when per-build value is not explicitly defined. DefaultFlags []string // DefaultLdflags defines the default ldflags when per-build value is not explicitly defined. DefaultLdflags []string // WorkingDirectory allows for setting the working directory for invocations of the `go` tool. // Empty string means the current working directory. WorkingDirectory string ConcurrentBuilds int DisableOptimizations bool SBOM string SBOMDir string Platforms []string Labels []string Annotations []string User string Debug bool // UserAgent enables overriding the default value of the `User-Agent` HTTP // request header used when retrieving the base image. UserAgent string InsecureRegistry bool // Trimpath controls whether ko adds the `-trimpath` flag to `go build` by default. // The `-trimpath` flags aids in achieving reproducible builds, but it removes path information that is useful for interactive debugging. // Set this field to `false` and `DisableOptimizations` to `true` if you want to interactively debug the binary in the resulting image. // `AddBuildOptions()` defaults this field to `true`. Trimpath bool // BuildConfigs stores the per-image build config from `.ko.yaml`. BuildConfigs map[string]build.Config } func AddBuildOptions(cmd *cobra.Command, bo *BuildOptions) { cmd.Flags().IntVarP(&bo.ConcurrentBuilds, "jobs", "j", 0, "The maximum number of concurrent builds (default GOMAXPROCS)") cmd.Flags().BoolVar(&bo.DisableOptimizations, "disable-optimizations", bo.DisableOptimizations, "Disable optimizations when building Go code. Useful when you want to interactively debug the created container.") cmd.Flags().StringVar(&bo.SBOM, "sbom", "spdx", "The SBOM media type to use (none will disable SBOM synthesis and upload).") cmd.Flags().StringVar(&bo.SBOMDir, "sbom-dir", "", "Path to file where the SBOM will be written.") cmd.Flags().StringSliceVar(&bo.Platforms, "platform", []string{}, "Which platform to use when pulling a multi-platform base. Format: all | [/[/]][,platform]*") cmd.Flags().StringSliceVar(&bo.Labels, "image-label", []string{}, "Which labels (key=value[,key=value]) to add to the image.") cmd.Flags().StringSliceVar(&bo.Annotations, "image-annotation", []string{}, "Which annotations (key=value[,key=value]) to add to the OCI manifest.") cmd.Flags().StringVar(&bo.User, "image-user", "", "The default user the image should be run as.") cmd.Flags().BoolVar(&bo.Debug, "debug", bo.Debug, "Include Delve debugger into image and wrap around ko-app. This debugger will listen to port 40000.") bo.Trimpath = true } // LoadConfig reads build configuration from defaults, environment variables, and the `.ko.yaml` config file. func (bo *BuildOptions) LoadConfig() error { v := viper.New() if bo.WorkingDirectory == "" { bo.WorkingDirectory = "." } // If omitted, use this base image. v.SetDefault("defaultBaseImage", configDefaultBaseImage) const configName = ".ko" v.SetConfigName(configName) // .yaml is implicit v.SetEnvPrefix("KO") v.AutomaticEnv() if override := os.Getenv("KO_CONFIG_PATH"); override != "" { file, err := os.Stat(override) if err != nil { return fmt.Errorf("error looking for config file: %w", err) } if file.Mode().IsRegular() { v.SetConfigFile(override) } else if file.IsDir() { path := filepath.Join(override, ".ko.yaml") file, err = os.Stat(path) if err != nil { return fmt.Errorf("error looking for config file: %w", err) } if file.Mode().IsRegular() { v.SetConfigFile(path) } else { return fmt.Errorf("config file %s is not a regular file", path) } } else { return fmt.Errorf("config file %s is not a regular file", override) } } v.AddConfigPath(bo.WorkingDirectory) if err := v.ReadInConfig(); err != nil { if !errors.As(err, &viper.ConfigFileNotFoundError{}) { return fmt.Errorf("error reading config file: %w", err) } } if dp := v.GetStringSlice("defaultPlatforms"); len(dp) > 0 { bo.DefaultPlatforms = dp } if env := v.GetStringSlice("defaultEnv"); len(env) > 0 { bo.DefaultEnv = env } if flags := v.GetStringSlice("defaultFlags"); len(flags) > 0 { bo.DefaultFlags = flags } if ldflags := v.GetStringSlice("defaultLdflags"); len(ldflags) > 0 { bo.DefaultLdflags = ldflags } if bo.BaseImage == "" { ref := v.GetString("defaultBaseImage") if _, err := name.ParseReference(ref); err != nil { return fmt.Errorf("'defaultBaseImage': error parsing %q as image reference: %w", ref, err) } bo.BaseImage = ref } if len(bo.BaseImageOverrides) == 0 { baseImageOverrides := map[string]string{} overrides := v.GetStringMapString("baseImageOverrides") for key, value := range overrides { if _, err := name.ParseReference(value); err != nil { return fmt.Errorf("'baseImageOverrides': error parsing %q as image reference: %w", value, err) } baseImageOverrides[key] = value } bo.BaseImageOverrides = baseImageOverrides } if len(bo.BuildConfigs) == 0 { var builds []build.Config useYAMLTagsAndUnmarshallers := func(c *mapstructure.DecoderConfig) { c.TagName = "yaml" // defaults to `mapstructure:""` c.DecodeHook = yamlUnmarshallerHookFunc } if err := v.UnmarshalKey("builds", &builds, useYAMLTagsAndUnmarshallers); err != nil { return fmt.Errorf("configuration section 'builds' cannot be parsed: %w", err) } buildConfigs, err := createBuildConfigMap(bo.WorkingDirectory, builds) if err != nil { return fmt.Errorf("could not create build config map: %w", err) } bo.BuildConfigs = buildConfigs } return nil } func yamlUnmarshallerHookFunc(_ reflect.Type, to reflect.Type, data any) (any, error) { type yamlUnmarshaller interface { UnmarshalYAML(func(any) error) error } result := reflect.New(to).Interface() unmarshaller, ok := result.(yamlUnmarshaller) if !ok { return data, nil } if err := unmarshaller.UnmarshalYAML(func(target any) error { dest := reflect.Indirect(reflect.ValueOf(target)) src := reflect.ValueOf(data) if dest.CanSet() && src.Type().AssignableTo(dest.Type()) { dest.Set(src) return nil } return fmt.Errorf("want %v, got %v", dest.Type(), src.Type()) }); err != nil { // We do not implement []string <- []any above, therefore YAML // unmarshaller could fail given perfectly valid input. Return // data AS IS, allowing mapstructure's logic to perform the // conversion. return data, nil } return result, nil } func createBuildConfigMap(workingDirectory string, configs []build.Config) (map[string]build.Config, error) { buildConfigsByImportPath := make(map[string]build.Config) for i, config := range configs { // In case no ID is specified, use the index of the build config in // the ko YAML file as a reference (debug help). if config.ID == "" { config.ID = fmt.Sprintf("#%d", i) } // Make sure to behave like GoReleaser by defaulting to the current // directory in case the build or main field is not set, check // https://goreleaser.com/customization/build/ for details if config.Dir == "" { config.Dir = "." } if config.Main == "" { config.Main = "." } // baseDir is the directory where `go list` will be run to look for package information baseDir := filepath.Join(workingDirectory, config.Dir) // To behave like GoReleaser, check whether the configured `main` config value points to a // source file, and if so, just use the directory it is in path := config.Main if fi, err := os.Stat(filepath.Join(baseDir, config.Main)); err == nil && fi.Mode().IsRegular() { path = filepath.Dir(config.Main) } // Verify that the path actually leads to a local file (https://github.com/google/ko/issues/483) if _, err := os.Stat(filepath.Join(baseDir, path)); err != nil { return nil, err } // By default, paths configured in the builds section are considered // local import paths, therefore add a "./" equivalent as a prefix to // the constructured import path localImportPath := fmt.Sprint(".", string(filepath.Separator), path) dir := filepath.Clean(baseDir) if dir == "." { dir = "" } pkgs, err := packages.Load(&packages.Config{Mode: packages.NeedName, Dir: dir}, localImportPath) if err != nil { return nil, fmt.Errorf("'builds': entry #%d does not contain a valid local import path (%s) for directory (%s): %w", i, localImportPath, baseDir, err) } if len(pkgs) != 1 { return nil, fmt.Errorf("'builds': entry #%d results in %d local packages, only 1 is expected", i, len(pkgs)) } importPath := pkgs[0].PkgPath buildConfigsByImportPath[importPath] = config } return buildConfigsByImportPath, nil } ================================================ FILE: pkg/commands/options/build_test.go ================================================ // Copyright 2021 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package options import ( "os" "reflect" "strings" "testing" "github.com/google/ko/pkg/build" "github.com/spf13/cobra" "github.com/stretchr/testify/require" ) func TestDefaultBaseImage(t *testing.T) { bo := &BuildOptions{ WorkingDirectory: "testdata/config", } err := bo.LoadConfig() if err != nil { t.Fatal(err) } wantDefaultBaseImage := "alpine" // matches value in ./testdata/config/.ko.yaml if bo.BaseImage != wantDefaultBaseImage { t.Fatalf("wanted BaseImage %s, got %s", wantDefaultBaseImage, bo.BaseImage) } } func TestDefaultPlatformsAll(t *testing.T) { allBo := &BuildOptions{ WorkingDirectory: "testdata/config", } err := allBo.LoadConfig() if err != nil { t.Fatal(err) } wantDefaultPlatformsAll := []string{"all"} // matches value in ./testdata/config/.ko.yaml if !reflect.DeepEqual(allBo.DefaultPlatforms, wantDefaultPlatformsAll) { t.Fatalf("wanted DefaultPlatforms %s, got %s", wantDefaultPlatformsAll, allBo.DefaultPlatforms) } multipleBo := &BuildOptions{ WorkingDirectory: "testdata/multiple-platforms", } err = multipleBo.LoadConfig() if err != nil { t.Fatal(err) } wantDefaultPlatformsMultiple := []string{"linux/arm64", "linux/amd64"} // matches value in ./testdata/multiple-platforms/.ko.yaml if !reflect.DeepEqual(multipleBo.DefaultPlatforms, wantDefaultPlatformsMultiple) { t.Fatalf("wanted DefaultPlatforms %s, got %s", wantDefaultPlatformsMultiple, multipleBo.DefaultPlatforms) } } func TestDefaultEnv(t *testing.T) { bo := &BuildOptions{ WorkingDirectory: "testdata/config", } err := bo.LoadConfig() require.NoError(t, err) require.Equal(t, []string{"FOO=bar"}, bo.DefaultEnv) } func TestDefaultFlags(t *testing.T) { bo := &BuildOptions{ WorkingDirectory: "testdata/config", } err := bo.LoadConfig() require.NoError(t, err) require.Equal(t, []string{"-tags", "netgo"}, bo.DefaultFlags) } func TestDefaultLdFlags(t *testing.T) { bo := &BuildOptions{ WorkingDirectory: "testdata/config", } err := bo.LoadConfig() require.NoError(t, err) require.Equal(t, []string{"-s -w"}, bo.DefaultLdflags) } func TestBuildConfigWithWorkingDirectoryAndDirAndMain(t *testing.T) { bo := &BuildOptions{ WorkingDirectory: "testdata/paths", } err := bo.LoadConfig() if err != nil { t.Fatalf("NewBuilder(): %v", err) } if len(bo.BuildConfigs) != 1 { t.Fatalf("expected 1 build config, got %d", len(bo.BuildConfigs)) } expectedImportPath := "example.com/testapp/cmd/foo" // module from app/go.mod + `main` from .ko.yaml if _, exists := bo.BuildConfigs[expectedImportPath]; !exists { t.Fatalf("expected build config for import path [%s], got %+v", expectedImportPath, bo.BuildConfigs) } } func TestCreateBuildConfigs(t *testing.T) { compare := func(expected string, actual string) { if expected != actual { t.Errorf("test case failed: expected '%#v', but actual value is '%#v'", expected, actual) } } buildConfigs := []build.Config{ {ID: "defaults"}, {ID: "OnlyMain", Main: "test"}, {ID: "OnlyMainWithFile", Main: "test/main.go"}, {ID: "OnlyDir", Dir: "test"}, {ID: "DirAndMain", Dir: "test", Main: "main.go"}, } for _, b := range buildConfigs { buildConfigMap, err := createBuildConfigMap("../../..", []build.Config{b}) if err != nil { t.Fatal(err) } for importPath, buildCfg := range buildConfigMap { switch buildCfg.ID { case "defaults": compare("github.com/google/ko", importPath) case "OnlyMain", "OnlyMainWithFile", "OnlyDir", "DirAndMain": compare("github.com/google/ko/test", importPath) default: t.Fatalf("unknown test case: %s", buildCfg.ID) } } } } func TestAddBuildOptionsSetsDefaultsForNonFlagOptions(t *testing.T) { cmd := &cobra.Command{} bo := &BuildOptions{} AddBuildOptions(cmd, bo) if !bo.Trimpath { t.Error("expected Trimpath=true") } } func TestOverrideConfigPath(t *testing.T) { const envName = "KO_CONFIG_PATH" bo := &BuildOptions{} for _, tc := range []struct { name string koConfigPath string err string }{{ name: ".ko.yaml does not exist", koConfigPath: "testdata", err: "testdata/.ko.yaml: no such file or directory", }, { name: "config path does not contain .ko.yaml", koConfigPath: "testdata/bad-config", err: "testdata/bad-config/.ko.yaml is not a regular file", }, { name: "config path is a directory containing a .ko.yaml", koConfigPath: "testdata/config", }, { name: "config path points to a file", koConfigPath: "testdata/config/my-ko.yaml", }} { t.Run(tc.name, func(t *testing.T) { oldEnv := os.Getenv(envName) defer os.Setenv(envName, oldEnv) os.Setenv(envName, tc.koConfigPath) err := bo.LoadConfig() if err == nil { if tc.err != "" { t.Fatalf("expected error %q, saw nil", tc.err) } } else { if tc.err == "" { t.Errorf("expected no error, saw: %v", err) } if !strings.Contains(err.Error(), tc.err) { t.Errorf("expected error to contain %q, saw: %v", tc.err, err) } } }) } } ================================================ FILE: pkg/commands/options/filestuff.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package options import ( "log" "os" "path/filepath" "github.com/spf13/cobra" ) // FilenameOptions is from pkg/kubectl. type FilenameOptions struct { Filenames []string Recursive bool } func AddFileArg(cmd *cobra.Command, fo *FilenameOptions) { // From pkg/kubectl cmd.Flags().StringSliceVarP(&fo.Filenames, "filename", "f", fo.Filenames, "Filename, directory, or URL to files to use to create the resource") cmd.Flags().BoolVarP(&fo.Recursive, "recursive", "R", fo.Recursive, "Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory.") } // Based heavily on pkg/kubectl func EnumerateFiles(fo *FilenameOptions) chan string { files := make(chan string) go func() { // When we're done enumerating files, close the channel defer close(files) for _, paths := range fo.Filenames { // Just pass through '-' as it is indicative of stdin. if paths == "-" { files <- paths continue } // For each of the "filenames" we are passed (file or directory) start a // "Walk" to enumerate all of the contained files recursively. err := filepath.Walk(paths, func(path string, fi os.FileInfo, err error) error { if err != nil { return err } // If this is a directory, skip it if it isn't the current directory we are // processing (unless we are in recursive mode). If we decide to process // the directory, and we're in watch mode, then we set up a watch on the // directory. if fi.IsDir() { if path != paths && !fo.Recursive { return filepath.SkipDir } // We don't stream back directories, we just decide to skip them, or not. return nil } // Don't check extension if the filepath was passed explicitly if path != paths { switch filepath.Ext(path) { case ".json", ".yaml": // Process these. default: return nil } } files <- path return nil }) if err != nil { log.Fatalf("Error enumerating files: %v", err) } } }() return files } ================================================ FILE: pkg/commands/options/namer_test.go ================================================ // Copyright 2021 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package options_test import ( "path" "testing" "github.com/google/ko/pkg/commands/options" ) func TestMakeNamer(t *testing.T) { foreachTestCaseMakeNamer(func(tc testMakeNamerCase) { t.Run(tc.name, func(t *testing.T) { namer := options.MakeNamer(&tc.opts) got := namer("registry.example.org/foo/bar", "example.org/sample/cmd/example") if got != tc.want { t.Errorf("got image name %s, wanted %s", got, tc.want) } }) }) } func foreachTestCaseMakeNamer(fn func(tc testMakeNamerCase)) { for _, namerCase := range testMakeNamerCases() { fn(namerCase) } } func testMakeNamerCases() []testMakeNamerCase { return []testMakeNamerCase{{ name: "defaults", want: "registry.example.org/foo/bar/example-51d74b7127c5f7495a338df33ecdeb19", }, { name: "with preserve import paths", want: "registry.example.org/foo/bar/example.org/sample/cmd/example", opts: options.PublishOptions{PreserveImportPaths: true}, }, { name: "with base import paths", want: "registry.example.org/foo/bar/example", opts: options.PublishOptions{BaseImportPaths: true}, }, { name: "with bare", want: "registry.example.org/foo/bar", opts: options.PublishOptions{Bare: true}, }, { name: "with custom namer", want: "registry.example.org/foo/bar-example", opts: options.PublishOptions{ImageNamer: func(base string, importpath string) string { return base + "-" + path.Base(importpath) }}, }} } type testMakeNamerCase struct { name string opts options.PublishOptions want string } ================================================ FILE: pkg/commands/options/publish.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package options import ( "crypto/md5" // nolint: gosec // No strong cryptography needed. "encoding/hex" "os" "path" "github.com/google/go-containerregistry/pkg/v1/daemon" "github.com/google/ko/pkg/publish" "github.com/spf13/cobra" ) // PublishOptions encapsulates options when publishing. type PublishOptions struct { // DockerRepo configures the destination image repository. // In normal ko usage, this is populated with the value of $KO_DOCKER_REPO. DockerRepo string // LocalDomain overrides the default domain for images loaded into the local Docker daemon. Use with Local=true. LocalDomain string // UserAgent enables overriding the default value of the `User-Agent` HTTP // request header used when pushing the built image to an image registry. UserAgent string // DockerClient enables overriding the default docker client when embedding // ko as a module in other tools. // If left as the zero value, ko uses github.com/docker/docker/client.FromEnv DockerClient daemon.Client Tags []string // TagOnly resolves images into tag-only references. TagOnly bool // Push publishes images to a registry. Push bool // Local publishes images to a local docker daemon. Local bool InsecureRegistry bool OCILayoutPath string TarballFile string ImageRefsFile string // PreserveImportPaths preserves the full import path after KO_DOCKER_REPO. PreserveImportPaths bool // BaseImportPaths uses the base path without MD5 hash after KO_DOCKER_REPO. BaseImportPaths bool // Bare uses a tag on the KO_DOCKER_REPO without anything additional. Bare bool // ImageNamer can be used to pass a custom image name function. When given // PreserveImportPaths, BaseImportPaths, Bare has no effect. ImageNamer publish.Namer Jobs int } func AddPublishArg(cmd *cobra.Command, po *PublishOptions) { // Set DockerRepo from the KO_DOCKER_REPO envionment variable. // See https://github.com/google/ko/pull/351 for flag discussion. if dockerRepo, exists := os.LookupEnv("KO_DOCKER_REPO"); exists { po.DockerRepo = dockerRepo } cmd.Flags().StringSliceVarP(&po.Tags, "tags", "t", []string{"latest"}, "Which tags to use for the produced image instead of the default 'latest' tag "+ "(may not work properly with --base-import-paths or --bare).") cmd.Flags().BoolVar(&po.TagOnly, "tag-only", false, "Include tags but not digests in resolved image references. Useful when digests are not preserved when images are repopulated.") cmd.Flags().BoolVar(&po.Push, "push", true, "Push images to KO_DOCKER_REPO") cmd.Flags().BoolVarP(&po.Local, "local", "L", po.Local, "Load into images to local docker daemon.") cmd.Flags().BoolVar(&po.InsecureRegistry, "insecure-registry", po.InsecureRegistry, "Whether to skip TLS verification on the registry") cmd.Flags().StringVar(&po.OCILayoutPath, "oci-layout-path", "", "Path to save the OCI image layout of the built images") cmd.Flags().StringVar(&po.TarballFile, "tarball", "", "File to save images tarballs") cmd.Flags().StringVar(&po.ImageRefsFile, "image-refs", "", "Path to file where a list of the published image references will be written.") cmd.Flags().BoolVarP(&po.PreserveImportPaths, "preserve-import-paths", "P", po.PreserveImportPaths, "Whether to preserve the full import path after KO_DOCKER_REPO.") cmd.Flags().BoolVarP(&po.BaseImportPaths, "base-import-paths", "B", po.BaseImportPaths, "Whether to use the base path without MD5 hash after KO_DOCKER_REPO (may not work properly with --tags).") cmd.Flags().BoolVar(&po.Bare, "bare", po.Bare, "Whether to just use KO_DOCKER_REPO without additional context (may not work properly with --tags).") } func packageWithMD5(base, importpath string) string { hasher := md5.New() // nolint: gosec // No strong cryptography needed. hasher.Write([]byte(importpath)) return path.Join(base, path.Base(importpath)+"-"+hex.EncodeToString(hasher.Sum(nil))) } func preserveImportPath(base, importpath string) string { return path.Join(base, importpath) } func baseImportPaths(base, importpath string) string { return path.Join(base, path.Base(importpath)) } func bareDockerRepo(base, _ string) string { return base } func MakeNamer(po *PublishOptions) publish.Namer { if po.ImageNamer != nil { return po.ImageNamer } else if po.PreserveImportPaths { return preserveImportPath } else if po.BaseImportPaths { return baseImportPaths } else if po.Bare { return bareDockerRepo } return packageWithMD5 } ================================================ FILE: pkg/commands/options/selector.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package options import ( "github.com/spf13/cobra" ) // SelectorOptions allows selecting objects from the input manifests by label type SelectorOptions struct { Selector string } func AddSelectorArg(cmd *cobra.Command, so *SelectorOptions) { cmd.Flags().StringVarP(&so.Selector, "selector", "l", "", "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)") } ================================================ FILE: pkg/commands/options/testdata/bad-config/.ko.yaml/.gitignore ================================================ .# We just want to have a practically empty directory. * !.gitignore ================================================ FILE: pkg/commands/options/testdata/config/.ko.yaml ================================================ defaultBaseImage: alpine defaultPlatforms: all defaultEnv: FOO=bar defaultFlags: - -tags - netgo defaultLdflags: - -s -w ================================================ FILE: pkg/commands/options/testdata/config/my-ko.yaml ================================================ defaultBaseImage: wow ================================================ FILE: pkg/commands/options/testdata/multiple-platforms/.ko.yaml ================================================ defaultBaseImage: alpine defaultPlatforms: - linux/arm64 - linux/amd64 ================================================ FILE: pkg/commands/options/testdata/paths/.ko.yaml ================================================ builds: - id: app-with-main-package-in-different-directory-to-go-mod-and-ko-yaml dir: ./app main: ./cmd/foo ================================================ FILE: pkg/commands/options/testdata/paths/app/cmd/foo/main.go ================================================ // Copyright 2021 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import "fmt" func main() { fmt.Println("cmd/foo") } ================================================ FILE: pkg/commands/options/testdata/paths/app/go.mod ================================================ module example.com/testapp go 1.15 ================================================ FILE: pkg/commands/options/validate.go ================================================ // Copyright 2022 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package options import ( "errors" "log" "slices" "strings" ) const bareBaseFlagsWarning = `WARNING! ----------------------------------------------------------------- Both --base-import-paths and --bare were set. --base-import-paths will take precedence and ignore --bare flag. In a future release this will be an error. ----------------------------------------------------------------- ` const localFlagsWarning = `WARNING! ----------------------------------------------------------------- The --local flag is set and KO_DOCKER_REPO is set to ko.local You can choose either one to build a local image. The --local flag might be deprecated in the future. ----------------------------------------------------------------- ` func Validate(po *PublishOptions, bo *BuildOptions) error { po.Jobs = bo.ConcurrentBuilds if po.Bare && po.BaseImportPaths { log.Print(bareBaseFlagsWarning) // TODO: return error when we decided to make this an error, for now it is a warning } if po.Local && strings.Contains(po.DockerRepo, "ko.local") { log.Print(localFlagsWarning) } if len(bo.Platforms) > 1 { if slices.Contains(bo.Platforms, "all") { return errors.New("all or specific platforms should be used") } } return nil } ================================================ FILE: pkg/commands/publisher.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package commands import ( "context" "fmt" "github.com/google/go-containerregistry/pkg/name" "github.com/google/ko/pkg/build" "github.com/google/ko/pkg/publish" ) // PublishImages publishes images func PublishImages(ctx context.Context, importpaths []string, pub publish.Interface, b build.Interface) (map[string]name.Reference, error) { return publishImages(ctx, importpaths, pub, b) } func publishImages(ctx context.Context, importpaths []string, pub publish.Interface, b build.Interface) (map[string]name.Reference, error) { imgs := make(map[string]name.Reference) for _, importpath := range importpaths { importpath, err := b.QualifyImport(importpath) if err != nil { return nil, err } if err := b.IsSupportedReference(importpath); err != nil { return nil, fmt.Errorf("importpath %q is not supported: %w", importpath, err) } img, err := b.Build(ctx, importpath) if err != nil { return nil, fmt.Errorf("error building %q: %w", importpath, err) } ref, err := pub.Publish(ctx, img, importpath) if err != nil { return nil, fmt.Errorf("error publishing %s: %w", importpath, err) } imgs[importpath] = ref } return imgs, nil } ================================================ FILE: pkg/commands/publisher_test.go ================================================ // Copyright 2021 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package commands import ( "context" "fmt" "path/filepath" "runtime" "strings" "testing" "github.com/google/ko/pkg/build" "github.com/google/ko/pkg/commands/options" ) func TestPublishImages(t *testing.T) { namespace := "base" s, err := registryServerWithImage(namespace) if err != nil { t.Fatalf("could not create test registry server: %v", err) } repo := s.Listener.Addr().String() baseImage := fmt.Sprintf("%s/%s", repo, namespace) sampleAppDir, err := sampleAppRelDir() if err != nil { t.Fatalf("sampleAppRelDir(): %v", err) } tests := []struct { description string publishArg string importpath string }{ { description: "import path with ko scheme", publishArg: "ko://github.com/google/ko/test", importpath: "github.com/google/ko/test", }, { description: "import path without ko scheme", publishArg: "github.com/google/ko/test", importpath: "github.com/google/ko/test", }, { description: "file path", publishArg: sampleAppDir, importpath: "github.com/google/ko/test", }, } for _, test := range tests { ctx := context.Background() bo := &options.BuildOptions{ BaseImage: baseImage, ConcurrentBuilds: 1, Platforms: []string{"all"}, } builder, err := NewBuilder(ctx, bo) if err != nil { t.Fatalf("%s: MakeBuilder(): %v", test.description, err) } po := &options.PublishOptions{ DockerRepo: repo, PreserveImportPaths: true, } publisher, err := NewPublisher(po) if err != nil { t.Fatalf("%s: MakePublisher(): %v", test.description, err) } importpathWithScheme := build.StrictScheme + test.importpath refs, err := PublishImages(ctx, []string{test.publishArg}, publisher, builder) if err != nil { t.Fatalf("%s: PublishImages(): %v", test.description, err) } ref, exists := refs[importpathWithScheme] if !exists { t.Errorf("%s: could not find image for importpath %s", test.description, importpathWithScheme) } gotImageName := ref.Context().Name() wantImageName := strings.ToLower(fmt.Sprintf("%s/%s", repo, test.importpath)) if gotImageName != wantImageName { t.Errorf("%s: got %s, wanted %s", test.description, gotImageName, wantImageName) } } } func sampleAppRelDir() (string, error) { _, filename, _, ok := runtime.Caller(0) if !ok { return "", fmt.Errorf("could not get current filename") } basepath := filepath.Dir(filename) testAppDir := filepath.Join(basepath, "..", "..", "test") return filepath.Rel(basepath, testAppDir) } ================================================ FILE: pkg/commands/resolve.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package commands import ( "fmt" "os" "github.com/google/ko/pkg/commands/options" "github.com/spf13/cobra" ) // addResolve augments our CLI surface with resolve. func addResolve(topLevel *cobra.Command) { po := &options.PublishOptions{} fo := &options.FilenameOptions{} so := &options.SelectorOptions{} bo := &options.BuildOptions{} resolve := &cobra.Command{ Use: "resolve -f FILENAME", Short: "Print the input files with image references resolved to built/pushed image digests.", Long: `This sub-command finds import path references within the provided files, builds them into Go binaries, containerizes them, publishes them, and prints the resulting yaml.`, Example: ` # Build and publish import path references to a Docker # Registry as: # ${KO_DOCKER_REPO}/- # When KO_DOCKER_REPO is ko.local, it is the same as if # --local and --preserve-import-paths were passed. ko resolve -f config/ # Build and publish import path references to a Docker # Registry preserving import path names as: # ${KO_DOCKER_REPO}/ # When KO_DOCKER_REPO is ko.local, it is the same as if # --local was passed. ko resolve --preserve-import-paths -f config/ # Build and publish import path references to a Docker # daemon as: # ko.local/ # This always preserves import paths. ko resolve --local -f config/`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { if err := options.Validate(po, bo); err != nil { return fmt.Errorf("validating options: %w", err) } ctx := cmd.Context() bo.InsecureRegistry = po.InsecureRegistry builder, err := makeBuilder(ctx, bo) if err != nil { return fmt.Errorf("error creating builder: %w", err) } publisher, err := makePublisher(po) if err != nil { return fmt.Errorf("error creating publisher: %w", err) } defer publisher.Close() return ResolveFilesToWriter(ctx, builder, publisher, fo, so, os.Stdout) }, } options.AddPublishArg(resolve, po) options.AddFileArg(resolve, fo) options.AddSelectorArg(resolve, so) options.AddBuildOptions(resolve, bo) topLevel.AddCommand(resolve) } ================================================ FILE: pkg/commands/resolver.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package commands import ( "bytes" "context" "errors" "fmt" "io" "os" "path" "strings" "sync" "github.com/google/go-containerregistry/pkg/name" "go.yaml.in/yaml/v4" "golang.org/x/sync/errgroup" "k8s.io/apimachinery/pkg/labels" "github.com/google/ko/pkg/build" "github.com/google/ko/pkg/commands/options" "github.com/google/ko/pkg/publish" "github.com/google/ko/pkg/resolve" ) // ua returns the ko user agent. func ua() string { if v := version(); v != "" { return "ko/" + v } return "ko" } func gobuildOptions(bo *options.BuildOptions) ([]build.Option, error) { creationTime, err := getCreationTime() if err != nil { return nil, err } kodataCreationTime, err := getKoDataCreationTime() if err != nil { return nil, err } if len(bo.Platforms) == 0 && len(bo.DefaultPlatforms) > 0 { bo.Platforms = bo.DefaultPlatforms } if len(bo.Platforms) == 0 { envPlatform := "linux/amd64" goos, goarch, goarm := os.Getenv("GOOS"), os.Getenv("GOARCH"), os.Getenv("GOARM") // Default to linux/amd64 unless GOOS and GOARCH are set. if goos != "" && goarch != "" { envPlatform = path.Join(goos, goarch) } // Use GOARM for variant if it's set and GOARCH is arm. if strings.Contains(goarch, "arm") && goarm != "" { envPlatform = path.Join(envPlatform, "v"+goarm) } bo.Platforms = []string{envPlatform} } else { // Make sure these are all unset for _, env := range []string{"GOOS", "GOARCH", "GOARM"} { if s, ok := os.LookupEnv(env); ok { return nil, fmt.Errorf("cannot use --platform or defaultPlatforms in .ko.yaml or env KO_DEFAULTPLATFORMS combined with %s=%q", env, s) } } } opts := []build.Option{ build.WithBaseImages(getBaseImage(bo)), build.WithDefaultEnv(bo.DefaultEnv), build.WithDefaultFlags(bo.DefaultFlags), build.WithDefaultLdflags(bo.DefaultLdflags), build.WithPlatforms(bo.Platforms...), build.WithJobs(bo.ConcurrentBuilds), } if creationTime != nil { opts = append(opts, build.WithCreationTime(*creationTime)) } if kodataCreationTime != nil { opts = append(opts, build.WithKoDataCreationTime(*kodataCreationTime)) } if bo.DisableOptimizations { opts = append(opts, build.WithDisabledOptimizations()) } if bo.Debug { opts = append(opts, build.WithDebugger()) opts = append(opts, build.WithDisabledOptimizations()) // also needed for Delve } switch bo.SBOM { case "none": opts = append(opts, build.WithDisabledSBOM()) default: // "spdx" opts = append(opts, build.WithSPDX(version())) } opts = append(opts, build.WithTrimpath(bo.Trimpath)) for _, lf := range bo.Labels { parts := strings.SplitN(lf, "=", 2) if len(parts) != 2 { return nil, fmt.Errorf("invalid label flag: %s", lf) } opts = append(opts, build.WithLabel(parts[0], parts[1])) } for _, an := range bo.Annotations { k, v, ok := strings.Cut(an, "=") if !ok { return nil, fmt.Errorf("missing '=' in annotation: %s", an) } opts = append(opts, build.WithAnnotation(k, v)) } if bo.User != "" { opts = append(opts, build.WithUser(bo.User)) } if bo.BuildConfigs != nil { opts = append(opts, build.WithConfig(bo.BuildConfigs)) } if bo.SBOMDir != "" { opts = append(opts, build.WithSBOMDir(bo.SBOMDir)) } return opts, nil } // NewBuilder creates a ko builder func NewBuilder(ctx context.Context, bo *options.BuildOptions) (build.Interface, error) { return makeBuilder(ctx, bo) } func makeBuilder(ctx context.Context, bo *options.BuildOptions) (*build.Caching, error) { if err := bo.LoadConfig(); err != nil { return nil, err } opt, err := gobuildOptions(bo) if err != nil { return nil, fmt.Errorf("error setting up builder options: %w", err) } innerBuilder, err := build.NewGobuilds(ctx, bo.WorkingDirectory, bo.BuildConfigs, opt...) if err != nil { return nil, err } // tl;dr Wrap builder in a caching builder. // // The caching builder should on Build calls: // - Check for a valid Build future // - if a valid Build future exists at the time of the request, // then block on it. // - if it does not, then initiate and record a Build future. // // This will benefit the following key cases: // 1. When the same import path is referenced across multiple yaml files // we can elide subsequent builds by blocking on the same image future. // 2. When an affected yaml file has multiple import paths (mostly unaffected) // we can elide the builds of unchanged import paths. return build.NewCaching(innerBuilder) } // NewPublisher creates a ko publisher func NewPublisher(po *options.PublishOptions) (publish.Interface, error) { return makePublisher(po) } func makePublisher(po *options.PublishOptions) (publish.Interface, error) { // use each tag only once po.Tags = unique(po.Tags) // Create the publish.Interface that we will use to publish image references // to either a docker daemon or a container image registry. innerPublisher, err := func() (publish.Interface, error) { repoName := po.DockerRepo namer := options.MakeNamer(po) // Default LocalDomain if unset. if po.LocalDomain == "" { po.LocalDomain = publish.LocalDomain } // If repoName is unset with --local, default it to the local domain. if po.Local && repoName == "" { repoName = po.LocalDomain } // When in doubt, if repoName is under the local domain, default to --local. po.Local = po.Local || strings.HasPrefix(repoName, po.LocalDomain) if po.Local { // TODO(jonjohnsonjr): I'm assuming that nobody will // use local with other publishers, but that might // not be true. po.LocalDomain = repoName return publish.NewDaemon(namer, po.Tags, publish.WithDockerClient(po.DockerClient), publish.WithLocalDomain(po.LocalDomain), ) } if strings.HasPrefix(repoName, publish.KindDomain) { return publish.NewKindPublisher(repoName, namer, po.Tags), nil } if repoName == "" && po.Push { return nil, errors.New("KO_DOCKER_REPO environment variable is unset") } if _, err := name.NewRegistry(repoName); err != nil { if _, err := name.NewRepository(repoName); err != nil { return nil, fmt.Errorf("failed to parse %q as repository: %w", repoName, err) } } publishers := []publish.Interface{} if po.OCILayoutPath != "" { lp := publish.NewLayout(po.OCILayoutPath) publishers = append(publishers, lp) } if po.TarballFile != "" { tp := publish.NewTarball(po.TarballFile, repoName, namer, po.Tags) publishers = append(publishers, tp) } userAgent := ua() if po.UserAgent != "" { userAgent = po.UserAgent } if po.Push { dp, err := publish.NewDefault(repoName, publish.WithUserAgent(userAgent), publish.WithAuthFromKeychain(keychain), publish.WithNamer(namer), publish.WithTags(po.Tags), publish.WithTagOnly(po.TagOnly), publish.Insecure(po.InsecureRegistry), publish.WithJobs(po.Jobs), ) if err != nil { return nil, err } publishers = append(publishers, dp) } // If not publishing, at least generate a digest to simulate // publishing. if len(publishers) == 0 { // If one or more tags are specified, use the first tag in the list var tag string if len(po.Tags) >= 1 { tag = po.Tags[0] } publishers = append(publishers, nopPublisher{ repoName: repoName, namer: namer, tag: tag, tagOnly: po.TagOnly, }) } return publish.MultiPublisher(publishers...), nil }() if err != nil { return nil, err } if po.ImageRefsFile != "" { innerPublisher, err = publish.NewRecorder(innerPublisher, po.ImageRefsFile) if err != nil { return nil, err } } // Wrap publisher in a memoizing publisher implementation. return publish.NewCaching(innerPublisher) } // nopPublisher simulates publishing without actually publishing anything, to // provide fallback behavior when the user configures no push destinations. type nopPublisher struct { repoName string namer publish.Namer tag string tagOnly bool } func (n nopPublisher) Publish(_ context.Context, br build.Result, s string) (name.Reference, error) { s = strings.TrimPrefix(s, build.StrictScheme) nm := n.namer(n.repoName, s) if n.tagOnly { if n.tag == "" { return nil, errors.New("must specify tag if requesting tag only") } return name.NewTag(fmt.Sprintf("%s:%s", nm, n.tag)) } h, err := br.Digest() if err != nil { return nil, err } if n.tag == "" { return name.NewDigest(fmt.Sprintf("%s@%s", nm, h)) } return name.NewDigest(fmt.Sprintf("%s:%s@%s", nm, n.tag, h)) } func (n nopPublisher) Close() error { return nil } // resolvedFuture represents a "future" for the bytes of a resolved file. type resolvedFuture chan []byte func ResolveFilesToWriter( ctx context.Context, builder *build.Caching, publisher publish.Interface, fo *options.FilenameOptions, so *options.SelectorOptions, out io.WriteCloser) error { defer out.Close() // By having this as a channel, we can hook this up to a filesystem // watcher and leave `fs` open to stream the names of yaml files // affected by code changes (including the modification of existing or // creation of new yaml files). fs := options.EnumerateFiles(fo) // This tracks filename -> []importpath var sm sync.Map // This tracks resolution errors and ensures we cancel other builds if an // individual build fails. errs, ctx := errgroup.WithContext(ctx) var futures []resolvedFuture for { // Each iteration, if there is anything in the list of futures, // listen to it in addition to the file enumerating channel. // A nil channel is never available to receive on, so if nothing // is available, this will result in us exclusively selecting // on the file enumerating channel. var bf resolvedFuture if len(futures) > 0 { bf = futures[0] } else if fs == nil { // There are no more files to enumerate and the futures // have been drained, so quit. break } select { case file, ok := <-fs: if !ok { // a nil channel is never available to receive on. // This allows us to drain the list of in-process // futures without this case of the select winning // each time. fs = nil break } // Make a new future to use to ship the bytes back and append // it to the list of futures (see comment below about ordering). ch := make(resolvedFuture) futures = append(futures, ch) // Kick off the resolution that will respond with its bytes on // the future. f := file // defensive copy errs.Go(func() error { defer close(ch) // Record the builds we do via this builder. recordingBuilder := &build.Recorder{ Builder: builder, } b, err := resolveFile(ctx, f, recordingBuilder, publisher, so) if err != nil { // This error is sometimes expected during watch mode, so this // isn't fatal. Just print it and keep the watch open. return fmt.Errorf("error processing import paths in %q: %w", f, err) } // Associate with this file the collection of binary import paths. sm.Store(f, recordingBuilder.ImportPaths) ch <- b return nil }) case b, ok := <-bf: // Once the head channel returns something, dequeue it. // We listen to the futures in order to be respectful of // the kubectl apply ordering, which matters! futures = futures[1:] if ok { // Write the next body and a trailing delimiter. // We write the delimiter LAST so that when streamed to // kubectl it knows that the resource is complete and may // be applied. out.Write(append(b, []byte("\n---\n")...)) } } } // Make sure we exit with an error. // See https://github.com/ko-build/ko/issues/84 return errs.Wait() } func resolveFile( ctx context.Context, f string, builder build.Interface, pub publish.Interface, so *options.SelectorOptions) (b []byte, err error) { var selector labels.Selector if so.Selector != "" { var err error selector, err = labels.Parse(so.Selector) if err != nil { return nil, fmt.Errorf("unable to parse selector: %w", err) } } if f == "-" { b, err = io.ReadAll(os.Stdin) } else { b, err = os.ReadFile(f) } if err != nil { return nil, err } var docNodes []*yaml.Node // The loop is to support multi-document yaml files. // This is handled by using a yaml.Decoder and reading objects until io.EOF, see: // https://godoc.org/go.yaml.in/yaml/v4#Decoder.Decode decoder := yaml.NewDecoder(bytes.NewBuffer(b)) for { var doc yaml.Node if err := decoder.Decode(&doc); err != nil { if errors.Is(err, io.EOF) { break } return nil, err } if selector != nil { if match, err := resolve.MatchesSelector(&doc, selector); err != nil { return nil, fmt.Errorf("error evaluating selector: %w", err) } else if !match { continue } } docNodes = append(docNodes, &doc) } if err := resolve.ImageReferences(ctx, docNodes, builder, pub); err != nil { return nil, fmt.Errorf("error resolving image references: %w", err) } buf := &bytes.Buffer{} e := yaml.NewEncoder(buf) e.SetIndent(2) for _, doc := range docNodes { err := e.Encode(doc) if err != nil { return nil, fmt.Errorf("failed to encode output: %w", err) } } e.Close() return buf.Bytes(), nil } // create a set from the input slice // preserving the order of unique elements func unique(ss []string) []string { var ( seen = make(map[string]struct{}, len(ss)) uniq = make([]string, 0, len(ss)) ) for _, s := range ss { if _, ok := seen[s]; !ok { seen[s] = struct{}{} uniq = append(uniq, s) } } return uniq } ================================================ FILE: pkg/commands/resolver_test.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package commands import ( "bytes" "context" "errors" "fmt" "io" "log" "net/http/httptest" "os" "path" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-containerregistry/pkg/crane" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/registry" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/daemon" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/random" "github.com/google/ko/pkg/build" "github.com/google/ko/pkg/commands/options" kotesting "github.com/google/ko/pkg/internal/testing" "github.com/moby/moby/api/types/image" "github.com/moby/moby/client" "go.yaml.in/yaml/v4" ) var ( fooRef = "github.com/awesomesauce/foo" foo = mustRandom() fooHash = mustDigest(foo) barRef = "github.com/awesomesauce/bar" bar = mustRandom() barHash = mustDigest(bar) testBuilder = kotesting.NewFixedBuild(map[string]build.Result{ fooRef: foo, barRef: bar, }) testHashes = map[string]v1.Hash{ fooRef: fooHash, barRef: barHash, } errImageLoad = fmt.Errorf("ImageLoad() error") errImageTag = fmt.Errorf("ImageTag() error") ) type erroringClient struct { daemon.Client inspectErr error inspectResp image.InspectResponse } func (m *erroringClient) Ping(context.Context, client.PingOptions) (client.PingResult, error) { return client.PingResult{}, nil } func (m *erroringClient) ImageLoad(context.Context, io.Reader, ...client.ImageLoadOption) (client.ImageLoadResult, error) { return io.NopCloser(strings.NewReader("")), errImageLoad } func (m *erroringClient) ImageTag(context.Context, client.ImageTagOptions) (client.ImageTagResult, error) { return client.ImageTagResult{}, errImageTag } func (m *erroringClient) ImageInspect(_ context.Context, _ string, _ ...client.ImageInspectOption) (client.ImageInspectResult, error) { return client.ImageInspectResult{InspectResponse: m.inspectResp}, m.inspectErr } func TestResolveMultiDocumentYAMLs(t *testing.T) { refs := []string{fooRef, barRef} hashes := []v1.Hash{fooHash, barHash} base := mustRepository("gcr.io/multi-pass") buf := bytes.NewBuffer(nil) encoder := yaml.NewEncoder(buf) for _, input := range refs { if err := encoder.Encode(build.StrictScheme + input); err != nil { t.Fatalf("Encode(%v) = %v", input, err) } } inputYAML := buf.Bytes() outYAML, err := resolveFile( context.Background(), yamlToTmpFile(t, buf.Bytes()), testBuilder, kotesting.NewFixedPublish(base, testHashes), &options.SelectorOptions{}) if err != nil { t.Fatalf("ImageReferences(%v) = %v", string(inputYAML), err) } buf = bytes.NewBuffer(outYAML) decoder := yaml.NewDecoder(buf) var outStructured []string for { var output string if err := decoder.Decode(&output); err == nil { outStructured = append(outStructured, output) } else if errors.Is(err, io.EOF) { break } else { t.Errorf("yaml.Unmarshal(%v) = %v", string(outYAML), err) } } expectedStructured := []string{ kotesting.ComputeDigest(base, refs[0], hashes[0]), kotesting.ComputeDigest(base, refs[1], hashes[1]), } if want, got := len(expectedStructured), len(outStructured); want != got { t.Errorf("resolveFile(%v) = %v, want %v", string(inputYAML), got, want) } if diff := cmp.Diff(expectedStructured, outStructured, cmpopts.EquateEmpty()); diff != "" { t.Errorf("resolveFile(%v); (-want +got) = %v", string(inputYAML), diff) } } func TestResolveMultiDocumentYAMLsWithSelector(t *testing.T) { passesSelector := `apiVersion: something/v1 kind: Foo metadata: labels: qux: baz ` failsSelector := `apiVersion: other/v2 kind: Bar ` // Note that this ends in '---', so it in ends in a final null YAML document. inputYAML := fmt.Appendf(nil, "%s---\n%s---", passesSelector, failsSelector) base := mustRepository("gcr.io/multi-pass") outputYAML, err := resolveFile( context.Background(), yamlToTmpFile(t, inputYAML), testBuilder, kotesting.NewFixedPublish(base, testHashes), &options.SelectorOptions{ Selector: "qux=baz", }) if err != nil { t.Fatalf("ImageReferences(%v) = %v", string(inputYAML), err) } if diff := cmp.Diff(passesSelector, string(outputYAML)); diff != "" { t.Errorf("resolveFile (-want +got) = %v", diff) } } func TestNewBuilder(t *testing.T) { namespace := "base" s, err := registryServerWithImage(namespace) if err != nil { t.Fatalf("could not create test registry server: %v", err) } defer s.Close() baseImage := fmt.Sprintf("%s/%s", s.Listener.Addr().String(), namespace) tests := []struct { description string importpath string bo *options.BuildOptions wantQualifiedImportpath string shouldBuildError bool }{ { description: "test app with already qualified import path", importpath: "ko://github.com/google/ko/test", bo: &options.BuildOptions{ BaseImage: baseImage, ConcurrentBuilds: 1, Platforms: []string{"all"}, }, wantQualifiedImportpath: "ko://github.com/google/ko/test", shouldBuildError: false, }, { description: "programmatic build config", importpath: "./test", bo: &options.BuildOptions{ BaseImage: baseImage, BuildConfigs: map[string]build.Config{ "github.com/google/ko/test": { ID: "id-can-be-anything", // no easy way to assert on the output, so trigger error to ensure config is picked up Flags: []string{"-invalid-flag-should-cause-error"}, }, }, ConcurrentBuilds: 1, WorkingDirectory: "../..", }, wantQualifiedImportpath: "ko://github.com/google/ko/test", shouldBuildError: true, }, } for _, test := range tests { t.Run(test.description, func(t *testing.T) { ctx := context.Background() builder, err := NewBuilder(ctx, test.bo) if err != nil { t.Fatalf("NewBuilder(): %v", err) } qualifiedImportpath, err := builder.QualifyImport(test.importpath) if err != nil { t.Fatalf("builder.QualifyImport(%s): %v", test.importpath, err) } if qualifiedImportpath != test.wantQualifiedImportpath { t.Fatalf("incorrect qualified import path, got %s, wanted %s", qualifiedImportpath, test.wantQualifiedImportpath) } _, err = builder.Build(ctx, qualifiedImportpath) if err != nil && !test.shouldBuildError { t.Fatalf("builder.Build(): %v", err) } if err == nil && test.shouldBuildError { t.Fatalf("expected error got nil") } }) } } func TestNewPublisherCanPublish(t *testing.T) { dockerRepo := "registry.example.com/repo" localDomain := "localdomain.example.com/repo" importpath := "github.com/google/ko/test" tests := []struct { description string wantImageName string po *options.PublishOptions shouldError bool wantError error }{ { description: "base import path", wantImageName: fmt.Sprintf("%s/%s", dockerRepo, path.Base(importpath)), po: &options.PublishOptions{ BaseImportPaths: true, DockerRepo: dockerRepo, }, }, { description: "preserve import path", wantImageName: fmt.Sprintf("%s/%s", dockerRepo, importpath), po: &options.PublishOptions{ DockerRepo: dockerRepo, PreserveImportPaths: true, }, }, { description: "override LocalDomain", wantImageName: fmt.Sprintf("%s/%s", localDomain, importpath), po: &options.PublishOptions{ Local: true, LocalDomain: localDomain, PreserveImportPaths: true, DockerClient: &kotesting.MockDaemon{}, }, }, { description: "override DockerClient", wantImageName: strings.ToLower(fmt.Sprintf("%s/%s", localDomain, importpath)), po: &options.PublishOptions{ DockerClient: &erroringClient{}, Local: true, }, shouldError: true, wantError: errImageTag, }, { description: "bare with local domain and repo", wantImageName: strings.ToLower(fmt.Sprintf("%s/foo", dockerRepo)), po: &options.PublishOptions{ DockerRepo: dockerRepo + "/foo", Local: true, Bare: true, }, }, } for _, test := range tests { t.Run(test.description, func(t *testing.T) { publisher, err := NewPublisher(test.po) if err != nil { t.Fatalf("NewPublisher(): %v", err) } defer publisher.Close() ref, err := publisher.Publish(context.Background(), empty.Image, build.StrictScheme+importpath) if test.shouldError { if err == nil || !strings.HasSuffix(err.Error(), test.wantError.Error()) { t.Errorf("%s: got error %v, wanted %v", test.description, err, test.wantError) } return } if err != nil { t.Fatalf("publisher.Publish(): %v", err) } gotImageName := ref.Context().Name() if gotImageName != test.wantImageName { t.Errorf("got %s, wanted %s", gotImageName, test.wantImageName) } }) } } // registryServerWithImage starts a local registry and pushes a random image. // Use this to speed up tests, by not having to reach out to gcr.io for the default base image. // The registry uses a NOP logger to avoid spamming test logs. // Remember to call `defer Close()` on the returned `httptest.Server`. func registryServerWithImage(namespace string) (*httptest.Server, error) { nopLog := log.New(io.Discard, "", 0) r := registry.New(registry.Logger(nopLog)) s := httptest.NewServer(r) imageName := fmt.Sprintf("%s/%s", s.Listener.Addr().String(), namespace) image, err := random.Image(1024, 1) if err != nil { return nil, fmt.Errorf("random.Image(): %w", err) } crane.Push(image, imageName) return s, nil } func mustRepository(s string) name.Repository { n, err := name.NewRepository(s) if err != nil { panic(err) } return n } func mustDigest(img v1.Image) v1.Hash { d, err := img.Digest() if err != nil { panic(err) } return d } func mustRandom() v1.Image { img, err := random.Image(1024, 5) if err != nil { panic(err) } return img } func yamlToTmpFile(t *testing.T, yaml []byte) string { t.Helper() tmpfile, err := os.CreateTemp("", "doc") if err != nil { t.Fatalf("error creating temp file: %v", err) } if _, err := tmpfile.Write(yaml); err != nil { t.Fatalf("error writing temp file: %v", err) } if err := tmpfile.Close(); err != nil { t.Fatalf("error closing temp file: %v", err) } return tmpfile.Name() } ================================================ FILE: pkg/commands/root.go ================================================ // Copyright 2021 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package commands import ( "os" cranecmd "github.com/google/go-containerregistry/cmd/crane/cmd" "github.com/google/go-containerregistry/pkg/logs" "github.com/spf13/cobra" "go.uber.org/automaxprocs/maxprocs" ) var Root = New() func New() *cobra.Command { var verbose bool root := &cobra.Command{ Use: "ko", Short: "Rapidly iterate with Go, Containers, and Kubernetes.", SilenceUsage: true, // Don't show usage on errors DisableAutoGenTag: true, PersistentPreRun: func(_ *cobra.Command, _ []string) { if verbose { logs.Warn.SetOutput(os.Stderr) logs.Debug.SetOutput(os.Stderr) } logs.Progress.SetOutput(os.Stderr) maxprocs.Set(maxprocs.Logger(logs.Debug.Printf)) }, Run: func(cmd *cobra.Command, _ []string) { cmd.Help() }, } root.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable debug logs") AddKubeCommands(root) // Also add the auth group from crane to facilitate logging into a // registry. authCmd := cranecmd.NewCmdAuth(nil, "ko", "auth") // That was a mistake, but just set it to Hidden so we don't break people. authCmd.Hidden = true root.AddCommand(authCmd) // Just add a `ko login` command: root.AddCommand(cranecmd.NewCmdAuthLogin("ko")) return root } ================================================ FILE: pkg/commands/run.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package commands import ( "errors" "fmt" "log" "os" "os/exec" "path/filepath" "strings" "github.com/google/ko/pkg/commands/options" "github.com/spf13/cobra" ) // addRun augments our CLI surface with run. func addRun(topLevel *cobra.Command) { po := &options.PublishOptions{} bo := &options.BuildOptions{} run := &cobra.Command{ Use: "run IMPORTPATH", Short: "A variant of `kubectl run` that containerizes IMPORTPATH first.", Long: `This sub-command combines "ko build" and "kubectl run" to support containerizing and running Go binaries on Kubernetes in a single command.`, Example: ` # Publish the image and run it on Kubernetes as: # ${KO_DOCKER_REPO}/- # When KO_DOCKER_REPO is ko.local, it is the same as if # --local and --preserve-import-paths were passed. ko run github.com/foo/bar/cmd/baz # This supports relative import paths as well. ko run ./cmd/baz # You can also supply args and flags to the command. ko run ./cmd/baz -- -v arg1 arg2 --yes`, RunE: func(cmd *cobra.Command, args []string) error { if err := options.Validate(po, bo); err != nil { return fmt.Errorf("validating options: %w", err) } ctx := cmd.Context() // Args after -- are for kubectl, so only consider importPaths before it. importPaths := args dashes := cmd.Flags().ArgsLenAtDash() if dashes != -1 { importPaths = args[:cmd.Flags().ArgsLenAtDash()] } if len(importPaths) == 0 { return errors.New("ko run: no importpaths listed") } kubectlArgs := []string{} dashes = unparsedDashes() if dashes != -1 && dashes != len(os.Args) { kubectlArgs = os.Args[dashes+1:] } bo.InsecureRegistry = po.InsecureRegistry builder, err := makeBuilder(ctx, bo) if err != nil { return fmt.Errorf("error creating builder: %w", err) } publisher, err := makePublisher(po) if err != nil { return fmt.Errorf("error creating publisher: %w", err) } defer publisher.Close() if len(os.Args) < 3 { return fmt.Errorf("usage: %s run ", os.Args[0]) } ip := os.Args[2] if strings.HasPrefix(ip, "-") { return fmt.Errorf("expected first arg to be positional, got %q", ip) } imgs, err := publishImages(ctx, importPaths, publisher, builder) if err != nil { return fmt.Errorf("failed to publish images: %w", err) } // Usually only one, but this is the simple way to access the // reference since the import path may have been qualified. for k, ref := range imgs { log.Printf("Running %q", k) pod := filepath.Base(ref.Context().String()) // These are better defaults: defaults := []string{ "--attach", // stream logs back "--rm", // clean up after ourselves "--restart=Never", // we just want to run once "--log-flush-frequency=1s", // flush logs more often } // Replaced "" with "--image=". argv := []string{"--image", ref.String()} // Add our default kubectl flags. // TODO: Add some way to override these. argv = append(argv, defaults...) // If present, adds -- arg1 arg2... argv = append(argv, kubectlArgs...) // "run --image " argv = append([]string{"run", pod}, argv...) log.Printf("$ kubectl %s", strings.Join(argv, " ")) kubectlCmd := exec.CommandContext(ctx, "kubectl", argv...) // Pass through our environment kubectlCmd.Env = os.Environ() // Pass through our std* kubectlCmd.Stderr = os.Stderr kubectlCmd.Stdout = os.Stdout kubectlCmd.Stdin = os.Stdin // Run it. if err := kubectlCmd.Run(); err != nil { return err } } return nil }, // We ignore unknown flags to avoid importing everything Go exposes // from our commands. FParseErrWhitelist: cobra.FParseErrWhitelist{ UnknownFlags: true, }, } options.AddPublishArg(run, po) options.AddBuildOptions(run, bo) topLevel.AddCommand(run) } func unparsedDashes() int { for i, s := range os.Args { if s == "--" { return i } } return -1 } ================================================ FILE: pkg/commands/version.go ================================================ // Copyright 2019 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package commands import ( "fmt" "runtime/debug" "github.com/spf13/cobra" ) // Version is provided by govvv at compile-time var Version string // addVersion augments our CLI surface with version. func addVersion(topLevel *cobra.Command) { topLevel.AddCommand(&cobra.Command{ Use: "version", Short: `Print ko version.`, Run: func(_ *cobra.Command, _ []string) { v := version() if v == "" { fmt.Println("could not determine build information") } else { fmt.Println(v) } }, }) } func version() string { if Version == "" { i, ok := debug.ReadBuildInfo() if !ok { return "" } Version = i.Main.Version } return Version } ================================================ FILE: pkg/doc.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package ko holds libraries used to implement the ko CLI. package ko ================================================ FILE: pkg/internal/git/clone.go ================================================ // Copyright 2024 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // MIT License // // Copyright (c) 2016-2022 Carlos Alexandro Becker // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. package git import ( "context" "fmt" "os/exec" ) // Clone the git repository from the repoURL to the specified dir. func Clone(ctx context.Context, dir string, repoURL string) error { rc := runConfig{ dir: dir, args: []string{"clone", "--depth", "1", repoURL, "."}, } cmd := exec.CommandContext(ctx, "git", "clone", repoURL, dir) cmd.Dir = dir _, err := run(ctx, rc) if err != nil { return fmt.Errorf("running git clone: %w", err) } return nil } ================================================ FILE: pkg/internal/git/errors.go ================================================ // Copyright 2024 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // MIT License // // Copyright (c) 2016-2022 Carlos Alexandro Becker // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. package git import ( "errors" "fmt" ) var ( // ErrNoTag happens if the underlying git repository doesn't contain any tags // but no snapshot-release was requested. ErrNoTag = errors.New("git doesn't contain any tags. Tag info will not be available") // ErrNotRepository happens if you try to run ko against a folder // which is not a git repository. ErrNotRepository = errors.New("current folder is not a git repository. Git info will not be available") // ErrNoGit happens when git is not present in PATH. ErrNoGit = errors.New("git not present in PATH. Git info will not be available") ) // ErrDirty happens when the repo has uncommitted/unstashed changes. type ErrDirty struct { status string } func (e ErrDirty) Error() string { return fmt.Sprintf("git is in a dirty state\nPlease check in your pipeline what can be changing the following files:\n%v\n", e.status) } ================================================ FILE: pkg/internal/git/git.go ================================================ // Copyright 2024 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // MIT License // // Copyright (c) 2016-2022 Carlos Alexandro Becker // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. package git import ( "bytes" "context" "errors" "os/exec" "strings" ) type runConfig struct { dir string env []string args []string } // run a git command and returns its output or errors. func run(ctx context.Context, cfg runConfig) (string, error) { extraArgs := []string{ "-c", "log.showSignature=false", } cfg.args = append(extraArgs, cfg.args...) /* #nosec */ cmd := exec.CommandContext(ctx, "git", cfg.args...) cmd.Dir = cfg.dir stdout := bytes.Buffer{} stderr := bytes.Buffer{} cmd.Stdout = &stdout cmd.Stderr = &stderr cmd.Env = append(cmd.Env, cfg.env...) err := cmd.Run() if err != nil { return "", errors.New(stderr.String()) } return stdout.String(), nil } // clean the output. func clean(output string, err error) (string, error) { output = strings.ReplaceAll(strings.Split(output, "\n")[0], "'", "") if err != nil { err = errors.New(strings.TrimSuffix(err.Error(), "\n")) } return output, err } // cleanAllLines returns all the non-empty lines of the output, cleaned up. func cleanAllLines(output string, err error) ([]string, error) { result := make([]string, 0) for line := range strings.SplitSeq(output, "\n") { l := strings.TrimSpace(strings.ReplaceAll(line, "'", "")) if l == "" { continue } result = append(result, l) } // TODO: maybe check for exec.ExitError only? if err != nil { err = errors.New(strings.TrimSuffix(err.Error(), "\n")) } return result, err } ================================================ FILE: pkg/internal/git/info.go ================================================ // Copyright 2024 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // MIT License // // Copyright (c) 2016-2022 Carlos Alexandro Becker // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. package git import ( "context" "errors" "fmt" "os/exec" "strconv" "strings" "time" ) // Info includes tags and diffs used in some point. type Info struct { Branch string Tag string ShortCommit string FullCommit string CommitDate time.Time Dirty bool } // TemplateValue converts this Info into a map for use in golang templates. func (i Info) TemplateValue() map[string]any { treeState := "clean" if i.Dirty { treeState = "dirty" } return map[string]any{ "Branch": i.Branch, "Tag": i.Tag, "ShortCommit": i.ShortCommit, "FullCommit": i.FullCommit, "CommitDate": i.CommitDate.UTC().Format(time.RFC3339), "CommitTimestamp": i.CommitDate.UTC().Unix(), "IsDirty": i.Dirty, "IsClean": !i.Dirty, "TreeState": treeState, } } // GetInfo returns git information for the given directory func GetInfo(ctx context.Context, dir string) (Info, error) { if _, err := exec.LookPath("git"); err != nil { return Info{}, ErrNoGit } if !isRepo(ctx, dir) { return Info{}, ErrNotRepository } branch, err := getBranch(ctx, dir) if err != nil { return Info{}, fmt.Errorf("couldn't get current branch: %w", err) } short, err := getShortCommit(ctx, dir) if err != nil { return Info{}, fmt.Errorf("couldn't get current commit: %w", err) } full, err := getFullCommit(ctx, dir) if err != nil { return Info{}, fmt.Errorf("couldn't get current commit: %w", err) } date, err := getCommitDate(ctx, dir) if err != nil { return Info{}, fmt.Errorf("couldn't get commit date: %w", err) } dirty := checkDirty(ctx, dir) // TODO: allow exclusions. tag, err := getTag(ctx, dir, []string{}) if err != nil { return Info{ Branch: branch, FullCommit: full, ShortCommit: short, CommitDate: date, Tag: "v0.0.0", Dirty: dirty != nil, }, errors.Join(ErrNoTag, dirty) } return Info{ Branch: branch, Tag: tag, FullCommit: full, ShortCommit: short, CommitDate: date, Dirty: dirty != nil, }, dirty } // isRepo returns true if current folder is a git repository. func isRepo(ctx context.Context, dir string) bool { out, err := run(ctx, runConfig{ dir: dir, args: []string{"rev-parse", "--is-inside-work-tree"}, }) return err == nil && strings.TrimSpace(out) == "true" } // checkDirty returns an error if the current git repository is dirty. func checkDirty(ctx context.Context, dir string) error { out, err := run(ctx, runConfig{ dir: dir, args: []string{"status", "--porcelain"}, }) if strings.TrimSpace(out) != "" || err != nil { return ErrDirty{status: out} } return nil } func getBranch(ctx context.Context, dir string) (string, error) { return clean(run(ctx, runConfig{ dir: dir, args: []string{"rev-parse", "--abbrev-ref", "HEAD", "--quiet"}, })) } func getCommitDate(ctx context.Context, dir string) (time.Time, error) { ct, err := clean(run(ctx, runConfig{ dir: dir, args: []string{"show", "--format='%ct'", "HEAD", "--quiet"}, })) if err != nil { return time.Time{}, err } if ct == "" { return time.Time{}, nil } i, err := strconv.ParseInt(ct, 10, 64) if err != nil { return time.Time{}, err } t := time.Unix(i, 0).UTC() return t, nil } func getShortCommit(ctx context.Context, dir string) (string, error) { return clean(run(ctx, runConfig{ dir: dir, args: []string{"show", "--format=%h", "HEAD", "--quiet"}, })) } func getFullCommit(ctx context.Context, dir string) (string, error) { return clean(run(ctx, runConfig{ dir: dir, args: []string{"show", "--format=%H", "HEAD", "--quiet"}, })) } func getTag(ctx context.Context, dir string, excluding []string) (string, error) { // this will get the last tag, even if it wasn't made against the // last commit... tags, err := cleanAllLines(gitDescribe(ctx, dir, "HEAD", excluding)) if err != nil { return "", err } tag := filterOut(tags, excluding) return tag, err } func gitDescribe(ctx context.Context, dir, ref string, excluding []string) (string, error) { args := []string{ "describe", "--tags", "--abbrev=0", ref, } for _, exclude := range excluding { args = append(args, "--exclude="+exclude) } return clean(run(ctx, runConfig{ dir: dir, args: args, })) } func filterOut(tags []string, exclude []string) string { if len(exclude) == 0 && len(tags) > 0 { return tags[0] } for _, tag := range tags { for _, exl := range exclude { if exl != tag { return tag } } } return "" } ================================================ FILE: pkg/internal/git/info_test.go ================================================ // Copyright 2024 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // MIT License // // Copyright (c) 2016-2022 Carlos Alexandro Becker // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. package git_test import ( "context" "os" "path/filepath" "testing" "github.com/google/ko/pkg/internal/git" "github.com/google/ko/pkg/internal/gittesting" "github.com/stretchr/testify/require" ) const fakeGitURL = "git@github.com:foo/bar.git" func TestNotAGitFolder(t *testing.T) { dir := t.TempDir() i, err := git.GetInfo(context.TODO(), dir) require.ErrorIs(t, err, git.ErrNotRepository) tpl := i.TemplateValue() requireEmpty(t, tpl) } func TestSingleCommit(t *testing.T) { dir := t.TempDir() gittesting.GitInit(t, dir) gittesting.GitRemoteAdd(t, dir, fakeGitURL) gittesting.GitCommit(t, dir, "commit1") gittesting.GitTag(t, dir, "v0.0.1") i, err := git.GetInfo(context.TODO(), dir) require.NoError(t, err) tpl := i.TemplateValue() require.Equal(t, "main", tpl["Branch"]) require.Equal(t, "v0.0.1", tpl["Tag"]) require.NotEmpty(t, tpl["ShortCommit"].(string)) require.NotEmpty(t, tpl["FullCommit"].(string)) require.NotEmpty(t, tpl["CommitDate"].(string)) require.NotZero(t, tpl["CommitTimestamp"].(int64)) require.False(t, tpl["IsDirty"].(bool)) require.True(t, tpl["IsClean"].(bool)) require.Equal(t, "clean", tpl["TreeState"]) } func TestBranch(t *testing.T) { dir := t.TempDir() gittesting.GitInit(t, dir) gittesting.GitRemoteAdd(t, dir, fakeGitURL) gittesting.GitCommit(t, dir, "test-branch-commit") gittesting.GitTag(t, dir, "test-branch-tag") gittesting.GitCheckoutBranch(t, dir, "test-branch") i, err := git.GetInfo(context.TODO(), dir) require.NoError(t, err) tpl := i.TemplateValue() require.Equal(t, "test-branch", tpl["Branch"]) require.Equal(t, "test-branch-tag", tpl["Tag"]) require.NotEmpty(t, tpl["ShortCommit"].(string)) require.NotEmpty(t, tpl["FullCommit"].(string)) require.NotEmpty(t, tpl["CommitDate"].(string)) require.NotZero(t, tpl["CommitTimestamp"].(int64)) require.False(t, tpl["IsDirty"].(bool)) require.True(t, tpl["IsClean"].(bool)) require.Equal(t, "clean", tpl["TreeState"]) } func TestNewRepository(t *testing.T) { dir := t.TempDir() gittesting.GitInit(t, dir) i, err := git.GetInfo(context.TODO(), dir) // TODO: improve this error handling require.ErrorContains(t, err, `fatal: ambiguous argument 'HEAD'`) tpl := i.TemplateValue() requireEmpty(t, tpl) } func TestNoTags(t *testing.T) { dir := t.TempDir() gittesting.GitInit(t, dir) gittesting.GitRemoteAdd(t, dir, fakeGitURL) gittesting.GitCommit(t, dir, "first") i, err := git.GetInfo(context.TODO(), dir) require.ErrorIs(t, err, git.ErrNoTag) tpl := i.TemplateValue() require.Equal(t, "main", tpl["Branch"]) require.Equal(t, "v0.0.0", tpl["Tag"]) require.NotEmpty(t, tpl["ShortCommit"].(string)) require.NotEmpty(t, tpl["FullCommit"].(string)) require.NotEmpty(t, tpl["CommitDate"].(string)) require.NotZero(t, tpl["CommitTimestamp"].(int64)) require.False(t, tpl["IsDirty"].(bool)) require.True(t, tpl["IsClean"].(bool)) require.Equal(t, "clean", tpl["TreeState"]) } func TestDirty(t *testing.T) { dir := t.TempDir() gittesting.GitInit(t, dir) gittesting.GitRemoteAdd(t, dir, fakeGitURL) testFile, err := os.Create(filepath.Join(dir, "testFile")) require.NoError(t, err) require.NoError(t, testFile.Close()) gittesting.GitAdd(t, dir) gittesting.GitCommit(t, dir, "commit2") gittesting.GitTag(t, dir, "v0.0.1") require.NoError(t, os.WriteFile(testFile.Name(), []byte("lorem ipsum"), 0o644)) i, err := git.GetInfo(context.TODO(), dir) require.ErrorContains(t, err, "git is in a dirty state") tpl := i.TemplateValue() require.Equal(t, "main", tpl["Branch"]) require.Equal(t, "v0.0.1", tpl["Tag"]) require.NotEmpty(t, tpl["ShortCommit"].(string)) require.NotEmpty(t, tpl["FullCommit"].(string)) require.NotEmpty(t, tpl["CommitDate"].(string)) require.NotZero(t, tpl["CommitTimestamp"].(int64)) require.True(t, tpl["IsDirty"].(bool)) require.False(t, tpl["IsClean"].(bool)) require.Equal(t, "dirty", tpl["TreeState"]) } func TestValidState(t *testing.T) { dir := t.TempDir() gittesting.GitInit(t, dir) gittesting.GitRemoteAdd(t, dir, fakeGitURL) gittesting.GitCommit(t, dir, "commit3") gittesting.GitTag(t, dir, "v0.0.1") gittesting.GitTag(t, dir, "v0.0.2") gittesting.GitCommit(t, dir, "commit4") gittesting.GitTag(t, dir, "v0.0.3") i, err := git.GetInfo(context.TODO(), dir) require.NoError(t, err) require.Equal(t, "v0.0.3", i.Tag) require.False(t, i.Dirty) } func TestGitNotInPath(t *testing.T) { t.Setenv("PATH", "") i, err := git.GetInfo(context.TODO(), "") require.ErrorIs(t, err, git.ErrNoGit) tpl := i.TemplateValue() requireEmpty(t, tpl) } func requireEmpty(t *testing.T, tpl map[string]any) { require.Equal(t, "", tpl["Branch"]) require.Equal(t, "", tpl["Tag"]) require.Equal(t, "", tpl["ShortCommit"]) require.Equal(t, "", tpl["FullCommit"]) require.False(t, tpl["IsDirty"].(bool)) require.True(t, tpl["IsClean"].(bool)) require.Equal(t, "clean", tpl["TreeState"]) } ================================================ FILE: pkg/internal/gittesting/git.go ================================================ // Copyright 2024 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // MIT License // // Copyright (c) 2016-2022 Carlos Alexandro Becker // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. package gittesting import ( "bytes" "errors" "os/exec" "testing" "github.com/stretchr/testify/require" ) // GitInit inits a new git project. func GitInit(t *testing.T, dir string) { t.Helper() out, err := fakeGit(dir, "init") require.NoError(t, err) require.Contains(t, out, "Initialized empty Git repository", "") require.NoError(t, err) GitCheckoutBranch(t, dir, "main") _, _ = fakeGit("branch", "-D", "master") } // GitRemoteAdd adds the given url as remote. func GitRemoteAdd(t *testing.T, dir, url string) { t.Helper() out, err := fakeGit(dir, "remote", "add", "origin", url) require.NoError(t, err) require.Empty(t, out) } // GitCommit creates a git commits. func GitCommit(t *testing.T, dir, msg string) { t.Helper() out, err := fakeGit(dir, "commit", "--allow-empty", "-m", msg) require.NoError(t, err) require.Contains(t, out, "main", msg) } // GitTag creates a git tag. func GitTag(t *testing.T, dir, tag string) { t.Helper() out, err := fakeGit(dir, "tag", tag) require.NoError(t, err) require.Empty(t, out) } // GitAdd adds all files to stage. func GitAdd(t *testing.T, dir string) { t.Helper() out, err := fakeGit(dir, "add", "-A") require.NoError(t, err) require.Empty(t, out) } func fakeGit(dir string, args ...string) (string, error) { allArgs := []string{ "-c", "user.name='GoReleaser'", "-c", "user.email='test@goreleaser.github.com'", "-c", "commit.gpgSign=false", "-c", "tag.gpgSign=false", "-c", "log.showSignature=false", } allArgs = append(allArgs, args...) return gitRun(dir, allArgs...) } // GitCheckoutBranch allows us to change the active branch that we're using. func GitCheckoutBranch(t *testing.T, dir, name string) { t.Helper() out, err := fakeGit(dir, "checkout", "-b", name) require.NoError(t, err) require.Empty(t, out) } func gitRun(dir string, args ...string) (string, error) { cmd := exec.Command("git", args...) cmd.Dir = dir stdout := bytes.Buffer{} stderr := bytes.Buffer{} cmd.Stdout = &stdout cmd.Stderr = &stderr err := cmd.Run() if err != nil { return "", errors.New(stderr.String()) } return stdout.String(), nil } ================================================ FILE: pkg/internal/gittesting/git_test.go ================================================ // Copyright 2024 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // MIT License // // Copyright (c) 2016-2022 Carlos Alexandro Becker // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. package gittesting import "testing" func TestGit(t *testing.T) { dir := t.TempDir() GitInit(t, dir) GitAdd(t, dir) GitCommit(t, dir, "commit1") GitRemoteAdd(t, dir, "git@github.com:goreleaser/nope.git") GitTag(t, dir, "v1.0.0") } ================================================ FILE: pkg/internal/testing/daemon.go ================================================ // Copyright 2021 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package testing import ( "context" "io" "strings" "github.com/google/go-containerregistry/pkg/v1/daemon" "github.com/moby/moby/client" ) type MockDaemon struct { daemon.Client Tags []string inspectErr error inspectResp client.ImageInspectResult } func (m *MockDaemon) Ping(context.Context, client.PingOptions) (client.PingResult, error) { return client.PingResult{}, nil } func (m *MockDaemon) ImageLoad(context.Context, io.Reader, ...client.ImageLoadOption) (client.ImageLoadResult, error) { return io.NopCloser(strings.NewReader("Loaded")), nil } func (m *MockDaemon) ImageTag(_ context.Context, opt client.ImageTagOptions) (client.ImageTagResult, error) { if m.Tags == nil { m.Tags = []string{} } m.Tags = append(m.Tags, opt.Target) return client.ImageTagResult{}, nil } func (m *MockDaemon) ImageInspect(context.Context, string, ...client.ImageInspectOption) (client.ImageInspectResult, error) { return m.inspectResp, m.inspectErr } ================================================ FILE: pkg/internal/testing/doc.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package testing holds a variety test doubles to help with testing package testing ================================================ FILE: pkg/internal/testing/fixed.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package testing import ( "context" "errors" "fmt" "strings" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/ko/pkg/build" "github.com/google/ko/pkg/publish" ) type fixedBuild struct { entries map[string]build.Result } // NewFixedBuild returns a build.Interface implementation that simply resolves // particular references to fixed v1.Image objects func NewFixedBuild(entries map[string]build.Result) build.Interface { return &fixedBuild{entries} } // QualifyImport implements build.Interface func (f *fixedBuild) QualifyImport(ip string) (string, error) { return ip, nil } // IsSupportedReference implements build.Interface func (f *fixedBuild) IsSupportedReference(s string) error { s = strings.TrimPrefix(s, build.StrictScheme) if _, ok := f.entries[s]; !ok { return errors.New("importpath is not supported") } return nil } // Build implements build.Interface func (f *fixedBuild) Build(_ context.Context, s string) (build.Result, error) { s = strings.TrimPrefix(s, build.StrictScheme) if img, ok := f.entries[s]; ok { return img, nil } return nil, fmt.Errorf("unsupported reference: %q", s) } type fixedPublish struct { base name.Repository entries map[string]v1.Hash } // NewFixedPublish returns a publish.Interface implementation that simply // resolves particular references to fixed name.Digest references. func NewFixedPublish(base name.Repository, entries map[string]v1.Hash) publish.Interface { return &fixedPublish{base, entries} } // Publish implements publish.Interface func (f *fixedPublish) Publish(_ context.Context, _ build.Result, s string) (name.Reference, error) { s = strings.TrimPrefix(s, build.StrictScheme) h, ok := f.entries[s] if !ok { return nil, fmt.Errorf("unsupported importpath: %q", s) } d, err := name.NewDigest(fmt.Sprintf("%s/%s@%s", f.base, s, h)) if err != nil { return nil, err } return &d, nil } func (f *fixedPublish) Close() error { return nil } func ComputeDigest(base name.Repository, ref string, h v1.Hash) string { d, err := name.NewDigest(fmt.Sprintf("%s/%s@%s", base, ref, h)) if err != nil { panic(err) } return d.String() } ================================================ FILE: pkg/internal/testing/fixed_test.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package testing import ( "context" "testing" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/random" "github.com/google/ko/pkg/build" ) func TestFixedPublish(t *testing.T) { hex1 := "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" hex2 := "baadf00dbaadf00dbaadf00dbaadf00dbaadf00dbaadf00dbaadf00dbaadf00d" fixedBaseRepo, _ := name.NewRepository("gcr.io/asdf") f := NewFixedPublish(fixedBaseRepo, map[string]v1.Hash{ "foo": { Algorithm: "sha256", Hex: hex1, }, "bar": { Algorithm: "sha256", Hex: hex2, }, }) fooDigest, err := f.Publish(context.Background(), nil, "foo") if err != nil { t.Errorf("Publish(foo) = %v", err) } if got, want := fooDigest.String(), "gcr.io/asdf/foo@sha256:"+hex1; got != want { t.Errorf("Publish(foo) = %q, want %q", got, want) } barDigest, err := f.Publish(context.Background(), nil, "bar") if err != nil { t.Errorf("Publish(bar) = %v", err) } if got, want := barDigest.String(), "gcr.io/asdf/bar@sha256:"+hex2; got != want { t.Errorf("Publish(bar) = %q, want %q", got, want) } d, err := f.Publish(context.Background(), nil, "baz") if err == nil { t.Errorf("Publish(baz) = %v, want error", d) } } func TestFixedBuild(t *testing.T) { testImage, _ := random.Image(1024, 5) f := NewFixedBuild(map[string]build.Result{ "asdf": testImage, }) if got := f.IsSupportedReference("asdf"); got != nil { t.Errorf("IsSupportedReference(asdf) = (%v), want nil", got) } if got, err := f.Build(context.Background(), "asdf"); err != nil { t.Errorf("Build(asdf) = %v, want %v", err, testImage) } else if got != testImage { t.Errorf("Build(asdf) = %v, want %v", got, testImage) } if got := f.IsSupportedReference("blah"); got == nil { t.Error("IsSupportedReference(blah) = nil, want error") } if got, err := f.Build(context.Background(), "blah"); err == nil { t.Errorf("Build(blah) = %v, want error", got) } } ================================================ FILE: pkg/publish/daemon.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package publish import ( "context" "fmt" "log" "os" "strings" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/daemon" "github.com/google/ko/pkg/build" ) const ( // LocalDomain is a sentinel "registry" that represents side-loading images into the daemon. LocalDomain = "ko.local" ) // demon is intentionally misspelled to avoid name collision (and drive Jon nuts). // [Narrator: Jon wasn't the only one driven nuts.] type demon struct { base string client daemon.Client namer Namer tags []string } // DaemonOption is a functional option for NewDaemon. type DaemonOption func(*demon) error // WithLocalDomain is a functional option for overriding the domain used for images that are side-loaded into the daemon. func WithLocalDomain(domain string) DaemonOption { return func(i *demon) error { if domain != "" { i.base = domain } return nil } } // WithDockerClient is a functional option for overriding the docker client. func WithDockerClient(client daemon.Client) DaemonOption { return func(i *demon) error { if client != nil { i.client = client } return nil } } // NewDaemon returns a new publish.Interface that publishes images to a container daemon. func NewDaemon(namer Namer, tags []string, opts ...DaemonOption) (Interface, error) { d := &demon{ base: LocalDomain, namer: namer, tags: tags, } for _, option := range opts { if err := option(d); err != nil { return nil, err } } return d, nil } func (d *demon) getOpts(ctx context.Context) []daemon.Option { return []daemon.Option{ daemon.WithContext(ctx), daemon.WithClient(d.client), } } // Publish implements publish.Interface func (d *demon) Publish(ctx context.Context, br build.Result, s string) (name.Reference, error) { s = strings.TrimPrefix(s, build.StrictScheme) // https://github.com/google/go-containerregistry/issues/212 s = strings.ToLower(s) // There's no way to write an index to a kind, so attempt to downcast it to an image. var img v1.Image switch i := br.(type) { case v1.Image: img = i case v1.ImageIndex: im, err := i.IndexManifest() if err != nil { return nil, err } goos, goarch := os.Getenv("GOOS"), os.Getenv("GOARCH") if goos == "" { goos = "linux" } if goarch == "" { goarch = "amd64" } for _, manifest := range im.Manifests { if manifest.Platform == nil { continue } if manifest.Platform.OS != goos { continue } if manifest.Platform.Architecture != goarch { continue } img, err = i.Image(manifest.Digest) if err != nil { return nil, err } break } if img == nil { return nil, fmt.Errorf("failed to find %s/%s image in index for image: %v", goos, goarch, s) } default: return nil, fmt.Errorf("failed to interpret %s result as image: %v", s, br) } h, err := img.Digest() if err != nil { return nil, err } digestTag, err := name.NewTag(fmt.Sprintf("%s:%s", d.namer(d.base, s), h.Hex)) if err != nil { return nil, err } log.Printf("Loading %v", digestTag) if resp, err := daemon.Write(digestTag, img, d.getOpts(ctx)...); err != nil { log.Println("daemon.Write response: ", resp) return nil, err } log.Printf("Loaded %v", digestTag) for _, tagName := range d.tags { log.Printf("Adding tag %v", tagName) tag, err := name.NewTag(fmt.Sprintf("%s:%s", d.namer(d.base, s), tagName)) if err != nil { return nil, err } if err := daemon.Tag(digestTag, tag, d.getOpts(ctx)...); err != nil { return nil, err } log.Printf("Added tag %v", tagName) } return &digestTag, nil } func (d *demon) Close() error { return nil } ================================================ FILE: pkg/publish/daemon_test.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package publish_test import ( "context" "fmt" "strings" "testing" "github.com/google/go-containerregistry/pkg/v1/random" kotesting "github.com/google/ko/pkg/internal/testing" "github.com/google/ko/pkg/publish" ) func TestDaemon(t *testing.T) { importpath := "github.com/google/ko" img, err := random.Image(1024, 1) if err != nil { t.Fatalf("random.Image() = %v", err) } client := &kotesting.MockDaemon{} def, err := publish.NewDaemon(md5Hash, []string{}, publish.WithDockerClient(client)) if err != nil { t.Fatalf("NewDaemon() = %v", err) } if d, err := def.Publish(context.Background(), img, importpath); err != nil { t.Errorf("Publish() = %v", err) } else if got, want := d.String(), md5Hash("ko.local", importpath); !strings.HasPrefix(got, want) { t.Errorf("Publish() = %v, wanted prefix %v", got, want) } } func TestDaemonTags(t *testing.T) { importpath := "github.com/google/ko" img, err := random.Image(1024, 1) if err != nil { t.Fatalf("random.Image() = %v", err) } client := &kotesting.MockDaemon{} def, err := publish.NewDaemon(md5Hash, []string{"v2.0.0", "v1.2.3", "production"}, publish.WithDockerClient(client)) if err != nil { t.Fatalf("NewDaemon() = %v", err) } if d, err := def.Publish(context.Background(), img, importpath); err != nil { t.Errorf("Publish() = %v", err) } else if got, want := d.String(), md5Hash("ko.local", importpath); !strings.HasPrefix(got, want) { t.Errorf("Publish() = %v, wanted prefix %v", got, want) } imgDigest, err := img.Digest() if err != nil { t.Fatalf("img.Digest() = %v", err) } expected := []string{fmt.Sprintf("ko.local/98b8c7facdad74510a7cae0cd368eb4e:%s", strings.Replace(imgDigest.String(), "sha256:", "", 1)), "ko.local/98b8c7facdad74510a7cae0cd368eb4e:v2.0.0", "ko.local/98b8c7facdad74510a7cae0cd368eb4e:v1.2.3", "ko.local/98b8c7facdad74510a7cae0cd368eb4e:production"} for i, v := range expected { if client.Tags[i] != v { t.Errorf("Expected tag %v got %v", v, client.Tags[i]) } } } func TestDaemonDomain(t *testing.T) { importpath := "github.com/google/ko" img, err := random.Image(1024, 1) if err != nil { t.Fatalf("random.Image() = %v", err) } localDomain := "registry.example.com/repository" client := &kotesting.MockDaemon{} def, err := publish.NewDaemon(md5Hash, []string{}, publish.WithLocalDomain(localDomain), publish.WithDockerClient(client)) if err != nil { t.Fatalf("NewDaemon() = %v", err) } if d, err := def.Publish(context.Background(), img, importpath); err != nil { t.Errorf("Publish() = %v", err) } else if got, want := d.String(), md5Hash(localDomain, importpath); !strings.HasPrefix(got, want) { t.Errorf("Publish() = %v, wanted prefix %v", got, want) } } ================================================ FILE: pkg/publish/default.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package publish import ( "context" "errors" "fmt" "log" "net/http" "path" "runtime" "strings" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/types" "github.com/sigstore/cosign/v3/pkg/oci" ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" "github.com/sigstore/cosign/v3/pkg/oci/walk" "golang.org/x/sync/errgroup" "github.com/google/ko/pkg/build" ) // defalt is intentionally misspelled to avoid keyword collision (and drive Jon nuts). type defalt struct { base string namer Namer tags []string tagOnly bool insecure bool jobs int pusher *remote.Pusher oopt []ociremote.Option } // Option is a functional option for NewDefault. type Option func(*defaultOpener) error type defaultOpener struct { base string t http.RoundTripper userAgent string keychain authn.Keychain namer Namer tags []string tagOnly bool insecure bool ropt []remote.Option jobs int } // Namer is a function from a supported import path to the portion of the resulting // image name that follows the "base" repository name. type Namer func(string, string) string // identity is the default namer, so import paths are affixed as-is under the repository // name for maximum clarity, e.g. // // gcr.io/foo/github.com/bar/baz/cmd/blah // ^--base--^ ^-------import path-------^ func identity(base, in string) string { return path.Join(base, in) } // As some registries do not support pushing an image by digest, the default tag for pushing // is the 'latest' tag. const latestTag = "latest" func (do *defaultOpener) Open() (Interface, error) { if do.tagOnly { if len(do.tags) != 1 { return nil, errors.New("must specify exactly one tag to resolve images into tag-only references") } if do.tags[0] == latestTag { return nil, errors.New("latest tag cannot be used in tag-only references") } } pusher, err := remote.NewPusher(do.ropt...) if err != nil { return nil, err } oopt := []ociremote.Option{ociremote.WithRemoteOptions(do.ropt...)} // Respect COSIGN_REPOSITORY targetRepoOverride, err := ociremote.GetEnvTargetRepository() if err != nil { return nil, err } if (targetRepoOverride != name.Repository{}) { oopt = append(oopt, ociremote.WithTargetRepository(targetRepoOverride)) } return &defalt{ base: do.base, namer: do.namer, tags: do.tags, tagOnly: do.tagOnly, insecure: do.insecure, jobs: do.jobs, pusher: pusher, oopt: oopt, }, nil } // NewDefault returns a new publish.Interface that publishes references under the provided base // repository using the default keychain to authenticate and the default naming scheme. func NewDefault(base string, options ...Option) (Interface, error) { do := &defaultOpener{ base: base, t: remote.DefaultTransport, userAgent: "ko", keychain: authn.DefaultKeychain, namer: identity, tags: []string{latestTag}, } for _, option := range options { if err := option(do); err != nil { return nil, err } } do.ropt = []remote.Option{remote.WithAuthFromKeychain(do.keychain), remote.WithTransport(do.t), remote.WithUserAgent(do.userAgent)} if do.jobs == 0 { do.jobs = runtime.GOMAXPROCS(0) do.ropt = append(do.ropt, remote.WithJobs(do.jobs)) } return do.Open() } func (d *defalt) pushResult(ctx context.Context, tag name.Tag, br build.Result) error { mt, err := br.MediaType() if err != nil { return err } g, ctx := errgroup.WithContext(ctx) g.SetLimit(d.jobs) g.Go(func() error { return d.pusher.Push(ctx, tag, br) }) // writePeripherals implements walk.Fn writePeripherals := func(ctx context.Context, se oci.SignedEntity) error { h, err := se.(interface{ Digest() (v1.Hash, error) }).Digest() if err != nil { return err } // TODO(mattmoor): We should have a WriteSBOM helper upstream. digest := tag.Context().Digest(h.String()) // Don't *get* the tag, we know the digest ref, err := ociremote.SBOMTag(digest, d.oopt...) if err != nil { return err } f, err := se.Attachment("sbom") if err != nil { // Some levels (e.g. the index) may not have an SBOM, // just like some levels may not have signatures/attestations. return nil } g.Go(func() error { if err := d.pusher.Push(ctx, ref, f); err != nil { return fmt.Errorf("writing sbom: %w", err) } log.Printf("Published SBOM %v", ref) return nil }) // TODO(mattmoor): Don't enable this until we start signing or it // will publish empty signatures! // if err := ociremote.WriteSignatures(tag.Context(), se, oopt...); err != nil { // return err // } // TODO(mattmoor): Are there any attestations we want to write? // if err := ociremote.WriteAttestations(tag.Context(), se, oopt...); err != nil { // return err // } return nil } switch mt { case types.OCIImageIndex, types.DockerManifestList: if sii, ok := br.(oci.SignedImageIndex); ok { if err := walk.SignedEntity(ctx, sii, writePeripherals); err != nil { return err } } case types.OCIManifestSchema1, types.DockerManifestSchema2: if si, ok := br.(oci.SignedImage); ok { if err := writePeripherals(ctx, si); err != nil { return err } } default: return fmt.Errorf("result image media type: %s", mt) } return g.Wait() } // Publish implements publish.Interface func (d *defalt) Publish(ctx context.Context, br build.Result, s string) (name.Reference, error) { s = strings.TrimPrefix(s, build.StrictScheme) // https://github.com/google/go-containerregistry/issues/212 s = strings.ToLower(s) no := []name.Option{} if d.insecure { no = append(no, name.Insecure) } g, ctx := errgroup.WithContext(ctx) g.SetLimit(d.jobs) for i, tagName := range d.tags { tag, err := name.NewTag(fmt.Sprintf("%s:%s", d.namer(d.base, s), tagName), no...) if err != nil { return nil, err } if i == 0 { log.Printf("Publishing %v", tag) g.Go(func() error { return d.pushResult(ctx, tag, br) }) } else { g.Go(func() error { log.Printf("Tagging %v", tag) return d.pusher.Push(ctx, tag, br) }) } } if err := g.Wait(); err != nil { return nil, err } if d.tagOnly { // We have already validated that there is a single tag (not latest). return name.NewTag(fmt.Sprintf("%s:%s", d.namer(d.base, s), d.tags[0])) } h, err := br.Digest() if err != nil { return nil, err } ref := fmt.Sprintf("%s@%s", d.namer(d.base, s), h) if len(d.tags) == 1 && d.tags[0] != latestTag { // If a single tag is explicitly set (not latest), then this // is probably a release, so include the tag in the reference. ref = fmt.Sprintf("%s:%s@%s", d.namer(d.base, s), d.tags[0], h) } dig, err := name.NewDigest(ref) if err != nil { return nil, err } log.Printf("Published %v", dig) return &dig, nil } func (d *defalt) Close() error { return nil } ================================================ FILE: pkg/publish/default_test.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package publish_test import ( "context" "crypto/md5" "encoding/hex" "fmt" "net/http/httptest" "net/url" "path/filepath" "strings" "testing" "github.com/google/go-containerregistry/pkg/crane" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/registry" "github.com/google/go-containerregistry/pkg/v1/random" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/ko/pkg/build" "github.com/google/ko/pkg/publish" ocimutate "github.com/sigstore/cosign/v3/pkg/oci/mutate" "github.com/sigstore/cosign/v3/pkg/oci/signed" "github.com/sigstore/cosign/v3/pkg/oci/static" ) var ( img, _ = random.Image(1024, 3) idx, _ = random.Index(1024, 3, 3) ) func TestDefault(t *testing.T) { f, err := static.NewFile([]byte("da bom")) if err != nil { t.Fatalf("static.NewFile() = %v", err) } si, err := ocimutate.AttachFileToImage(signed.Image(img), "sbom", f) if err != nil { t.Fatalf("ocimutate.AttachFileToImage() = %v", err) } sii, err := ocimutate.AttachFileToImageIndex(signed.ImageIndex(idx), "sbom", f) if err != nil { t.Fatalf("ocimutate.AttachFileToImageIndex() = %v", err) } for _, br := range []build.Result{img, idx, si, sii} { base := "blah" importpath := "github.com/Google/go-containerregistry/cmd/crane" expectedRepo := fmt.Sprintf("%s/%s", base, strings.ToLower(importpath)) server := httptest.NewServer(registry.New()) defer server.Close() u, err := url.Parse(server.URL) if err != nil { t.Fatalf("url.Parse(%v) = %v", server.URL, err) } tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo)) if err != nil { t.Fatalf("NewTag() = %v", err) } repoName := fmt.Sprintf("%s/%s", u.Host, base) def, err := publish.NewDefault(repoName) if err != nil { t.Errorf("NewDefault() = %v", err) } if d, err := def.Publish(context.Background(), br, build.StrictScheme+importpath); err != nil { t.Errorf("Publish() = %v", err) } else if !strings.HasPrefix(d.String(), tag.Repository.String()) { t.Errorf("Publish() = %v, wanted prefix %v", d, tag.Repository) } } } func md5Hash(base, s string) string { // md5 as hex. hasher := md5.New() hasher.Write([]byte(s)) return filepath.Join(base, hex.EncodeToString(hasher.Sum(nil))) } func TestDefaultWithCustomNamer(t *testing.T) { for _, br := range []build.Result{img, idx} { base := "blah" importpath := "github.com/Google/go-containerregistry/cmd/crane" expectedRepo := fmt.Sprintf("%s/%s", base, strings.ToLower(importpath)) server := httptest.NewServer(registry.New()) defer server.Close() u, err := url.Parse(server.URL) if err != nil { t.Fatalf("url.Parse(%v) = %v", server.URL, err) } tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo)) if err != nil { t.Fatalf("NewTag() = %v", err) } repoName := fmt.Sprintf("%s/%s", u.Host, base) def, err := publish.NewDefault(repoName, publish.WithNamer(md5Hash)) if err != nil { t.Errorf("NewDefault() = %v", err) } if d, err := def.Publish(context.Background(), br, build.StrictScheme+importpath); err != nil { t.Errorf("Publish() = %v", err) } else if !strings.HasPrefix(d.String(), repoName) { t.Errorf("Publish() = %v, wanted prefix %v", d, tag.Repository) } else if !strings.HasSuffix(d.Context().String(), md5Hash("", strings.ToLower(importpath))) { t.Errorf("Publish() = %v, wanted suffix %v", d.Context(), md5Hash("", importpath)) } } } func TestDefaultWithTags(t *testing.T) { for _, br := range []build.Result{img, idx} { base := "blah" importpath := "github.com/Google/go-containerregistry/cmd/crane" expectedRepo := fmt.Sprintf("%s/%s", base, strings.ToLower(importpath)) server := httptest.NewServer(registry.New()) defer server.Close() u, err := url.Parse(server.URL) if err != nil { t.Fatalf("url.Parse(%v) = %v", server.URL, err) } tag, err := name.NewTag(fmt.Sprintf("%s/%s:notLatest", u.Host, expectedRepo)) if err != nil { t.Fatalf("NewTag() = %v", err) } repoName := fmt.Sprintf("%s/%s", u.Host, base) def, err := publish.NewDefault(repoName, publish.WithTags([]string{"notLatest", "v1.2.3"})) if err != nil { t.Errorf("NewDefault() = %v", err) } if d, err := def.Publish(context.Background(), br, build.StrictScheme+importpath); err != nil { t.Errorf("Publish() = %v", err) } else if !strings.HasPrefix(d.String(), repoName) { t.Errorf("Publish() = %v, wanted prefix %v", d, tag.Repository) } else if !strings.HasSuffix(d.Context().String(), strings.ToLower(importpath)) { t.Errorf("Publish() = %v, wanted suffix %v", d.Context(), md5Hash("", importpath)) } otherTag := fmt.Sprintf("%s/%s:v1.2.3", u.Host, expectedRepo) first, err := crane.Digest(tag.String()) if err != nil { t.Fatal(err) } second, err := crane.Digest(otherTag) if err != nil { t.Fatal(err) } if first != second { t.Errorf("tagging didn't work: %s != %s", second, first) } } } func TestDefaultWithReleaseTag(t *testing.T) { img, err := random.Image(1024, 1) if err != nil { t.Fatalf("random.Image() = %v", err) } base := "blah" releaseTag := "v1.2.3" importpath := "github.com/Google/go-containerregistry/cmd/crane" expectedRepo := fmt.Sprintf("%s/%s", base, strings.ToLower(importpath)) server := httptest.NewServer(registry.New()) defer server.Close() u, err := url.Parse(server.URL) if err != nil { t.Fatalf("url.Parse(%v) = %v", server.URL, err) } tag, err := name.NewTag(fmt.Sprintf("%s/%s:notLatest", u.Host, expectedRepo)) if err != nil { t.Fatalf("NewTag() = %v", err) } repoName := fmt.Sprintf("%s/%s", u.Host, base) def, err := publish.NewDefault(repoName, publish.WithTags([]string{releaseTag})) if err != nil { t.Errorf("NewDefault() = %v", err) } if d, err := def.Publish(context.Background(), img, build.StrictScheme+importpath); err != nil { t.Errorf("Publish() = %v", err) } else if !strings.HasPrefix(d.String(), repoName) { t.Errorf("Publish() = %v, wanted prefix %v", d, tag.Repository) } else if !strings.HasSuffix(d.Context().String(), strings.ToLower(importpath)) { t.Errorf("Publish() = %v, wanted suffix %v", d.Context(), md5Hash("", importpath)) } else if !strings.Contains(d.String(), releaseTag) { t.Errorf("Publish() = %v, wanted tag included: %v", d.String(), releaseTag) } tags, err := remote.List(tag.Context()) if err != nil { t.Fatalf("remote.List(): %v", err) } createdTags := make(map[string]struct{}) for _, got := range tags { createdTags[got] = struct{}{} } if _, ok := createdTags["v1.2.3"]; !ok { t.Errorf("Tag v1.2.3 was not created.") } def, err = publish.NewDefault(repoName, publish.WithTags([]string{releaseTag}), publish.WithTagOnly(true)) if err != nil { t.Errorf("NewDefault() = %v", err) } if d, err := def.Publish(context.Background(), img, build.StrictScheme+importpath); err != nil { t.Errorf("Publish() = %v", err) } else if !strings.HasPrefix(d.String(), repoName) { t.Errorf("Publish() = %v, wanted prefix %v", d, tag.Repository) } else if !strings.HasSuffix(d.Context().String(), strings.ToLower(importpath)) { t.Errorf("Publish() = %v, wanted suffix %v", d.Context(), md5Hash("", importpath)) } else if !strings.Contains(d.String(), releaseTag) { t.Errorf("Publish() = %v, wanted tag included: %v", d.String(), releaseTag) } else if strings.Contains(d.String(), "@sha256:") { t.Errorf("Publish() = %v, wanted no digest", d.String()) } } ================================================ FILE: pkg/publish/doc.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package publish defines methods for publishing a v1.Image reference and // returning the published digest for embedding back into a Kubernetes yaml. package publish ================================================ FILE: pkg/publish/future.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package publish import ( "sync" "github.com/google/go-containerregistry/pkg/name" ) func newFuture(work func() (name.Reference, error)) *future { // Create a channel on which to send the result. ch := make(chan *result) // Initiate the actual work, sending its result // along the above channel. go func() { ref, err := work() ch <- &result{ref: ref, err: err} }() // Return a future for the above work. Callers should // call .Get() on this result (as many times as needed). // One of these calls will receive the result, store it, // and close the channel so that the rest of the callers // can consume it. return &future{ promise: ch, } } type result struct { ref name.Reference err error } type future struct { m sync.RWMutex result *result promise chan *result } // Get blocks on the result of the future. func (f *future) Get() (name.Reference, error) { // Block on the promise of a result until we get one. result, ok := <-f.promise if ok { func() { f.m.Lock() defer f.m.Unlock() // If we got the result, then store it so that // others may access it. f.result = result // Close the promise channel so that others // are signaled that the result is available. close(f.promise) }() } f.m.RLock() defer f.m.RUnlock() return f.result.ref, f.result.err } ================================================ FILE: pkg/publish/future_test.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package publish import ( "fmt" "testing" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/random" ) func makeRef() (name.Reference, error) { img, err := random.Image(256, 8) if err != nil { return nil, err } d, err := img.Digest() if err != nil { return nil, err } return name.NewDigest(fmt.Sprintf("gcr.io/foo/bar@%s", d)) } func TestSameFutureSameReference(t *testing.T) { f := newFuture(makeRef) ref1, err := f.Get() if err != nil { t.Errorf("Get() = %v", err) } d1 := ref1.String() ref2, err := f.Get() if err != nil { t.Errorf("Get() = %v", err) } d2 := ref2.String() if d1 != d2 { t.Errorf("Got different digests %s and %s", d1, d2) } } func TestDiffFutureDiffReference(t *testing.T) { f1 := newFuture(makeRef) f2 := newFuture(makeRef) ref1, err := f1.Get() if err != nil { t.Errorf("Get() = %v", err) } d1 := ref1.String() ref2, err := f2.Get() if err != nil { t.Errorf("Get() = %v", err) } d2 := ref2.String() if d1 == d2 { t.Errorf("Got same digest %s, wanted different", d1) } } ================================================ FILE: pkg/publish/kind/doc.go ================================================ // Copyright 2020 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package kind defines methods for publishing images into kind nodes. package kind ================================================ FILE: pkg/publish/kind/write.go ================================================ // Copyright 2020 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kind import ( "bytes" "context" "fmt" "io" "os" "golang.org/x/sync/errgroup" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/tarball" "sigs.k8s.io/kind/pkg/cluster" "sigs.k8s.io/kind/pkg/cluster/nodes" ) // Supported since kind 0.8.0 (https://github.com/kubernetes-sigs/kind/releases/tag/v0.8.0) const clusterNameEnvKey = "KIND_CLUSTER_NAME" // provider is an interface for kind providers to facilitate testing. type provider interface { ListInternalNodes(name string) ([]nodes.Node, error) } // GetProvider is a variable so we can override in tests. var GetProvider = func() provider { return cluster.NewProvider() } // Tag adds a tag to an already existent image. func Tag(ctx context.Context, src, dest name.Tag) error { return onEachNode(func(n nodes.Node) error { var buf bytes.Buffer cmd := n.CommandContext(ctx, "ctr", "--namespace=k8s.io", "images", "tag", "--force", src.String(), dest.String()) cmd.SetStdout(&buf) cmd.SetStderr(&buf) if err := cmd.Run(); err != nil { return fmt.Errorf("failed to tag image: %w\n%s", err, buf.String()) } return nil }) } // Write saves the image into the kind nodes as the given tag. func Write(ctx context.Context, tag name.Tag, img v1.Image) error { return onEachNode(func(n nodes.Node) error { pr, pw := io.Pipe() grp := errgroup.Group{} grp.Go(func() error { return pw.CloseWithError(tarball.Write(tag, img, pw)) }) var buf bytes.Buffer cmd := n.CommandContext(ctx, "ctr", "--namespace=k8s.io", "images", "import", "--all-platforms", "-").SetStdin(pr) cmd.SetStdout(&buf) cmd.SetStderr(&buf) if err := cmd.Run(); err != nil { return fmt.Errorf("failed to load image to node %q: %w\n%s", n, err, buf.String()) } if err := grp.Wait(); err != nil { return fmt.Errorf("failed to write intermediate tarball representation: %w", err) } return nil }) } // onEachNode executes the given function on each node. Exits on first error. func onEachNode(f func(nodes.Node) error) error { nodeList, err := getNodes() if err != nil { return err } for _, n := range nodeList { if err := f(n); err != nil { return err } } return nil } // getNodes gets all the nodes of the default cluster. // Returns an error if none were found. func getNodes() ([]nodes.Node, error) { provider := GetProvider() clusterName := os.Getenv(clusterNameEnvKey) if clusterName == "" { clusterName = cluster.DefaultName } nodeList, err := provider.ListInternalNodes(clusterName) if err != nil { return nil, err } if len(nodeList) == 0 { return nil, fmt.Errorf("no nodes found for cluster %q", clusterName) } return nodeList, nil } ================================================ FILE: pkg/publish/kind/write_test.go ================================================ // Copyright 2020 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kind import ( "context" "errors" "fmt" "io" "strings" "testing" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/random" "sigs.k8s.io/kind/pkg/cluster/nodes" "sigs.k8s.io/kind/pkg/exec" ) func TestWrite(t *testing.T) { ctx := context.Background() img, err := random.Image(1024, 1) if err != nil { t.Fatalf("random.Image() = %v", err) } tag, err := name.NewTag("kind.local/test:new") if err != nil { t.Fatalf("name.NewTag() = %v", err) } n1 := &fakeNode{} n2 := &fakeNode{} GetProvider = func() provider { return &fakeProvider{nodes: []nodes.Node{n1, n2}} } if err := Write(ctx, tag, img); err != nil { t.Fatalf("Write() = %v", err) } // Verify the respective command is executed on each node. for _, n := range []*fakeNode{n1, n2} { if got, want := len(n.cmds), 1; got != want { t.Fatalf("len(n.cmds) = %d, want %d", got, want) } c := n.cmds[0] if got, want := c.cmd, "ctr --namespace=k8s.io images import --all-platforms -"; got != want { t.Fatalf("c.cmd = %s, want %s", got, want) } } } func TestTag(t *testing.T) { ctx := context.Background() oldTag, err := name.NewTag("kind.local/test:test") if err != nil { t.Fatalf("name.NewTag() = %v", err) } newTag, err := name.NewTag("kind.local/test:new") if err != nil { t.Fatalf("name.NewTag() = %v", err) } n1 := &fakeNode{} n2 := &fakeNode{} GetProvider = func() provider { return &fakeProvider{nodes: []nodes.Node{n1, n2}} } if err := Tag(ctx, oldTag, newTag); err != nil { t.Fatalf("Tag() = %v", err) } // Verify the respective command is executed on each node. for _, n := range []*fakeNode{n1, n2} { if got, want := len(n.cmds), 1; got != want { t.Fatalf("len(n.cmds) = %d, want %d", got, want) } c := n.cmds[0] if got, want := c.cmd, fmt.Sprintf("ctr --namespace=k8s.io images tag --force %s %s", oldTag, newTag); got != want { t.Fatalf("c.cmd = %s, want %s", got, want) } } } func TestFailWithNoNodes(t *testing.T) { ctx := context.Background() img, err := random.Image(1024, 1) if err != nil { panic(err) } oldTag, err := name.NewTag("kind.local/test:test") if err != nil { t.Fatalf("name.NewTag() = %v", err) } newTag, err := name.NewTag("kind.local/test:new") if err != nil { t.Fatalf("name.NewTag() = %v", err) } GetProvider = func() provider { return &fakeProvider{} } if err := Write(ctx, newTag, img); err == nil { t.Fatal("Write() = nil, wanted an error") } if err := Tag(ctx, oldTag, newTag); err == nil { t.Fatal("Tag() = nil, wanted an error") } } func TestFailCommands(t *testing.T) { ctx := context.Background() img, err := random.Image(1024, 1) if err != nil { panic(err) } oldTag, err := name.NewTag("kind.local/test:test") if err != nil { t.Fatalf("name.NewTag() = %v", err) } newTag, err := name.NewTag("kind.local/test:new") if err != nil { t.Fatalf("name.NewTag() = %v", err) } errTest := errors.New("test") n1 := &fakeNode{err: errTest} n2 := &fakeNode{err: errTest} GetProvider = func() provider { return &fakeProvider{nodes: []nodes.Node{n1, n2}} } if err := Write(ctx, newTag, img); !errors.Is(err, errTest) { t.Fatalf("Write() = %v, want %v", err, errTest) } if err := Tag(ctx, oldTag, newTag); !errors.Is(err, errTest) { t.Fatalf("Write() = %v, want %v", err, errTest) } } // fakeProvider type fakeProvider struct { nodes []nodes.Node } func (f *fakeProvider) ListInternalNodes(string) ([]nodes.Node, error) { return f.nodes, nil } type fakeNode struct { cmds []*fakeCmd err error } func (f *fakeNode) CommandContext(_ context.Context, cmd string, args ...string) exec.Cmd { command := &fakeCmd{ cmd: strings.Join(append([]string{cmd}, args...), " "), err: f.err, } f.cmds = append(f.cmds, command) return command } func (f *fakeNode) String() string { return "test" } // The following functions are not used by our code at all. func (f *fakeNode) Command(string, ...string) exec.Cmd { return nil } func (f *fakeNode) Role() (string, error) { return "", nil } func (f *fakeNode) IP() (string, string, error) { return "", "", nil } func (f *fakeNode) SerialLogs(io.Writer) error { return nil } type fakeCmd struct { cmd string err error stdin io.Reader } func (f *fakeCmd) Run() error { if f.stdin != nil { // Consume the entire stdin to move the image publish forward. io.ReadAll(f.stdin) } return f.err } func (f *fakeCmd) SetStdin(stdin io.Reader) exec.Cmd { f.stdin = stdin return f } // The following functions are not used by our code at all. func (f *fakeCmd) SetEnv(...string) exec.Cmd { return f } func (f *fakeCmd) SetStdout(io.Writer) exec.Cmd { return f } func (f *fakeCmd) SetStderr(io.Writer) exec.Cmd { return f } ================================================ FILE: pkg/publish/kind.go ================================================ // Copyright 2020 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package publish import ( "context" "fmt" "log" "os" "strings" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/ko/pkg/build" "github.com/google/ko/pkg/publish/kind" ) const ( // KindDomain is a sentinel "registry" that represents side-loading images into kind nodes. KindDomain = "kind.local" ) type kindPublisher struct { base string namer Namer tags []string } // NewKindPublisher returns a new publish.Interface that loads images into kind nodes. func NewKindPublisher(base string, namer Namer, tags []string) Interface { return &kindPublisher{ base: base, namer: namer, tags: tags, } } // Publish implements publish.Interface. func (t *kindPublisher) Publish(ctx context.Context, br build.Result, s string) (name.Reference, error) { s = strings.TrimPrefix(s, build.StrictScheme) // https://github.com/google/go-containerregistry/issues/212 s = strings.ToLower(s) // There's no way to write an index to a kind, so attempt to downcast it to an image. var img v1.Image switch i := br.(type) { case v1.Image: img = i case v1.ImageIndex: im, err := i.IndexManifest() if err != nil { return nil, err } goos, goarch := os.Getenv("GOOS"), os.Getenv("GOARCH") if goos == "" { goos = "linux" } if goarch == "" { goarch = "amd64" } for _, manifest := range im.Manifests { if manifest.Platform == nil { continue } if manifest.Platform.OS != goos { continue } if manifest.Platform.Architecture != goarch { continue } img, err = i.Image(manifest.Digest) if err != nil { return nil, err } break } if img == nil { return nil, fmt.Errorf("failed to find %s/%s image in index for image: %v", goos, goarch, s) } default: return nil, fmt.Errorf("failed to interpret %s result as image: %v", s, br) } h, err := img.Digest() if err != nil { return nil, err } digestTag, err := name.NewTag(fmt.Sprintf("%s:%s", t.namer(t.base, s), h.Hex)) if err != nil { return nil, err } log.Printf("Loading %v", digestTag) if err := kind.Write(ctx, digestTag, img); err != nil { return nil, err } log.Printf("Loaded %v", digestTag) for _, tagName := range t.tags { log.Printf("Adding tag %v", tagName) tag, err := name.NewTag(fmt.Sprintf("%s:%s", t.namer(t.base, s), tagName)) if err != nil { return nil, err } if err := kind.Tag(ctx, digestTag, tag); err != nil { return nil, err } log.Printf("Added tag %v", tagName) } return &digestTag, nil } func (t *kindPublisher) Close() error { return nil } ================================================ FILE: pkg/publish/layout.go ================================================ // Copyright 2020 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package publish import ( "context" "fmt" "log" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/layout" "github.com/google/go-containerregistry/pkg/v1/types" "github.com/google/ko/pkg/build" ) type LayoutPublisher struct { p string } // NewLayout returns a new publish.Interface that saves images to an OCI Image Layout. func NewLayout(p string) Interface { return &LayoutPublisher{p} } func (l *LayoutPublisher) writeResult(br build.Result) (layout.Path, error) { p, err := layout.FromPath(l.p) if err != nil { p, err = layout.Write(l.p, empty.Index) if err != nil { return "", err } } mt, err := br.MediaType() if err != nil { return "", err } switch mt { case types.OCIImageIndex, types.DockerManifestList: idx, ok := br.(v1.ImageIndex) if !ok { return "", fmt.Errorf("failed to interpret result as index: %v", br) } if err := p.AppendIndex(idx); err != nil { return "", err } return p, nil case types.OCIManifestSchema1, types.DockerManifestSchema2: img, ok := br.(v1.Image) if !ok { return "", fmt.Errorf("failed to interpret result as image: %v", br) } if err := p.AppendImage(img); err != nil { return "", err } return p, nil default: return "", fmt.Errorf("result image media type: %s", mt) } } // Publish implements publish.Interface. func (l *LayoutPublisher) Publish(_ context.Context, br build.Result, s string) (name.Reference, error) { log.Printf("Saving %v", s) p, err := l.writeResult(br) if err != nil { return nil, err } log.Printf("Saved %v", s) h, err := br.Digest() if err != nil { return nil, err } dig, err := name.NewDigest(fmt.Sprintf("%s@%s", p, h)) if err != nil { return nil, err } return dig, nil } func (l *LayoutPublisher) Close() error { return nil } ================================================ FILE: pkg/publish/layout_test.go ================================================ // Copyright 2020 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package publish import ( "context" "os" "strings" "testing" "github.com/google/go-containerregistry/pkg/v1/random" ) func TestLayout(t *testing.T) { img, err := random.Image(1024, 1) if err != nil { t.Fatalf("random.Image() = %v", err) } importpath := "github.com/Google/go-containerregistry/cmd/crane" tmp, err := os.MkdirTemp("/tmp", "ko") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmp) lp := NewLayout(tmp) if d, err := lp.Publish(context.Background(), img, importpath); err != nil { t.Errorf("Publish() = %v", err) } else if !strings.HasPrefix(d.String(), tmp) { t.Errorf("Publish() = %v, wanted prefix %v", d, tmp) } } ================================================ FILE: pkg/publish/multi.go ================================================ // Copyright 2020 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package publish import ( "context" "errors" "github.com/google/go-containerregistry/pkg/name" "github.com/google/ko/pkg/build" ) // MultiPublisher creates a publisher that publishes to all // the provided publishers, similar to the Unix tee(1) command. // // When calling Publish, the name.Reference returned will be the return value // of the last publisher passed to MultiPublisher (last one wins). func MultiPublisher(publishers ...Interface) Interface { return &multiPublisher{publishers} } type multiPublisher struct { publishers []Interface } // Publish implements publish.Interface. func (p *multiPublisher) Publish(ctx context.Context, br build.Result, s string) (ref name.Reference, err error) { if len(p.publishers) == 0 { return nil, errors.New("MultiPublisher configured with zero publishers") } for _, pub := range p.publishers { ref, err = pub.Publish(ctx, br, s) if err != nil { return } } return } func (p *multiPublisher) Close() (err error) { for _, pub := range p.publishers { if perr := pub.Close(); perr != nil { err = perr } } return } ================================================ FILE: pkg/publish/multi_test.go ================================================ // Copyright 2020 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package publish_test import ( "context" "fmt" "os" "testing" "github.com/google/go-containerregistry/pkg/v1/random" "github.com/google/ko/pkg/publish" ) func TestMulti(t *testing.T) { img, err := random.Image(1024, 1) if err != nil { t.Fatalf("random.Image() = %v", err) } base := "blah" repoName := fmt.Sprintf("%s/%s", "example.com", base) importpath := "github.com/Google/go-containerregistry/cmd/crane" fp, err := os.CreateTemp("", "") if err != nil { t.Fatal(err) } defer fp.Close() defer os.Remove(fp.Name()) tp := publish.NewTarball(fp.Name(), repoName, md5Hash, []string{}) tmp, err := os.MkdirTemp("/tmp", "ko") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmp) lp := publish.NewLayout(tmp) p := publish.MultiPublisher(lp, tp) if _, err := p.Publish(context.Background(), img, importpath); err != nil { t.Errorf("Publish() = %v", err) } if err := p.Close(); err != nil { t.Errorf("Close() = %v", err) } } func TestMulti_Zero(t *testing.T) { img, err := random.Image(1024, 1) if err != nil { t.Fatalf("random.Image() = %v", err) } p := publish.MultiPublisher() // No publishers. if _, err := p.Publish(context.Background(), img, "foo"); err == nil { t.Errorf("Publish() got nil error") } } ================================================ FILE: pkg/publish/options.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package publish import ( "crypto/tls" "net/http" "github.com/google/go-containerregistry/pkg/authn" ) type staticKeychain struct { auth authn.Authenticator } func (s staticKeychain) Resolve(authn.Resource) (authn.Authenticator, error) { return s.auth, nil } // WithTransport is a functional option for overriding the default transport // on a default publisher. func WithTransport(t http.RoundTripper) Option { return func(i *defaultOpener) error { i.t = t return nil } } // WithUserAgent is a functional option for overriding the User-Agent // on a default publisher. func WithUserAgent(ua string) Option { return func(i *defaultOpener) error { i.userAgent = ua return nil } } // WithAuth is a functional option for overriding the default authenticator // on a default publisher. func WithAuth(a authn.Authenticator) Option { return func(i *defaultOpener) error { i.keychain = staticKeychain{a} return nil } } // WithAuthFromKeychain is a functional option for overriding the default // authenticator on a default publisher using an authn.Keychain func WithAuthFromKeychain(keys authn.Keychain) Option { return func(i *defaultOpener) error { i.keychain = keys return nil } } // WithNamer is a functional option for overriding the image naming behavior // in our default publisher. func WithNamer(n Namer) Option { return func(i *defaultOpener) error { i.namer = n return nil } } // WithTags is a functional option for overriding the image tags func WithTags(tags []string) Option { return func(i *defaultOpener) error { i.tags = tags return nil } } // WithTagOnly is a functional option for resolving images into tag-only references func WithTagOnly(tagOnly bool) Option { return func(i *defaultOpener) error { i.tagOnly = tagOnly return nil } } func Insecure(b bool) Option { return func(i *defaultOpener) error { i.insecure = b t, ok := i.t.(*http.Transport) if !ok { return nil } t = t.Clone() if t.TLSClientConfig == nil { t.TLSClientConfig = &tls.Config{} //nolint: gosec } t.TLSClientConfig.InsecureSkipVerify = b //nolint: gosec i.t = t return nil } } // WithJobs limits the number of concurrent pushes. func WithJobs(jobs int) Option { return func(i *defaultOpener) error { i.jobs = jobs return nil } } ================================================ FILE: pkg/publish/publish.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package publish import ( "context" "github.com/google/go-containerregistry/pkg/name" "github.com/google/ko/pkg/build" ) // Interface abstracts different methods for publishing images. type Interface interface { // Publish uploads the given build.Result to a registry incorporating the // provided string into the image's repository name. Returns the digest // of the published image. Publish(context.Context, build.Result, string) (name.Reference, error) // Close exists for the tarball implementation so we can // do the whole thing in one write. Close() error } ================================================ FILE: pkg/publish/recorder.go ================================================ // Copyright 2021 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package publish import ( "context" "io" "os" "strings" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/ko/pkg/build" "github.com/sigstore/cosign/v3/pkg/oci" "github.com/sigstore/cosign/v3/pkg/oci/walk" ) // recorder wraps a publisher implementation in a layer that records the published // references to a file. type recorder struct { inner Interface fileName string wc io.Writer } // recorder implements Interface var _ Interface = (*recorder)(nil) // NewRecorder wraps the provided publish.Interface in an implementation that // records publish results to a file. func NewRecorder(inner Interface, name string) (Interface, error) { return &recorder{ inner: inner, fileName: name, }, nil } // Publish implements Interface func (r *recorder) Publish(ctx context.Context, br build.Result, ref string) (name.Reference, error) { result, err := r.inner.Publish(ctx, br, ref) if err != nil { return nil, err } references := make([]string, 0, 20 /* just try to avoid resizing*/) switch t := br.(type) { case oci.SignedImageIndex: if err := walk.SignedEntity(ctx, t, func(_ context.Context, se oci.SignedEntity) error { // Both of the SignedEntity types implement Digest() h, err := se.(interface{ Digest() (v1.Hash, error) }).Digest() if err != nil { return err } references = append(references, result.Context().Digest(h.String()).String()) return nil }); err != nil { return nil, err } default: references = append(references, result.String()) } if r.wc == nil { f, err := os.OpenFile(r.fileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return nil, err } r.wc = f } if _, err := r.wc.Write([]byte(strings.Join(references, "\n") + "\n")); err != nil { return nil, err } return result, nil } // Close implements Interface func (r *recorder) Close() error { if err := r.inner.Close(); err != nil { return err } if c, ok := r.wc.(io.Closer); ok { return c.Close() } return nil } ================================================ FILE: pkg/publish/recorder_test.go ================================================ // Copyright 2021 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package publish import ( "context" "os" "path" "strings" "testing" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/random" "github.com/google/ko/pkg/build" "github.com/sigstore/cosign/v3/pkg/oci/signed" ) type cbPublish struct { cb func(context.Context, build.Result, string) (name.Reference, error) } var _ Interface = (*cbPublish)(nil) func (sp *cbPublish) Publish(ctx context.Context, br build.Result, ref string) (name.Reference, error) { return sp.cb(ctx, br, ref) } func (sp *cbPublish) Close() error { return nil } func TestRecorder(t *testing.T) { repo := name.MustParseReference("docker.io/ubuntu:latest") inner := &cbPublish{cb: func(_ context.Context, b build.Result, _ string) (name.Reference, error) { h, err := b.Digest() if err != nil { return nil, err } return repo.Context().Digest(h.String()), nil }} dir := t.TempDir() file := path.Join(dir, "testfile") recorder, err := NewRecorder(inner, file) if err != nil { t.Fatalf("NewRecorder() = %v", err) } img, err := random.Image(3, 3) if err != nil { t.Fatalf("random.Image() = %v", err) } si := signed.Image(img) index, err := random.Index(3, 3, 2) if err != nil { t.Fatalf("random.Image() = %v", err) } sii := signed.ImageIndex(index) if _, err := recorder.Publish(context.Background(), si, ""); err != nil { t.Errorf("recorder.Publish() = %v", err) } if _, err := recorder.Publish(context.Background(), si, ""); err != nil { t.Errorf("recorder.Publish() = %v", err) } if _, err := recorder.Publish(context.Background(), sii, ""); err != nil { t.Errorf("recorder.Publish() = %v", err) } if err := recorder.Close(); err != nil { t.Errorf("recorder.Close() = %v", err) } buf, err := os.ReadFile(file) if err != nil { t.Fatalf("os.ReadFile() = %v", err) } refs := strings.Split(strings.TrimSpace(string(buf)), "\n") if want, got := len(refs), 5; got != want { t.Errorf("len(refs) = %d, wanted %d", got, want) } for _, s := range refs { ref, err := name.ParseReference(s) if err != nil { t.Fatalf("name.ParseReference() = %v", err) } // Don't compare the digests, they are random. if want, got := repo.Context().String(), ref.Context().String(); want != got { t.Errorf("reference repo = %v, wanted %v", got, want) } } } ================================================ FILE: pkg/publish/shared.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package publish import ( "context" "sync" "github.com/google/go-containerregistry/pkg/name" "github.com/google/ko/pkg/build" ) // caching wraps a publisher implementation in a layer that shares publish results // for the same inputs using a simple "future" implementation. type caching struct { inner Interface m sync.Mutex results map[string]*entry } // entry holds the last image published and the result of publishing it for a // particular reference. type entry struct { br build.Result f *future } // caching implements Interface var _ Interface = (*caching)(nil) // NewCaching wraps the provided publish.Interface in an implementation that // shares publish results for a given path until the passed image object changes. func NewCaching(inner Interface) (Interface, error) { return &caching{ inner: inner, results: make(map[string]*entry), }, nil } // Publish implements Interface func (c *caching) Publish(ctx context.Context, br build.Result, ref string) (name.Reference, error) { f := func() *future { // Lock the map of futures. c.m.Lock() defer c.m.Unlock() // If a future for "ref" exists, then return it. ent, ok := c.results[ref] if ok { // If the image matches, then return the same future. if ent.br == br { return ent.f } } // Otherwise create and record a future for publishing "br" to "ref". f := newFuture(func() (name.Reference, error) { return c.inner.Publish(ctx, br, ref) }) c.results[ref] = &entry{br: br, f: f} return f }() return f.Get() } func (c *caching) Close() error { return c.inner.Close() } ================================================ FILE: pkg/publish/shared_test.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package publish import ( "context" "testing" "time" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/random" "github.com/google/ko/pkg/build" ) type slowpublish struct { sleep time.Duration } // slowpublish implements Interface var _ Interface = (*slowpublish)(nil) func (sp *slowpublish) Publish(context.Context, build.Result, string) (name.Reference, error) { time.Sleep(sp.sleep) return makeRef() } func (sp *slowpublish) Close() error { return nil } func TestCaching(t *testing.T) { duration := 100 * time.Millisecond ref := "foo" sp := &slowpublish{duration} cb, _ := NewCaching(sp) previousDigest := "not-a-digest" // Each iteration, we test that the first publish is slow and subsequent // publishs are fast and return the same reference. For each of these // iterations we use a new random image, which should invalidate the // cached reference from previous iterations. for range 3 { img, _ := random.Index(256, 8, 1) start := time.Now() ref1, err := cb.Publish(context.Background(), img, ref) if err != nil { t.Errorf("Publish() = %v", err) } end := time.Now() elapsed := end.Sub(start) if elapsed < duration { t.Errorf("Elapsed time %v, wanted >= %s", elapsed, duration) } d1 := ref1.String() if d1 == previousDigest { t.Errorf("Got same digest as previous iteration, wanted different: %v", d1) } previousDigest = d1 start = time.Now() ref2, err := cb.Publish(context.Background(), img, ref) if err != nil { t.Errorf("Publish() = %v", err) } end = time.Now() elapsed = end.Sub(start) if elapsed >= duration { t.Errorf("Elapsed time %v, wanted < %s", elapsed, duration) } d2 := ref2.String() if d1 != d2 { t.Error("Got different references, wanted same") } } } ================================================ FILE: pkg/publish/tarball.go ================================================ // Copyright 2020 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package publish import ( "context" "fmt" "log" "strings" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/tarball" "github.com/google/ko/pkg/build" ) type tar struct { file string base string namer Namer tags []string refs map[name.Reference]v1.Image } // NewTarball returns a new publish.Interface that saves images to a tarball. func NewTarball(file, base string, namer Namer, tags []string) Interface { return &tar{ file: file, base: base, namer: namer, tags: tags, refs: make(map[name.Reference]v1.Image), } } // Publish implements publish.Interface. func (t *tar) Publish(_ context.Context, br build.Result, s string) (name.Reference, error) { s = strings.TrimPrefix(s, build.StrictScheme) // https://github.com/google/go-containerregistry/issues/212 s = strings.ToLower(s) // There's no way to write an index to a tarball, so attempt to downcast it to an image. img, ok := br.(v1.Image) if !ok { return nil, fmt.Errorf("failed to interpret %s result as image: %v", s, br) } for _, tagName := range t.tags { tag, err := name.ParseReference(fmt.Sprintf("%s:%s", t.namer(t.base, s), tagName)) if err != nil { return nil, err } t.refs[tag] = img } h, err := img.Digest() if err != nil { return nil, err } if len(t.tags) == 0 { ref, err := name.ParseReference(fmt.Sprintf("%s@%s", t.namer(t.base, s), h)) if err != nil { return nil, err } t.refs[ref] = img } ref := fmt.Sprintf("%s@%s", t.namer(t.base, s), h) if len(t.tags) == 1 && t.tags[0] != latestTag { // If a single tag is explicitly set (not latest), then this // is probably a release, so include the tag in the reference. ref = fmt.Sprintf("%s:%s@%s", t.namer(t.base, s), t.tags[0], h) } dig, err := name.NewDigest(ref) if err != nil { return nil, err } return &dig, nil } func (t *tar) Close() error { log.Printf("Saving %v", t.file) if err := tarball.MultiRefWriteToFile(t.file, t.refs); err != nil { // Bad practice, but we log this here because right now we just defer the Close. log.Printf("failed to save %q: %v", t.file, err) return err } log.Printf("Saved %v", t.file) return nil } ================================================ FILE: pkg/publish/tarball_test.go ================================================ // Copyright 2020 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package publish_test import ( "context" "fmt" "os" "strings" "testing" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/random" "github.com/google/ko/pkg/publish" ) func TestTarball(t *testing.T) { img, err := random.Image(1024, 1) if err != nil { t.Fatalf("random.Image() = %v", err) } base := "blah" importpath := "github.com/Google/go-containerregistry/cmd/crane" fp, err := os.CreateTemp("", "") if err != nil { t.Fatal(err) } defer fp.Close() defer os.Remove(fp.Name()) expectedRepo := md5Hash(base, strings.ToLower(importpath)) tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", "example.com", expectedRepo)) if err != nil { t.Fatalf("NewTag() = %v", err) } repoName := fmt.Sprintf("%s/%s", "example.com", base) tagss := [][]string{{ // no tags }, { // one tag "v0.1.0", }, { // multiple tags "latest", "debug", }} for _, tags := range tagss { tp := publish.NewTarball(fp.Name(), repoName, md5Hash, tags) if d, err := tp.Publish(context.Background(), img, importpath); err != nil { t.Errorf("Publish() = %v", err) } else if !strings.HasPrefix(d.String(), tag.Repository.String()) { t.Errorf("Publish() = %v, wanted prefix %v", d, tag.Repository) } } } ================================================ FILE: pkg/resolve/doc.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package resolve defines logic for resolving K8s yaml inputs to ko. package resolve ================================================ FILE: pkg/resolve/resolve.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package resolve import ( "context" "fmt" "strings" "sync" "github.com/dprotaso/go-yit" "github.com/google/ko/pkg/build" "github.com/google/ko/pkg/publish" "go.yaml.in/yaml/v4" "golang.org/x/sync/errgroup" ) // ImageReferences resolves supported references to images within the input yaml // to published image digests. // // If a reference can be built and pushed, its yaml.Node will be mutated. func ImageReferences(ctx context.Context, docs []*yaml.Node, builder build.Interface, publisher publish.Interface) error { // First, walk the input objects and collect a list of supported references refs := make(map[string][]*yaml.Node) for _, doc := range docs { it := refsFromDoc(doc) for node, ok := it(); ok; node, ok = it() { ref := strings.TrimSpace(node.Value) if err := builder.IsSupportedReference(ref); err != nil { return fmt.Errorf("found strict reference but %s is not a valid import path: %w", ref, err) } refs[ref] = append(refs[ref], node) } } // Next, perform parallel builds for each of the supported references. var sm sync.Map var errg errgroup.Group for ref := range refs { errg.Go(func() error { img, err := builder.Build(ctx, ref) if err != nil { return err } digest, err := publisher.Publish(ctx, img, ref) if err != nil { return err } sm.Store(ref, digest.String()) return nil }) } if err := errg.Wait(); err != nil { return err } // Walk the tags and update them with their digest. for ref, nodes := range refs { digest, ok := sm.Load(ref) if !ok { return fmt.Errorf("resolved reference to %q not found", ref) } for _, node := range nodes { node.Value = digest.(string) } } return nil } func refsFromDoc(doc *yaml.Node) yit.Iterator { it := yit.FromNode(doc). RecurseNodes(). Filter(yit.StringValue) return it.Filter(yit.WithPrefix(build.StrictScheme)) } ================================================ FILE: pkg/resolve/resolve_test.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package resolve import ( "bytes" "context" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/random" "github.com/google/ko/pkg/build" kotesting "github.com/google/ko/pkg/internal/testing" "go.yaml.in/yaml/v4" ) var ( fooRef = "github.com/awesomesauce/foo" foo = mustRandom() fooHash = mustDigest(foo) barRef = "github.com/awesomesauce/bar" bar = mustRandom() barHash = mustDigest(bar) bazRef = "github.com/awesomesauce/baz" baz = mustRandom() bazHash = mustDigest(baz) testBuilder = kotesting.NewFixedBuild(map[string]build.Result{ fooRef: foo, barRef: bar, bazRef: baz, }) testHashes = map[string]v1.Hash{ fooRef: fooHash, barRef: barHash, bazRef: bazHash, } ) func TestYAMLArrays(t *testing.T) { tests := []struct { desc string refs []string hashes []v1.Hash base name.Repository }{{ desc: "singleton array", refs: []string{fooRef}, hashes: []v1.Hash{fooHash}, base: mustRepository("gcr.io/mattmoor"), }, { desc: "singleton array (different base)", refs: []string{fooRef}, hashes: []v1.Hash{fooHash}, base: mustRepository("gcr.io/jasonhall"), }, { desc: "two element array", refs: []string{fooRef, barRef}, hashes: []v1.Hash{fooHash, barHash}, base: mustRepository("gcr.io/jonjohnson"), }, { desc: "empty array", refs: []string{}, hashes: []v1.Hash{}, base: mustRepository("gcr.io/blah"), }} for _, test := range tests { t.Run(test.desc, func(t *testing.T) { inputStructured := []string{} for _, ref := range test.refs { inputStructured = append(inputStructured, build.StrictScheme+ref) } inputYAML, err := yaml.Marshal(inputStructured) if err != nil { t.Fatalf("yaml.Marshal(%v) = %v", inputStructured, err) } doc := strToYAML(t, string(inputYAML)) err = ImageReferences(context.Background(), []*yaml.Node{doc}, testBuilder, kotesting.NewFixedPublish(test.base, testHashes)) if err != nil { t.Fatalf("ImageReferences(%v) = %v", string(inputYAML), err) } var outStructured []string if err := doc.Decode(&outStructured); err != nil { t.Errorf("doc.Decode(%v) = %v", yamlToStr(t, doc), err) } if want, got := len(inputStructured), len(outStructured); want != got { t.Errorf("ImageReferences(%v) = %v, want %v", string(inputYAML), got, want) } var expectedStructured []string for i, ref := range test.refs { hash := test.hashes[i] expectedStructured = append(expectedStructured, kotesting.ComputeDigest(test.base, ref, hash)) } if diff := cmp.Diff(expectedStructured, outStructured, cmpopts.EquateEmpty()); diff != "" { t.Errorf("ImageReferences(%v); (-want +got) = %v", string(inputYAML), diff) } }) } } func TestYAMLMaps(t *testing.T) { base := mustRepository("gcr.io/mattmoor") tests := []struct { desc string input map[string]string expected map[string]string }{{ desc: "simple value", input: map[string]string{"image": build.StrictScheme + fooRef}, expected: map[string]string{"image": kotesting.ComputeDigest(base, fooRef, fooHash)}, }, { desc: "simple key", input: map[string]string{build.StrictScheme + bazRef: "blah"}, expected: map[string]string{ kotesting.ComputeDigest(base, bazRef, bazHash): "blah", }, }, { desc: "key and value", input: map[string]string{build.StrictScheme + fooRef: build.StrictScheme + barRef}, expected: map[string]string{ kotesting.ComputeDigest(base, fooRef, fooHash): kotesting.ComputeDigest(base, barRef, barHash), }, }, { desc: "empty map", input: map[string]string{}, expected: map[string]string{}, }, { desc: "multiple values", input: map[string]string{ "arg1": build.StrictScheme + fooRef, "arg2": build.StrictScheme + barRef, }, expected: map[string]string{ "arg1": kotesting.ComputeDigest(base, fooRef, fooHash), "arg2": kotesting.ComputeDigest(base, barRef, barHash), }, }} for _, test := range tests { t.Run(test.desc, func(t *testing.T) { inputStructured := test.input inputYAML, err := yaml.Marshal(inputStructured) if err != nil { t.Fatalf("yaml.Marshal(%v) = %v", inputStructured, err) } doc := strToYAML(t, string(inputYAML)) err = ImageReferences(context.Background(), []*yaml.Node{doc}, testBuilder, kotesting.NewFixedPublish(base, testHashes)) if err != nil { t.Fatalf("ImageReferences(%v) = %v", string(inputYAML), err) } var outStructured map[string]string if err := doc.Decode(&outStructured); err != nil { t.Errorf("doc.Decode(%v) = %v", yamlToStr(t, doc), err) } if want, got := len(inputStructured), len(outStructured); want != got { t.Errorf("ImageReferences(%v) = %v, want %v", string(inputYAML), got, want) } if diff := cmp.Diff(test.expected, outStructured, cmpopts.EquateEmpty()); diff != "" { t.Errorf("ImageReferences(%v); (-want +got) = %v", string(inputYAML), diff) } }) } } // object has public fields to avoid `yaml:"foo"` annotations. type object struct { S string M map[string]object A []object P *object } func TestYAMLObject(t *testing.T) { base := mustRepository("gcr.io/bazinga") tests := []struct { desc string input *object expected *object }{{ desc: "empty object", input: &object{}, expected: &object{}, }, { desc: "string field", input: &object{S: build.StrictScheme + fooRef}, expected: &object{S: kotesting.ComputeDigest(base, fooRef, fooHash)}, }, { desc: "map field", input: &object{M: map[string]object{"blah": {S: build.StrictScheme + fooRef}}}, expected: &object{M: map[string]object{"blah": {S: kotesting.ComputeDigest(base, fooRef, fooHash)}}}, }, { desc: "array field", input: &object{A: []object{{S: build.StrictScheme + fooRef}}}, expected: &object{A: []object{{S: kotesting.ComputeDigest(base, fooRef, fooHash)}}}, }, { desc: "pointer field", input: &object{P: &object{S: build.StrictScheme + fooRef}}, expected: &object{P: &object{S: kotesting.ComputeDigest(base, fooRef, fooHash)}}, }, { desc: "deep field", input: &object{M: map[string]object{"blah": {A: []object{{P: &object{S: build.StrictScheme + fooRef}}}}}}, expected: &object{M: map[string]object{"blah": {A: []object{{P: &object{S: kotesting.ComputeDigest(base, fooRef, fooHash)}}}}}}, }} for _, test := range tests { t.Run(test.desc, func(t *testing.T) { inputStructured := test.input inputYAML, err := yaml.Marshal(inputStructured) if err != nil { t.Fatalf("yaml.Marshal(%v) = %v", inputStructured, err) } doc := strToYAML(t, string(inputYAML)) err = ImageReferences(context.Background(), []*yaml.Node{doc}, testBuilder, kotesting.NewFixedPublish(base, testHashes)) if err != nil { t.Fatalf("ImageReferences(%v) = %v", string(inputYAML), err) } var outStructured *object if err := doc.Decode(&outStructured); err != nil { t.Errorf("doc.Decode(%v) = %v", yamlToStr(t, doc), err) } if diff := cmp.Diff(test.expected, outStructured, cmpopts.EquateEmpty()); diff != "" { t.Errorf("ImageReferences(%v); (-want +got) = %v", string(inputYAML), diff) } }) } } func TestStrict(t *testing.T) { refs := []string{ fooRef, barRef, } buf := bytes.NewBuffer(nil) encoder := yaml.NewEncoder(buf) for _, input := range refs { if err := encoder.Encode(build.StrictScheme + input); err != nil { t.Fatalf("Encode(%v) = %v", input, err) } } base := mustRepository("gcr.io/multi-pass") doc := strToYAML(t, buf.String()) err := ImageReferences(context.Background(), []*yaml.Node{doc}, testBuilder, kotesting.NewFixedPublish(base, testHashes)) if err != nil { t.Fatalf("ImageReferences: %v", err) } t.Log(yamlToStr(t, doc)) } func TestIsSupportedReferenceError(t *testing.T) { ref := build.StrictScheme + fooRef buf := bytes.NewBuffer(nil) encoder := yaml.NewEncoder(buf) if err := encoder.Encode(ref); err != nil { t.Fatalf("Encode(%v) = %v", ref, err) } base := mustRepository("gcr.io/multi-pass") doc := strToYAML(t, buf.String()) noMatchBuilder := kotesting.NewFixedBuild(nil) err := ImageReferences(context.Background(), []*yaml.Node{doc}, noMatchBuilder, kotesting.NewFixedPublish(base, testHashes)) if err == nil { t.Fatal("ImageReferences should err, got nil") } } func mustRandom() build.Result { img, err := random.Index(1024, 5, 1) if err != nil { panic(err) } return img } func mustRepository(s string) name.Repository { n, err := name.NewRepository(s) if err != nil { panic(err) } return n } func mustDigest(img build.Result) v1.Hash { d, err := img.Digest() if err != nil { panic(err) } return d } ================================================ FILE: pkg/resolve/selector.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package resolve import ( "errors" y "github.com/dprotaso/go-yit" "go.yaml.in/yaml/v4" "k8s.io/apimachinery/pkg/labels" ) // MatchesSelector returns true if the Kubernetes object (represented as a // yaml.Node) matches the selector. An error is returned if the yaml.Node is // not an K8s object or list. // // If the document is a list, the yaml.Node will be mutated to only include // items that match the selector. func MatchesSelector(doc *yaml.Node, selector labels.Selector) (bool, error) { // ignore the document node if doc.Kind == yaml.DocumentNode && len(doc.Content) > 0 { doc = doc.Content[0] } kind, err := docKind(doc) if err != nil { return false, err } if kind == "" { return false, nil } if kind == "List" { return listMatchesSelector(doc, selector) } return objMatchesSelector(doc, selector), nil } func docKind(doc *yaml.Node) (string, error) { // Null nodes will fail the check below, so simply ignore them. if doc.Tag == "!!null" { return "", nil } it := y.FromNode(doc). Filter(y.Intersect( y.WithKind(yaml.MappingNode), y.WithMapKeyValue( y.WithStringValue("apiVersion"), y.StringValue, ), )). ValuesForMap( // Key Predicate y.WithStringValue("kind"), // Value Predicate y.StringValue, ) node, ok := it() if !ok { return "", errors.New("yaml doesn't represent a k8s object") } return node.Value, nil } func objMatchesSelector(doc *yaml.Node, selector labels.Selector) bool { it := y.FromNode(doc). Filter(y.WithKind(yaml.MappingNode)). // Return the metadata map ValuesForMap( // Key Predicate y.WithStringValue("metadata"), // Value Predicate y.WithKind(yaml.MappingNode), ). // Return the labels map ValuesForMap( // Key Predicate y.WithStringValue("labels"), // Value Predicate y.WithKind(yaml.MappingNode), ) node, ok := it() // Object has no metadata.labels, verify matching against an empty set. if !ok { node = emptyMapNode } return selector.Matches(labelsNode{node}) } func listMatchesSelector(doc *yaml.Node, selector labels.Selector) (bool, error) { it := y.FromNode(doc).ValuesForMap( // Key Predicate y.WithStringValue("items"), // Value Predicate y.WithKind(yaml.SequenceNode), ) node, ok := it() // We don't have a k8s list if !ok { return false, errors.New("yaml is not a valid k8s list") } var matches []*yaml.Node for _, content := range node.Content { if _, err := docKind(content); err != nil { return false, err } if objMatchesSelector(content, selector) { matches = append(matches, content) } } node.Content = matches return len(matches) != 0, nil } var emptyMapNode = &yaml.Node{ Kind: yaml.MappingNode, Tag: "!!map", } type labelsNode struct { *yaml.Node } var _ labels.Labels = labelsNode{} func (n labelsNode) Get(label string) (value string) { for i := 0; i < len(n.Content); i += 2 { if n.Content[i].Value == label { return n.Content[i+1].Value } } return } func (n labelsNode) Has(label string) bool { for i := 0; i < len(n.Content); i += 2 { if n.Content[i].Value == label { return true } } return false } func (n labelsNode) Lookup(label string) (value string, exists bool) { for i := 0; i < len(n.Content); i += 2 { if n.Content[i].Value == label { return n.Content[i+1].Value, true } } return "", false } ================================================ FILE: pkg/resolve/selector_test.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package resolve import ( "testing" "github.com/google/go-cmp/cmp" "go.yaml.in/yaml/v4" "k8s.io/apimachinery/pkg/labels" ) var ( webSelector = selector(`app=web`) notWebSelector = selector(`app!=web`) nopSelector = selector(`foo!=bark`) hasSelector = selector(`app`) notHasSelector = selector(`!app`) ) const ( webPod = `apiVersion: v1 kind: Pod metadata: labels: # comments should be preserved app: web name: rss-site ` dbPod = `apiVersion: v1 kind: Pod metadata: labels: # comments should be preserved app: db name: rss-db ` podNoLabel = `apiVersion: v1 kind: Pod metadata: name: rss-site ` podList = `apiVersion: v1 kind: List metadata: resourceVersion: "" selfLink: "" items: - apiVersion: v1 kind: Pod metadata: labels: app: web name: rss-site - apiVersion: v1 kind: Pod metadata: labels: app: db name: rss-db ` webPodList = `apiVersion: v1 kind: List metadata: resourceVersion: "" selfLink: "" items: - apiVersion: v1 kind: Pod metadata: labels: app: web name: rss-site ` dbPodList = `apiVersion: v1 kind: List metadata: resourceVersion: "" selfLink: "" items: - apiVersion: v1 kind: Pod metadata: labels: app: db name: rss-db ` podListNoLabel = `apiVersion: v1 kind: List metadata: resourceVersion: "" selfLink: "" items: - apiVersion: v1 kind: Pod metadata: name: rss-site - apiVersion: v1 kind: Pod metadata: name: rss-db ` ) func TestMatchesSelector(t *testing.T) { tests := []struct { desc string input string selector labels.Selector output string matches bool }{{ desc: "single object with matching selector", input: webPod, selector: webSelector, output: webPod, matches: true, }, { desc: "single object with non-matching selector", input: webPod, selector: notWebSelector, matches: false, }, { desc: "single object with noop selector", input: dbPod, selector: nopSelector, output: dbPod, matches: true, }, { desc: "single object with has selector", input: webPod, selector: hasSelector, output: webPod, matches: true, }, { desc: "single object with not-has selector", input: webPod, selector: notHasSelector, matches: false, }, { desc: "single non-labeled object with has selector", input: podNoLabel, selector: hasSelector, matches: false, }, { desc: "single non-labeled object with not-has selector", input: podNoLabel, selector: notHasSelector, output: podNoLabel, matches: true, }, { desc: "selector matching elements of list object", input: podList, selector: webSelector, output: webPodList, matches: true, }, { desc: "selector matching other elements of list object", input: podList, selector: notWebSelector, output: dbPodList, matches: true, }, { desc: "has selector matching no non-labeled element of list object", input: podListNoLabel, selector: hasSelector, matches: false, }, { desc: "not-has selector matching all non-labeled elements of list object", input: podListNoLabel, selector: notHasSelector, output: podListNoLabel, matches: true, }, { desc: "selector matching all elements of list object", input: podList, selector: labels.Everything(), output: podList, matches: true, }, { desc: "selector matching no element of list object", input: podList, selector: labels.Nothing(), matches: false, }} for _, test := range tests { t.Run(test.desc, func(t *testing.T) { doc := strToYAML(t, test.input) matches, err := MatchesSelector(doc, test.selector) if err != nil { t.Fatalf("unexpected error: %v", err) } if matches != test.matches { t.Errorf("unexpected result: got %v - want %v", matches, test.matches) } // assert doc is mutated correctly if test.output != "" { // Normalize whitespace formatting output := normalizeYAML(t, test.output) if diff := cmp.Diff(output, yamlToStr(t, doc)); diff != "" { t.Errorf("unexpected diff (-want, +got) %v", diff) } } }) } } func TestSelectorFailure(t *testing.T) { tests := []struct { desc string input string }{ { desc: "not an object", input: "image: some.go/package", }, { desc: "not an object in a list", input: `apiVersion: v1 kind: List metadata: resourceVersion: "" selfLink: "" items: - blah: ha `, }, { desc: "not a valid list", input: `apiVersion: v1 kind: List `, }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { _, err := MatchesSelector(strToYAML(t, test.input), labels.Everything()) if err == nil { t.Error("expected error") } }) } } func selector(s string) labels.Selector { selector, err := labels.Parse(s) if err != nil { panic("unable to parse selector " + s) } return selector } func normalizeYAML(t *testing.T, yuml string) string { t.Helper() return yamlToStr(t, strToYAML(t, yuml)) } func yamlToStr(t *testing.T, node *yaml.Node) string { result, err := yaml.Marshal(node) if err != nil { t.Fatalf("error marshalling yaml: %v", err) } return string(result) } func strToYAML(t *testing.T, yuml string) *yaml.Node { t.Helper() var node yaml.Node if err := yaml.Unmarshal([]byte(yuml), &node); err != nil { t.Fatalf("error unmarshalling yaml: %v\n%v", err, yuml) } return &node } func TestLabelsNode(t *testing.T) { tests := []struct { desc string labels map[string]string label string value string exists bool }{{ desc: "label exists", labels: map[string]string{"app": "web", "version": "v1"}, label: "app", value: "web", exists: true, }, { desc: "label does not exist", labels: map[string]string{"app": "web", "version": "v1"}, label: "tier", value: "", exists: false, }, { desc: "empty labels", labels: map[string]string{}, label: "app", value: "", exists: false, }, { desc: "label with empty value", labels: map[string]string{"app": "", "version": "v1"}, label: "app", value: "", exists: true, }, { desc: "multiple labels with special characters", labels: map[string]string{"app.io/name": "web", "version": "v1", "owner": "team-a"}, label: "app.io/name", value: "web", exists: true, }, { desc: "label with numeric value", labels: map[string]string{"replicas": "3", "port": "8080"}, label: "replicas", value: "3", exists: true, }, { desc: "case sensitive label lookup", labels: map[string]string{"App": "web", "app": "database"}, label: "app", value: "database", exists: true, }} for _, test := range tests { t.Run(test.desc, func(t *testing.T) { // Create a mapping node directly node := &yaml.Node{ Kind: yaml.MappingNode, } // Add key-value pairs to the node for key, value := range test.labels { keyNode := &yaml.Node{ Kind: yaml.ScalarNode, Value: key, } valueNode := &yaml.Node{ Kind: yaml.ScalarNode, Value: value, } node.Content = append(node.Content, keyNode, valueNode) } ln := labelsNode{node} // Test Get method gotValue := ln.Get(test.label) if gotValue != test.value { t.Errorf("Get(%q) = %q, want %q", test.label, gotValue, test.value) } // Test Has method gotExists := ln.Has(test.label) if gotExists != test.exists { t.Errorf("Has(%q) = %v, want %v", test.label, gotExists, test.exists) } // Test Lookup method gotLookupValue, gotLookupExists := ln.Lookup(test.label) if gotLookupValue != test.value { t.Errorf("Lookup(%q) value = %q, want %q", test.label, gotLookupValue, test.value) } if gotLookupExists != test.exists { t.Errorf("Lookup(%q) exists = %v, want %v", test.label, gotLookupExists, test.exists) } }) } } ================================================ FILE: test/build-configs/.ko.yaml ================================================ # Copyright 2021 ko Build Authors All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. builds: - id: foo-app dir: ./foo main: ./cmd flags: -v -v # build.Config parser must handle shorthand syntax - id: bar-app dir: ./bar main: ./cmd - id: toolexec dir: ./toolexec main: ./cmd flags: - -toolexec - go - id: caps-app dir: ./caps main: ./cmd ================================================ FILE: test/build-configs/bar/cmd/main.go ================================================ // Copyright 2021 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import "fmt" func main() { fmt.Println("bar") } ================================================ FILE: test/build-configs/bar/go.mod ================================================ // Copyright 2021 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. module example.com/bar go 1.16 ================================================ FILE: test/build-configs/caps/cmd/main.go ================================================ // Copyright 2024 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "io/ioutil" "os" "strconv" "strings" ) func permittedCaps() (uint64, error) { data, err := ioutil.ReadFile("/proc/self/status") if err != nil { return 0, err } const prefix = "CapPrm:" for _, line := range strings.Split(string(data), "\n") { if strings.HasPrefix(line, prefix) { return strconv.ParseUint(strings.TrimSpace(line[len(prefix):]), 16, 64) } } return 0, fmt.Errorf("didn't find %#v in /proc/self/status", prefix) } func main() { caps, err := permittedCaps() if err != nil { fmt.Println(err) os.Exit(1) } if caps == 0 { fmt.Println("No capabilities") } else { fmt.Printf("Has capabilities (%x)\n", caps) } } ================================================ FILE: test/build-configs/caps/go.mod ================================================ // Copyright 2024 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. module example.com/caps go 1.16 ================================================ FILE: test/build-configs/caps.ko.yaml ================================================ # Copyright 2024 ko Build Authors All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. builds: - id: caps-app-with-caps dir: ./caps main: ./cmd linux_capabilities: net_bind_service chown ================================================ FILE: test/build-configs/foo/cmd/main.go ================================================ // Copyright 2021 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import "fmt" func main() { fmt.Println("foo") } ================================================ FILE: test/build-configs/foo/go.mod ================================================ // Copyright 2021 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. module example.com/foo go 1.16 ================================================ FILE: test/build-configs/toolexec/cmd/main.go ================================================ // Copyright 2022 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import "fmt" func main() { fmt.Println("toolexec") } ================================================ FILE: test/build-configs/toolexec/go.mod ================================================ // Copyright 2022 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. module example.com/toolexec go 1.16 ================================================ FILE: test/kodata/a ================================================ hello ================================================ FILE: test/kodata/kenobi ================================================ Hello there ================================================ FILE: test/kodata/subdir/file.txt ================================================ subdir file content ================================================ FILE: test/main.go ================================================ // Copyright 2018 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "flag" "fmt" "log" "os" "os/signal" "path/filepath" "runtime" "syscall" "time" // Give this an interesting import _ "github.com/google/go-containerregistry/pkg/registry" ) var ( f = flag.String("f", "kenobi", "File in kodata to print") wait = flag.Bool("wait", false, "Whether to wait for SIGTERM") ) // This is defined so we can test build-time variable setting using ldflags. var version = "default" func main() { flag.Parse() log.Println("version =", version) if runtime.GOOS == "windows" { // Go seems to not load location data from Windows, so timezone // conversion fails unless tzdata is embedded in the Go program // with the go build tag `timetzdata`. Since we want to test // loading tzdata provided by the base image below, we'll just // skip that for Windows here. // See https://github.com/ko-build/ko/issues/739 log.Println("skipping timezone conversion on Windows") } else { // Exercise timezone conversions, which demonstrates tzdata is provided // by the base image. now := time.Now() loc, _ := time.LoadLocation("UTC") fmt.Printf("UTC Time: %s\n", now.In(loc)) loc, _ = time.LoadLocation("America/New_York") fmt.Printf("New York Time: %s\n", now.In(loc)) } dp := os.Getenv("KO_DATA_PATH") file := filepath.Join(dp, *f) bytes, err := os.ReadFile(file) if err != nil { log.Fatalf("Error reading %q: %v", file, err) } fmt.Println(string(bytes)) // Cause the pod to "hang" to allow us to check for a readiness state. if *wait { sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGTERM) <-sigs } } ================================================ FILE: test/test.yaml ================================================ # Copyright 2018 ko Build Authors All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. apiVersion: v1 kind: Pod metadata: name: kodata spec: containers: - name: obiwan image: ko://github.com/google/ko/test args: - --wait=true restartPolicy: Never