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
[](https://github.com/ko-build/ko/actions?query=workflow%3ABuild)
[](https://godoc.org/github.com/google/ko)
[](https://goreportcard.com/report/ko-build/ko)
[](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.

`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.
================================================
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
]