diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b811a289b..e3de8457b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -236,7 +236,7 @@ jobs: go-version-file: 'go.mod' - name: Setup carvel - uses: carvel-dev/setup-action@v1 + uses: carvel-dev/setup-action@v2 with: token: ${{ secrets.RELEASE_TOKEN }} only: ytt, kapp @@ -293,7 +293,7 @@ jobs: uses: imjasonh/setup-crane@v0.3 - name: Setup carvel - uses: carvel-dev/setup-action@v1 + uses: carvel-dev/setup-action@v2 with: token: ${{ secrets.RELEASE_TOKEN }} only: ytt, kapp @@ -450,7 +450,7 @@ jobs: uses: imjasonh/setup-crane@v0.3 - name: Setup carvel - uses: carvel-dev/setup-action@v1 + uses: carvel-dev/setup-action@v2 with: token: ${{ secrets.RELEASE_TOKEN }} only: ytt diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 8fc21be6e..0154de525 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -16,8 +16,7 @@ You'll also need: ```bash kubectl cluster-info # ensure you have access to a cluster -docker login # must be writable and publicly accessible; e.g., your Docker Hub username or gcr.io/ -./hack/apply.sh +./hack/local.sh --help #this will provide all options for building/deploying kpack ``` #### When using private registries @@ -85,7 +84,7 @@ make unit * 🍿 These tests can take anywhere from 20-30 minutes depending on your setup ```bash -IMAGE_REGISTRY=gcr.io/ \ + IMAGE_REGISTRY=gcr.io/ \ IMAGE_REGISTRY_USERNAME=_json_key \ IMAGE_REGISTRY_PASSWORD=$(cat gcp.json) \ make e2e diff --git a/cmd/completion/main.go b/cmd/completion/main.go index 2e5f32429..f0feec6d2 100644 --- a/cmd/completion/main.go +++ b/cmd/completion/main.go @@ -9,6 +9,8 @@ import ( "path/filepath" "strings" + "github.com/sigstore/cosign/v2/pkg/oci/remote" + "github.com/BurntSushi/toml" "github.com/buildpacks/lifecycle/platform/files" "github.com/google/go-containerregistry/pkg/authn" @@ -154,7 +156,7 @@ func main() { func signImage(report files.Report, keychain authn.Keychain) error { if hasCosign() { - cosignSigner := cosign.NewImageSigner(logger, sign.SignCmd) + cosignSigner := cosign.NewImageSigner(sign.SignCmd, remote.SignatureTag) annotations, err := mapKeyValueArgs(cosignAnnotations) if err != nil { diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 96b723a8b..f1ed25948 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -9,7 +9,13 @@ import ( "os" "time" - "github.com/Masterminds/semver/v3" + "github.com/pivotal/kpack/pkg/secret" + + "github.com/sigstore/cosign/v2/cmd/cosign/cli/sign" + ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote" + + "github.com/pivotal/kpack/pkg/cosign" + "go.uber.org/zap" "golang.org/x/sync/errgroup" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -198,19 +204,22 @@ func main() { KpackVersion: cmd.Identifer, LifecycleProvider: lifecycleProvider, KeychainFactory: keychainFactory, + ImageSigner: cosign.NewImageSigner(sign.SignCmd, ociremote.SignatureTag), } podProgressLogger := &buildchange.ProgressLogger{ K8sClient: k8sClient, } + secretFetcher := &secret.Fetcher{Client: k8sClient} + buildController := build.NewController(ctx, options, k8sClient, buildInformer, podInformer, metadataRetriever, buildpodGenerator, podProgressLogger, keychainFactory, *injectedSidecarSupport) imageController := image.NewController(ctx, options, k8sClient, imageInformer, buildInformer, duckBuilderInformer, sourceResolverInformer, pvcInformer, *enablePriorityClasses) sourceResolverController := sourceresolver.NewController(ctx, options, sourceResolverInformer, gitResolver, blobResolver, registryResolver) - builderController, builderResync := builder.NewController(ctx, options, builderInformer, builderCreator, keychainFactory, clusterStoreInformer, buildpackInformer, clusterBuildpackInformer, clusterStackInformer, extensionInformer, clusterExtensionInformer) + builderController, builderResync := builder.NewController(ctx, options, builderInformer, builderCreator, keychainFactory, clusterStoreInformer, buildpackInformer, clusterBuildpackInformer, clusterStackInformer, extensionInformer, clusterExtensionInformer, secretFetcher) buildpackController := buildpack.NewController(ctx, options, keychainFactory, buildpackInformer, remoteStoreReader) extensionController := extension.NewController(ctx, options, keychainFactory, extensionInformer, remoteStoreReader) - clusterBuilderController, clusterBuilderResync := clusterbuilder.NewController(ctx, options, clusterBuilderInformer, builderCreator, keychainFactory, clusterStoreInformer, clusterBuildpackInformer, clusterStackInformer, clusterExtensionInformer) + clusterBuilderController, clusterBuilderResync := clusterbuilder.NewController(ctx, options, clusterBuilderInformer, builderCreator, keychainFactory, clusterStoreInformer, clusterBuildpackInformer, clusterStackInformer, clusterExtensionInformer, secretFetcher) clusterBuildpackController := clusterbuildpack.NewController(ctx, options, keychainFactory, clusterBuildpackInformer, remoteStoreReader) clusterExtensionController := clusterextension.NewController(ctx, options, keychainFactory, clusterExtensionInformer, remoteStoreReader) clusterStoreController := clusterstore.NewController(ctx, options, keychainFactory, clusterStoreInformer, remoteStoreReader) diff --git a/cmd/rebase/main.go b/cmd/rebase/main.go index bf46c5636..bf97243db 100644 --- a/cmd/rebase/main.go +++ b/cmd/rebase/main.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "flag" - "io/ioutil" "log" "os" "path/filepath" @@ -134,7 +133,7 @@ func rebase(tags []string, logger *log.Logger) error { return err } - return ioutil.WriteFile(*reportFilePath, buf.Bytes(), 0777) + return os.WriteFile(*reportFilePath, buf.Bytes(), 0777) } func logLoadingSecrets(logger *log.Logger, secretsSlices ...[]string) { diff --git a/go.mod b/go.mod index 19ad63bdd..b5c8f4c17 100644 --- a/go.mod +++ b/go.mod @@ -26,9 +26,9 @@ require ( github.com/vdemeester/k8s-pkg-credentialprovider v1.22.4 github.com/whilp/git-urls v1.0.0 go.uber.org/zap v1.26.0 - golang.org/x/crypto v0.13.0 - golang.org/x/net v0.15.0 - golang.org/x/sync v0.3.0 + golang.org/x/crypto v0.14.0 + golang.org/x/net v0.17.0 + golang.org/x/sync v0.4.0 k8s.io/api v0.27.3 k8s.io/apimachinery v0.27.3 k8s.io/client-go v0.27.3 @@ -293,8 +293,8 @@ require ( golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/oauth2 v0.11.0 // indirect - golang.org/x/sys v0.12.0 // indirect - golang.org/x/term v0.12.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/term v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.13.0 // indirect @@ -324,3 +324,5 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) + +replace github.com/AdamKorcz/go-fuzz-headers-1 => github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d diff --git a/go.sum b/go.sum index 875708d30..9bc4f767f 100644 --- a/go.sum +++ b/go.sum @@ -1374,8 +1374,8 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1482,8 +1482,8 @@ golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1517,8 +1517,8 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/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.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1609,8 +1609,8 @@ golang.org/x/sys v0.3.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.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1621,8 +1621,8 @@ golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/hack/apply.sh b/hack/apply.sh deleted file mode 100755 index 969b75026..000000000 --- a/hack/apply.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash - -cd $(dirname "${BASH_SOURCE[0]}")/.. - -if [ -z "$1" ]; then - echo "Usage: ./hack/apply.sh " - exit 0 -fi - -source hack/common.sh - -set -e - -TMP_DIR="$(mktemp -d)" - -compile $1 ${TMP_DIR}/out.yaml - -kubectl apply -f ${TMP_DIR}/out.yaml \ No newline at end of file diff --git a/hack/build.sh b/hack/build.sh new file mode 100644 index 000000000..696f4516a --- /dev/null +++ b/hack/build.sh @@ -0,0 +1,216 @@ +#!/bin/bash + +function lifecycle_image_build() { + image=$1 + go run hack/lifecycle/main.go --tag=${image} +} + +function generate_kbld_config_pack() { + path=$1 + registry=$2 + + controller_args=("--env" "BP_GO_TARGETS=./cmd/controller") + controller_args+=($buildArgs) + controller_args="${controller_args[@]}"; + + webhook_args=("--env" "BP_GO_TARGETS=./cmd/webhook") + webhook_args+=($buildArgs) + webhook_args="${webhook_args[@]}"; + + build_init_args=("--env" "BP_GO_TARGETS=./cmd/build-init") + build_init_args+=($buildArgs) + build_init_args="${build_init_args[@]}"; + + build_waiter_args=("--env" "BP_GO_TARGETS=./cmd/build-waiter") + build_waiter_args+=($buildArgs) + build_waiter_args="${build_waiter_args[@]}"; + + rebase_args=("--env" "BP_GO_TARGETS=./cmd/rebase") + rebase_args+=($buildArgs) + rebase_args="${rebase_args[@]}"; + + completion_args=("--env" "BP_GO_TARGETS=./cmd/completion") + completion_args+=($buildArgs) + completion_args="${completion_args[@]}"; + + cat < $path + apiVersion: kbld.k14s.io/v1alpha1 + kind: Config + sources: + - image: controller + path: . + pack: + build: + builder: paketobuildpacks/builder-jammy-tiny + rawOptions: [${controller_args// /,}] + - image: webhook + path: . + pack: + build: + builder: paketobuildpacks/builder-jammy-tiny + rawOptions: [${webhook_args// /,}] + - image: build-init + path: . + pack: + build: + builder: paketobuildpacks/builder-jammy-tiny + rawOptions: [${build_init_args// /,}] + - image: build-waiter + path: . + pack: + build: + builder: paketobuildpacks/builder-jammy-tiny + rawOptions: [${build_waiter_args// /,}] + - image: rebase + path: . + pack: + build: + builder: paketobuildpacks/builder-jammy-tiny + rawOptions: [${rebase_args// /,}] + - image: completion + path: . + pack: + build: + builder: paketobuildpacks/builder-jammy-tiny + rawOptions: [${completion_args// /,}] + overrides: + - image: build-init-windows + newImage: build-init-windows + preresolved: true + - image: completion-windows + newImage: completion-windows + preresolved: true + - image: lifecycle + newImage: $lifecycle_image + destinations: + - image: controller + newImage: $controller_image + - image: webhook + newImage: $webhook_image + - image: build-init + newImage: $build_init_image + - image: build-waiter + newImage: $build_waiter_image + - image: rebase + newImage: $rebase_image + - image: completion + newImage: $completion_image +EOT +} + +function generate_kbld_config_ko() { + kbld_config_path=$1 + ko_config_path=$2 + registry=$3 + + args=("--disable-optimizations") + args+=($buildArgs) + args="${args[@]}"; + + cat < $kbld_config_path + apiVersion: kbld.k14s.io/v1alpha1 + kind: Config + sources: + - image: controller + path: cmd/controller + ko: + build: + rawOptions: [${args// /,}] + - image: webhook + path: cmd/webhook + ko: + build: + rawOptions: [${args// /,}] + - image: build-init + path: cmd/build-init + ko: + build: + rawOptions: [${args// /,}] + - image: build-waiter + path: cmd/build-waiter + ko: + build: + rawOptions: [${args// /,}] + - image: rebase + path: cmd/rebase + ko: + build: + rawOptions: [${args// /,}] + - image: completion + path: cmd/completion + ko: + build: + rawOptions: [${args// /,}] + overrides: + - image: build-init-windows + newImage: build-init-windows + preresolved: true + - image: completion-windows + newImage: completion-windows + preresolved: true + - image: lifecycle + newImage: $lifecycle_image + destinations: + - image: controller + newImage: $controller_image + - image: webhook + newImage: $webhook_image + - image: build-init + newImage: $build_init_image + - image: build-waiter + newImage: $build_waiter_image + - image: rebase + newImage: $rebase_image + - image: completion + newImage: $completion_image +EOT + + prefix="github.com/pivotal/kpack/pkg/apis/build/v1alpha2" + cat < $ko_config_path + defaultBaseImage: paketobuildpacks/run-jammy-tiny + + builds: + - id: controller + ldflags: + - -X ${prefix}.CompletionCommand=/ko-app/completion + - -X ${prefix}.PrepareCommand=/ko-app/build-init + - -X ${prefix}.RebaseCommand=/ko-app/rebase +EOT + +} + +function compile() { + type=$1 + registry=$2 + output=$3 + + # Overrides registry=$1 for Docker Hub images + # e.g. IMAGE_PREFIX=username/kpack- + IMAGE_PREFIX=${IMAGE_PREFIX:-"${registry}/"} + controller_image=${IMAGE_PREFIX}controller + webhook_image=${IMAGE_PREFIX}webhook + build_init_image=${IMAGE_PREFIX}build-init + build_waiter_image=${IMAGE_PREFIX}build-waiter + rebase_image=${IMAGE_PREFIX}rebase + completion_image=${IMAGE_PREFIX}completion + lifecycle_image=${IMAGE_PREFIX}lifecycle + + echo "Building Lifecycle" + lifecycle_image_build ${lifecycle_image} + + echo "Generating kbld config" + temp_dir=$(mktemp -d) + kbld_config_path="${temp_dir}/kbld-config" + ko_config_path="${temp_dir}/.ko.yaml" + if [ $type = "ko" ]; then + generate_kbld_config_ko $kbld_config_path $ko_config_path $registry + elif [ $type = "pack" ]; then + generate_kbld_config_pack $kbld_config_path $registry + else + echo "invalid build type, either 'pack' or 'ko' is allowed" + exit 1 + fi + + echo "Building Images" + ytt -f config | KO_CONFIG_PATH="$ko_config_path" kbld -f $kbld_config_path -f- > $output +} \ No newline at end of file diff --git a/hack/common.sh b/hack/common.sh deleted file mode 100644 index 34176d9fc..000000000 --- a/hack/common.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/bin/bash - -function pack_build() { - image=$1 - target=$2 - pack_args=${@:3} - builder="gcr.io/cf-build-service-public/ci/kpack-builder" # builder used ci - - pack build ${image} --builder ${builder} -e BP_GO_TARGETS=${target} ${pack_args} --publish --trust-builder - - docker pull ${image} - resolved_image_name=$(docker inspect ${image} --format '{{index .RepoDigests 0}}' ) -} - -function lifecycle_image_build() { - image=$1 - go run hack/lifecycle/main.go --tag=${image} - - docker pull ${image} - resolved_image_name=$(docker inspect ${image} --format '{{index .RepoDigests 0}}' ) -} - -function compile() { - registry=$1 - output=$2 - # Overrides registry=$1 for Docker Hub images - # e.g. IMAGE_PREFIX=username/kpack- - IMAGE_PREFIX=${IMAGE_PREFIX:-"${registry}/"} - - controller_image=${IMAGE_PREFIX}controller - webhook_image=${IMAGE_PREFIX}webhook - build_init_image=${IMAGE_PREFIX}build-init - build_waiter_image=${IMAGE_PREFIX}build-waiter - rebase_image=${IMAGE_PREFIX}rebase - completion_image=${IMAGE_PREFIX}completion - lifecycle_image=${IMAGE_PREFIX}lifecycle - - pack_build ${controller_image} "./cmd/controller" - controller_image=${resolved_image_name} - - pack_build ${build_waiter_image} "./cmd/build-waiter" - build_waiter_image=${resolved_image_name} - - pack_build ${webhook_image} "./cmd/webhook" - webhook_image=${resolved_image_name} - - pack_build ${build_init_image} "./cmd/build-init" - build_init_image=${resolved_image_name} - - pack_build ${rebase_image} "./cmd/rebase" - rebase_image=${resolved_image_name} - - pack_build ${completion_image} "./cmd/completion" - completion_image=${resolved_image_name} - - lifecycle_image_build ${lifecycle_image} - lifecycle_image=${resolved_image_name} - - ytt -f config/. \ - -v controller.image=${controller_image} \ - -v webhook.image=${webhook_image} \ - -v build_init.image=${build_init_image} \ - -v build_waiter.image=${build_waiter_image} \ - -v rebase.image=${rebase_image} \ - -v completion.image=${completion_image} \ - -v lifecycle.image=${lifecycle_image} > $output -} diff --git a/hack/lifecycle/main.go b/hack/lifecycle/main.go index 2f9b67722..be65c7a50 100644 --- a/hack/lifecycle/main.go +++ b/hack/lifecycle/main.go @@ -7,7 +7,6 @@ import ( "flag" "fmt" "io" - "io/ioutil" "log" "net/http" "os" @@ -193,7 +192,7 @@ func lifecycleLayer(url, os string) (v1.Layer, error) { return nil, err } - buf, err := ioutil.ReadAll(tr) + buf, err := io.ReadAll(tr) if err != nil { return nil, err } @@ -210,7 +209,7 @@ func lifecycleLayer(url, os string) (v1.Layer, error) { } func lifecycleReader(url string) (io.ReadCloser, error) { - dir, err := ioutil.TempDir("", "lifecycle") + dir, err := os.MkdirTemp("", "lifecycle") if err != nil { return nil, err } diff --git a/hack/local.sh b/hack/local.sh new file mode 100755 index 000000000..33bfb5e73 --- /dev/null +++ b/hack/local.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash + +set -eu +set -o pipefail + +cd $(dirname "${BASH_SOURCE[0]}")/.. + +function usage() { + cat <<-USAGE +local.sh [OPTIONS] + +Builds and generates a deployment yaml for kpack. This only builds linux images. + +Prerequisites: +- pack or ko installed +- kubectl installed +- docker login to your registry + +OPTIONS + --help -h prints the command usage + --build-type build system to use. valid options are pack or ko. + --registry registry to publish built images to (e.g. gcr.io/myproject/my-repo or my-dockerhub-username) + --output filepath for generated deployment yaml. defaults to a temp file + --apply (boolean) apply deployment yaml to current kubectl context + --build-args argument to pass to build system (i.e --clear-cache for pack) + +USAGE +} + +function main() { + local buildType registry output apply buildArgs + apply="false" + + while [[ "${#}" != 0 ]]; do + case "${1}" in + --help|-h) + shift 1 + usage + exit 0 + ;; + + --build-type) + buildType=("${2}") + shift 2 + ;; + + --registry) + registry=("${2}") + shift 2 + ;; + + --output) + output=("${2}") + shift 2 + ;; + + --build-args) + buildArgs=("${2}") + shift 2 + ;; + + --apply) + apply="true" + shift 1 + ;; + + "") + # skip if the argument is empty + shift 1 + ;; + + *) + echo -e "unknown argument \"${1}\"" >&2 + exit 1 + esac + done + + if [ -z "${registry:-}" ]; then + echo "--registry is required" + usage + exit 1 + fi + + if [ -z "${buildType:-}" ]; then + echo "--buildType is required" + usage + exit 1 + fi + + if [ -z "${output:-}" ]; then + tmp_dir="$(mktemp -d)" + output=${tmp_dir}/out.yaml + echo "will write to $output" + fi + + if [ -z "${buildArgs:-}" ]; then + buildArgs="" + fi + + source hack/build.sh + compile $buildType $registry $output + + if "${apply}"; then + echo "Applying $output to cluster" + kubectl apply -f $output + fi + +} + +main "${@:-}" \ No newline at end of file diff --git a/hack/release.sh b/hack/release.sh deleted file mode 100755 index 82e232a63..000000000 --- a/hack/release.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash - -cd $(dirname "${BASH_SOURCE[0]}")/.. - -set -o errexit -set -o nounset -set -o pipefail - -if [ -z "$2" ]; then - echo "Usage: ./hack/release.sh " - exit 0 -fi - -source hack/common.sh - -# e.g. path/to/release.yml -release_yaml=$2 - -compile $1 ${release_yaml} \ No newline at end of file diff --git a/pkg/apis/build/v1alpha2/build_pod.go b/pkg/apis/build/v1alpha2/build_pod.go index 3a2ab2acb..eabf5243c 100644 --- a/pkg/apis/build/v1alpha2/build_pod.go +++ b/pkg/apis/build/v1alpha2/build_pod.go @@ -70,6 +70,17 @@ const ( PlatformEnvVarPrefix = "PLATFORM_ENV_" ) +var ( + PrepareCommand = "/cnb/process/build-init" + AnalyzeCommand = "/cnb/lifecycle/analyzer" + DetectCommand = "/cnb/lifecycle/detector" + RestoreCommand = "/cnb/lifecycle/restorer" + BuildCommand = "/cnb/lifecycle/builder" + ExportCommand = "/cnb/lifecycle/exporter" + CompletionCommand = "/cnb/process/completion" + RebaseCommand = "/cnb/process/rebase" +) + type ServiceBinding interface { ServiceName() string } @@ -267,7 +278,7 @@ func (b *Build) BuildPod(images BuildPodImages, buildContext BuildContext) (*cor analyzeContainer := corev1.Container{ Name: AnalyzeContainerName, Image: b.Spec.Builder.Image, - Command: []string{"/cnb/lifecycle/analyzer"}, + Command: []string{AnalyzeCommand}, Resources: b.Spec.Resources, Args: args( []string{ @@ -314,7 +325,7 @@ func (b *Build) BuildPod(images BuildPodImages, buildContext BuildContext) (*cor detectContainer := corev1.Container{ Name: DetectContainerName, Image: b.Spec.Builder.Image, - Command: []string{"/cnb/lifecycle/detector"}, + Command: []string{DetectCommand}, Resources: b.Spec.Resources, Args: []string{ "-app=/workspace", @@ -363,7 +374,7 @@ func (b *Build) BuildPod(images BuildPodImages, buildContext BuildContext) (*cor corev1.Container{ Name: CompletionContainerName, Image: images.completion(buildContext.os()), - Command: []string{"/cnb/process/completion"}, + Command: []string{CompletionCommand}, Env: []corev1.EnvVar{ homeEnv, {Name: CacheTagEnvVar, Value: b.Spec.RegistryCacheTag()}, @@ -398,7 +409,7 @@ func (b *Build) BuildPod(images BuildPodImages, buildContext BuildContext) (*cor corev1.Container{ Name: PrepareContainerName, Image: images.buildInit(buildContext.os()), - Command: []string{"/cnb/process/build-init"}, + Command: []string{PrepareCommand}, Args: append(secretArgs, imagePullArgs...), Resources: b.Spec.Resources, SecurityContext: containerSecurityContext(buildContext.BuildPodBuilderConfig), @@ -467,7 +478,7 @@ func (b *Build) BuildPod(images BuildPodImages, buildContext BuildContext) (*cor corev1.Container{ Name: RestoreContainerName, Image: b.Spec.Builder.Image, - Command: []string{"/cnb/lifecycle/restorer"}, + Command: []string{RestoreCommand}, Resources: b.Spec.Resources, SecurityContext: containerSecurityContext(buildContext.BuildPodBuilderConfig), Args: args([]string{ @@ -496,7 +507,7 @@ func (b *Build) BuildPod(images BuildPodImages, buildContext BuildContext) (*cor corev1.Container{ Name: BuildContainerName, Image: b.Spec.Builder.Image, - Command: []string{"/cnb/lifecycle/builder"}, + Command: []string{BuildCommand}, Resources: b.Spec.Resources, SecurityContext: containerSecurityContext(buildContext.BuildPodBuilderConfig), Args: []string{ @@ -522,7 +533,7 @@ func (b *Build) BuildPod(images BuildPodImages, buildContext BuildContext) (*cor corev1.Container{ Name: ExportContainerName, Image: b.Spec.Builder.Image, - Command: []string{"/cnb/lifecycle/exporter"}, + Command: []string{ExportCommand}, Resources: b.Spec.Resources, SecurityContext: containerSecurityContext(buildContext.BuildPodBuilderConfig), Args: args( @@ -926,7 +937,7 @@ func (b *Build) rebasePod(buildContext BuildContext, images BuildPodImages) (*co { Name: CompletionContainerName, Image: images.completion(buildContext.os()), - Command: []string{"/cnb/process/completion"}, + Command: []string{CompletionCommand}, Env: []corev1.EnvVar{ {Name: CacheTagEnvVar, Value: b.Spec.RegistryCacheTag()}, {Name: TerminationMessagePathEnvVar, Value: completionTerminationMessagePath}, @@ -953,7 +964,7 @@ func (b *Build) rebasePod(buildContext BuildContext, images BuildPodImages) (*co { Name: RebaseContainerName, Image: images.RebaseImage, - Command: []string{"/cnb/process/rebase"}, + Command: []string{RebaseCommand}, Resources: b.Spec.Resources, SecurityContext: containerSecurityContext(buildContext.BuildPodBuilderConfig), Args: args(a( diff --git a/pkg/apis/build/v1alpha2/builder_lifecycle.go b/pkg/apis/build/v1alpha2/builder_lifecycle.go index d351d654b..51dc255d4 100644 --- a/pkg/apis/build/v1alpha2/builder_lifecycle.go +++ b/pkg/apis/build/v1alpha2/builder_lifecycle.go @@ -18,6 +18,7 @@ type BuilderRecord struct { ObservedStoreGeneration int64 ObservedStackGeneration int64 OS string + SignaturePaths []CosignSignature } func (bs *BuilderStatus) BuilderRecord(record BuilderRecord) { @@ -37,10 +38,11 @@ func (bs *BuilderStatus) BuilderRecord(record BuilderRecord) { bs.ObservedStoreGeneration = record.ObservedStoreGeneration bs.ObservedStackGeneration = record.ObservedStackGeneration bs.OS = record.OS + bs.SignaturePaths = record.SignaturePaths } -func (cb *BuilderStatus) ErrorCreate(err error) { - cb.Status = corev1alpha1.Status{ +func (bs *BuilderStatus) ErrorCreate(err error) { + bs.Status = corev1alpha1.Status{ Conditions: corev1alpha1.Conditions{ { Type: corev1alpha1.ConditionReady, diff --git a/pkg/apis/build/v1alpha2/builder_types.go b/pkg/apis/build/v1alpha2/builder_types.go index e39d2c836..6376649cb 100644 --- a/pkg/apis/build/v1alpha2/builder_types.go +++ b/pkg/apis/build/v1alpha2/builder_types.go @@ -56,6 +56,12 @@ type NamespacedBuilderSpec struct { BackwardsCompatibleServiceAccount string `json:"serviceAccount,omitempty"` } +// +k8s:openapi-gen=true +type CosignSignature struct { + SigningSecret string `json:"signingSecret"` + TargetDigest string `json:"targetDigest"` +} + // +k8s:openapi-gen=true type BuilderStatus struct { corev1alpha1.Status `json:",inline"` @@ -68,6 +74,7 @@ type BuilderStatus struct { ObservedStackGeneration int64 `json:"observedStackGeneration,omitempty"` ObservedStoreGeneration int64 `json:"observedStoreGeneration,omitempty"` OS string `json:"os,omitempty"` + SignaturePaths []CosignSignature `json:"signaturePaths,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/blob/fetch.go b/pkg/blob/fetch.go index 19c86572c..3f2c0add5 100644 --- a/pkg/blob/fetch.go +++ b/pkg/blob/fetch.go @@ -2,7 +2,6 @@ package blob import ( "io" - "io/ioutil" "log" "net/http" "net/url" @@ -74,7 +73,7 @@ func downloadBlob(blobURL string) (*os.File, error) { return nil, errors.Errorf("failed to get blob %s", blobURL) } - file, err := ioutil.TempFile("", "") + file, err := os.CreateTemp("", "") if err != nil { return nil, err } diff --git a/pkg/blob/fetch_test.go b/pkg/blob/fetch_test.go index 2527b7211..2688babf2 100644 --- a/pkg/blob/fetch_test.go +++ b/pkg/blob/fetch_test.go @@ -3,7 +3,6 @@ package blob_test import ( "bytes" "fmt" - "io/ioutil" "log" "net/http" "net/http/httptest" @@ -35,7 +34,7 @@ func testBlobFetcher(t *testing.T, when spec.G, it spec.S) { it.Before(func() { var err error - dir, err = ioutil.TempDir("", "fetch_test") + dir, err = os.MkdirTemp("", "fetch_test") require.NoError(t, err) }) @@ -49,21 +48,21 @@ func testBlobFetcher(t *testing.T, when spec.G, it spec.S) { err := fetcher.Fetch(dir, fmt.Sprintf("%s/%s", server.URL, testFile), 0) require.NoError(t, err) - files, err := ioutil.ReadDir(dir) + files, err := os.ReadDir(dir) require.NoError(t, err) require.Len(t, files, 1) testDir := files[0] require.Equal(t, "testdir", testDir.Name()) require.True(t, testDir.IsDir()) - files, err = ioutil.ReadDir(filepath.Join(dir, testDir.Name())) + files, err = os.ReadDir(filepath.Join(dir, testDir.Name())) require.NoError(t, err) require.Len(t, files, 1) testFile := files[0] require.Equal(t, "testfile", testFile.Name()) require.False(t, testFile.IsDir()) - file, err := ioutil.ReadFile(filepath.Join(dir, testDir.Name(), testFile.Name())) + file, err := os.ReadFile(filepath.Join(dir, testDir.Name(), testFile.Name())) require.NoError(t, err) require.Equal(t, "test file contents", string(file)) @@ -78,14 +77,15 @@ func testBlobFetcher(t *testing.T, when spec.G, it spec.S) { err := fetcher.Fetch(dir, fmt.Sprintf("%s/%s", server.URL, "fat-zip.zip"), 0) require.NoError(t, err) - files, err := ioutil.ReadDir(dir) + files, err := os.ReadDir(dir) require.NoError(t, err) require.Len(t, files, 1) testFile := files[0] require.Equal(t, "some-file.txt", testFile.Name()) - - require.Equal(t, os.FileMode(0777).String(), testFile.Mode().String()) + info, err := testFile.Info() + require.NoError(t, err) + require.Equal(t, os.FileMode(0777).String(), info.Mode().String()) require.Contains(t, output.String(), "Successfully downloaded") }) @@ -94,7 +94,7 @@ func testBlobFetcher(t *testing.T, when spec.G, it spec.S) { err := fetcher.Fetch(dir, fmt.Sprintf("%s/%s", server.URL, "test-exe.tar"), 0) require.NoError(t, err) - files, err := ioutil.ReadDir(dir) + files, err := os.ReadDir(dir) require.NoError(t, err) require.Len(t, files, 1) @@ -102,7 +102,7 @@ func testBlobFetcher(t *testing.T, when spec.G, it spec.S) { require.Equal(t, "test-exe", testDir.Name()) require.True(t, testDir.IsDir()) - files, err = ioutil.ReadDir(filepath.Join(dir, testDir.Name())) + files, err = os.ReadDir(filepath.Join(dir, testDir.Name())) require.NoError(t, err) require.Len(t, files, 1) @@ -120,7 +120,7 @@ func testBlobFetcher(t *testing.T, when spec.G, it spec.S) { err := fetcher.Fetch(dir, fmt.Sprintf("%s/%s", server.URL, archiveFile), 1) require.NoError(t, err) - files, err := ioutil.ReadDir(dir) + files, err := os.ReadDir(dir) require.NoError(t, err) require.Len(t, files, 1) diff --git a/pkg/cnb/build_metadata.go b/pkg/cnb/build_metadata.go index 7b7e52305..44cd4d2c3 100644 --- a/pkg/cnb/build_metadata.go +++ b/pkg/cnb/build_metadata.go @@ -5,7 +5,7 @@ import ( "compress/gzip" "encoding/base64" "encoding/json" - "io/ioutil" + "io" lifecyclebuildpack "github.com/buildpacks/lifecycle/buildpack" "github.com/buildpacks/lifecycle/platform" @@ -176,7 +176,7 @@ func DecompressBuildMetadata(compressedMetadata string) (*BuildMetadata, error) return nil, err } defer zr.Close() - data, err := ioutil.ReadAll(zr) + data, err := io.ReadAll(zr) if err != nil { return nil, err } diff --git a/pkg/cnb/create_builder.go b/pkg/cnb/create_builder.go index 3e8f1da5d..f3eb4dc09 100644 --- a/pkg/cnb/create_builder.go +++ b/pkg/cnb/create_builder.go @@ -6,6 +6,8 @@ import ( "github.com/google/go-containerregistry/pkg/authn" ggcrv1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/pivotal/kpack/pkg/cosign" + corev1 "k8s.io/api/core/v1" buildapi "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" corev1alpha1 "github.com/pivotal/kpack/pkg/apis/core/v1alpha1" @@ -26,10 +28,12 @@ type RemoteBuilderCreator struct { LifecycleProvider LifecycleProvider KpackVersion string KeychainFactory registry.KeychainFactory + ImageSigner cosign.BuilderSigner } -func (r *RemoteBuilderCreator) CreateBuilder(ctx context.Context, builderKeychain authn.Keychain, stackKeychain authn.Keychain, fetcher RemoteBuildpackFetcher, clusterStack *buildapi.ClusterStack, spec buildapi.BuilderSpec) (buildapi.BuilderRecord, error) { +func (r *RemoteBuilderCreator) CreateBuilder(ctx context.Context, builderKeychain authn.Keychain, stackKeychain authn.Keychain, fetcher RemoteBuildpackFetcher, clusterStack *buildapi.ClusterStack, spec buildapi.BuilderSpec, serviceAccountSecrets []*corev1.Secret) (buildapi.BuilderRecord, error) { buildImage, _, err := r.RegistryClient.Fetch(stackKeychain, clusterStack.Status.BuildImage.LatestImage) + if err != nil { return buildapi.BuilderRecord{}, err } @@ -96,6 +100,17 @@ func (r *RemoteBuilderCreator) CreateBuilder(ctx context.Context, builderKeychai return buildapi.BuilderRecord{}, err } + var ( + signaturePaths = make([]buildapi.CosignSignature, 0) + ) + + if len(serviceAccountSecrets) > 0 { + signaturePaths, err = r.ImageSigner.SignBuilder(ctx, identifier, serviceAccountSecrets, builderKeychain) + if err != nil { + return buildapi.BuilderRecord{}, err + } + } + builder := buildapi.BuilderRecord{ Image: identifier, Stack: corev1alpha1.BuildStack{ @@ -109,6 +124,7 @@ func (r *RemoteBuilderCreator) CreateBuilder(ctx context.Context, builderKeychai ObservedStackGeneration: clusterStack.Status.ObservedGeneration, ObservedStoreGeneration: fetcher.ClusterStoreObservedGeneration(), OS: config.OS, + SignaturePaths: signaturePaths, } return builder, nil diff --git a/pkg/cnb/create_builder_test.go b/pkg/cnb/create_builder_test.go index 64a0997a3..4ee422000 100644 --- a/pkg/cnb/create_builder_test.go +++ b/pkg/cnb/create_builder_test.go @@ -181,6 +181,12 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { KpackVersion: "v1.2.3 (git sha: abcdefg123456)", KeychainFactory: keychainFactory, LifecycleProvider: lifecycleProvider, + ImageSigner: &fakeBuilderSigner{ + signBuilder: func(ctx context.Context, s string, secrets []*corev1.Secret, keychain authn.Keychain) ([]buildapi.CosignSignature, error) { + // no-op + return nil, nil + }, + }, } addBuildpack = func(t *testing.T, id, version, homepage, api string, stacks []corev1alpha1.BuildpackStack) { @@ -664,7 +670,7 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { } it("creates a custom builder", func() { - builderRecord, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec) + builderRecord, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}) require.NoError(t, err) savedImage := assertBuilderRecord(t, builderRecord, registryClient) @@ -673,11 +679,12 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }) it("creates images deterministically ", func() { - original, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec) + original, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}) require.NoError(t, err) for i := 1; i <= 50; i++ { - other, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec) + other, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}) + require.NoError(t, err) require.Equal(t, original.Image, other.Image) @@ -726,7 +733,7 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { } addExtension(t, extensionRef.Id, extensionRef.Version, "", "0.3") - builderRecord, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec) + builderRecord, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}) if os == "windows" { assert.Error(t, err, "image extensions are not supported for Windows builds") @@ -849,7 +856,7 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { } addExtension(t, extensionRef.Id, extensionRef.Version, "", "0.1") - _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec) + _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}) require.EqualError(t, err, "validating extension some-unsupported-extension-id@v1: unsupported buildpack api: 0.1, expecting: 0.2, 0.3") }) }) @@ -877,7 +884,7 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }, } - _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec) + _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}) require.EqualError(t, err, "validating buildpack io.buildpack.unsupported.stack@v4: stack io.buildpacks.stacks.some-stack is not supported") }) @@ -901,7 +908,7 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }}, }} - _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec) + _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}) require.EqualError(t, err, "validating buildpack io.buildpack.unsupported.mixin@v4: stack missing mixin(s): something-missing-mixin, something-missing-mixin2") }) @@ -946,7 +953,7 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }}, }} - _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec) + _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}) require.Nil(t, err) }) @@ -971,7 +978,7 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }}, }} - _, err := subject.CreateBuilder(ctx, builderKeychain, nil, fetcher, stack, clusterBuilderSpec) + _, err := subject.CreateBuilder(ctx, builderKeychain, nil, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}) require.Error(t, err, "validating buildpack io.buildpack.relaxed.old.mixin@v4: stack missing mixin(s): build:common-mixin, run:common-mixin, another-common-mixin") }) @@ -994,7 +1001,7 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }}, }} - _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec) + _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}) require.EqualError(t, err, "validating buildpack io.buildpack.unsupported.buildpack.api@v4: unsupported buildpack api: 0.1, expecting: 0.2, 0.3") }) @@ -1037,7 +1044,7 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }}, }} - _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec) + _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}) require.NoError(t, err) }) }) @@ -1064,13 +1071,73 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }, } - _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec) - require.EqualError(t, err, "unsupported platform apis in kpack lifecycle: 0.1, 0.2, 0.999, expecting one of: 0.3, 0.4, 0.5, 0.6, 0.7, 0.8") + _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}) + require.EqualError(t, err, "unsupported platform apis in kpack lifecycle: 0.1, 0.2, 0.999, expecting one of: 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.10") + }) + }) + + when("signing a builder image", func() { + it("does not populate the signature paths when no secrets were present", func() { + builderRecord, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}) + require.NoError(t, err) + require.NotNil(t, builderRecord) + require.Empty(t, builderRecord.SignaturePaths) + }) + + it("returns an error if signing fails", func() { + subject.ImageSigner = &fakeBuilderSigner{ + signBuilder: func(ctx context.Context, s string, secrets []*corev1.Secret, keychain authn.Keychain) ([]buildapi.CosignSignature, error) { + return nil, fmt.Errorf("failed to sign builder") + }, + } + + fakeSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cosign-creds", + Namespace: "test-namespace", + }, + } + + _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{&fakeSecret}) + require.Error(t, err) + }) + + it("populates the signature paths when signing succeeds", func() { + fakeSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cosign-creds", + Namespace: "test-namespace", + }, + } + + subject.ImageSigner = &fakeBuilderSigner{ + signBuilder: func(ctx context.Context, s string, secrets []*corev1.Secret, keychain authn.Keychain) ([]buildapi.CosignSignature, error) { + return []buildapi.CosignSignature{ + { + SigningSecret: fmt.Sprintf("k8s://%s/%s", fakeSecret.Namespace, fakeSecret.Name), + TargetDigest: "registry.local/test-image:signature-tag", + }, + }, nil + }, + } + + builderRecord, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{&fakeSecret}) + require.NoError(t, err) + require.NotNil(t, builderRecord) + require.NotEmpty(t, builderRecord.SignaturePaths) }) }) }) } +type fakeBuilderSigner struct { + signBuilder func(context.Context, string, []*corev1.Secret, authn.Keychain) ([]buildapi.CosignSignature, error) +} + +func (s *fakeBuilderSigner) SignBuilder(ctx context.Context, imageReference string, signingSecrets []*corev1.Secret, builderKeychain authn.Keychain) ([]buildapi.CosignSignature, error) { + return s.signBuilder(ctx, imageReference, signingSecrets, builderKeychain) +} + type fakeLifecycleProvider struct { metadata LifecycleMetadata layers map[string]v1.Layer diff --git a/pkg/cnb/env_vars.go b/pkg/cnb/env_vars.go index 56fd56bc9..ce6577edb 100644 --- a/pkg/cnb/env_vars.go +++ b/pkg/cnb/env_vars.go @@ -1,7 +1,6 @@ package cnb import ( - "io/ioutil" "os" "path" @@ -17,7 +16,7 @@ func serializeEnvVars(envVars []envVariable, platformDir string) error { } for _, envVar := range envVars { - err = ioutil.WriteFile(path.Join(folder, envVar.Name), []byte(envVar.Value), os.ModePerm) + err = os.WriteFile(path.Join(folder, envVar.Name), []byte(envVar.Value), os.ModePerm) if err != nil { return err } diff --git a/pkg/cnb/platform_env_vars_setup_test.go b/pkg/cnb/platform_env_vars_setup_test.go index 81013a29b..b3e7b44dd 100644 --- a/pkg/cnb/platform_env_vars_setup_test.go +++ b/pkg/cnb/platform_env_vars_setup_test.go @@ -1,7 +1,6 @@ package cnb_test import ( - "io/ioutil" "os" "path" "testing" @@ -25,7 +24,7 @@ func testPlatformEnvVarsSetup(t *testing.T, when spec.G, it spec.S) { it.Before(func() { var err error - testVolume, err = ioutil.TempDir("", "permission") + testVolume, err = os.MkdirTemp("", "permission") require.NoError(t, err) platformEnv = map[string]string{ @@ -61,7 +60,7 @@ func testPlatformEnvVarsSetup(t *testing.T, when spec.G, it spec.S) { func checkEnvVar(t *testing.T, testVolume, key, value string) { require.FileExists(t, path.Join(testVolume, "env", key)) - buf, err := ioutil.ReadFile(path.Join(testVolume, "env", key)) + buf, err := os.ReadFile(path.Join(testVolume, "env", key)) require.NoError(t, err) require.Equal(t, value, string(buf)) } diff --git a/pkg/cnb/project_descriptor_test.go b/pkg/cnb/project_descriptor_test.go index 882491754..334ce8727 100644 --- a/pkg/cnb/project_descriptor_test.go +++ b/pkg/cnb/project_descriptor_test.go @@ -3,7 +3,6 @@ package cnb_test import ( "bytes" "fmt" - "io/ioutil" "log" "os" "path/filepath" @@ -31,9 +30,9 @@ func testProcessProjectDescriptor(t *testing.T, when spec.G, it spec.S) { var err error buf = new(bytes.Buffer) logger = log.New(buf, "", 0) - appDir, err = ioutil.TempDir("", "appDir") + appDir, err = os.MkdirTemp("", "appDir") require.NoError(t, err) - platformDir, err = ioutil.TempDir("", "platform") + platformDir, err = os.MkdirTemp("", "platform") require.NoError(t, err) projectToml = filepath.Join(appDir, "project.toml") }) @@ -45,7 +44,7 @@ func testProcessProjectDescriptor(t *testing.T, when spec.G, it spec.S) { when("unsupported project descriptor version", func() { it.Before(func() { - ioutil.WriteFile(projectToml, []byte(` + os.WriteFile(projectToml, []byte(` [_] schema-version = "0.99" `), 0644) @@ -63,7 +62,7 @@ schema-version = "0.99" when("using descriptor v1", func() { when("the descriptor has build env vars", func() { it.Before(func() { - ioutil.WriteFile(projectToml, []byte(` + os.WriteFile(projectToml, []byte(` [[build.env]] name = "keyA" value = "valueA" @@ -107,38 +106,38 @@ value = "valueAnotherC" err = os.Mkdir(filepath.Join(appDir, "secrets"), 0755) assert.Nil(t, err) - err = ioutil.WriteFile(filepath.Join(appDir, "secrets", "api_keys.json"), []byte("{}"), 0755) + err = os.WriteFile(filepath.Join(appDir, "secrets", "api_keys.json"), []byte("{}"), 0755) assert.Nil(t, err) - err = ioutil.WriteFile(filepath.Join(appDir, "secrets", "user_token"), []byte("token"), 0755) + err = os.WriteFile(filepath.Join(appDir, "secrets", "user_token"), []byte("token"), 0755) assert.Nil(t, err) err = os.Mkdir(filepath.Join(appDir, "nested"), 0755) assert.Nil(t, err) - err = ioutil.WriteFile(filepath.Join(appDir, "nested", "nested-cookie.jar"), []byte("chocolate chip"), 0755) + err = os.WriteFile(filepath.Join(appDir, "nested", "nested-cookie.jar"), []byte("chocolate chip"), 0755) assert.Nil(t, err) - err = ioutil.WriteFile(filepath.Join(appDir, "other-cookie.jar"), []byte("chocolate chip"), 0755) + err = os.WriteFile(filepath.Join(appDir, "other-cookie.jar"), []byte("chocolate chip"), 0755) assert.Nil(t, err) - err = ioutil.WriteFile(filepath.Join(appDir, "nested-cookie.jar"), []byte("chocolate chip"), 0755) + err = os.WriteFile(filepath.Join(appDir, "nested-cookie.jar"), []byte("chocolate chip"), 0755) assert.Nil(t, err) err = os.Mkdir(filepath.Join(appDir, "media"), 0755) assert.Nil(t, err) - err = ioutil.WriteFile(filepath.Join(appDir, "media", "mountain.jpg"), []byte("fake image bytes"), 0755) + err = os.WriteFile(filepath.Join(appDir, "media", "mountain.jpg"), []byte("fake image bytes"), 0755) assert.Nil(t, err) - err = ioutil.WriteFile(filepath.Join(appDir, "media", "person.png"), []byte("fake image bytes"), 0755) + err = os.WriteFile(filepath.Join(appDir, "media", "person.png"), []byte("fake image bytes"), 0755) assert.Nil(t, err) - err = ioutil.WriteFile(filepath.Join(appDir, "cookie.jar"), []byte("chocolate chip"), 0755) + err = os.WriteFile(filepath.Join(appDir, "cookie.jar"), []byte("chocolate chip"), 0755) assert.Nil(t, err) - err = ioutil.WriteFile(filepath.Join(appDir, "test.sh"), []byte("echo test"), 0755) + err = os.WriteFile(filepath.Join(appDir, "test.sh"), []byte("echo test"), 0755) assert.Nil(t, err) }) when("it has excludes", func() { it.Before(func() { - ioutil.WriteFile(projectToml, []byte(` + os.WriteFile(projectToml, []byte(` [build] exclude = ["*.sh", "secrets/", "media/metadata", "/other-cookie.jar" ,"/nested-cookie.jar"] `), 0644) @@ -159,7 +158,7 @@ exclude = ["*.sh", "secrets/", "media/metadata", "/other-cookie.jar" ,"/nested-c when("it has includes", func() { it.Before(func() { - ioutil.WriteFile(projectToml, []byte(` + os.WriteFile(projectToml, []byte(` [build] include = [ "*.jar", "media/mountain.jpg", "/media/person.png", ] `), 0644) @@ -181,7 +180,7 @@ include = [ "*.jar", "media/mountain.jpg", "/media/person.png", ] when("it has both excludes and includes", func() { it.Before(func() { - ioutil.WriteFile(projectToml, []byte(` + os.WriteFile(projectToml, []byte(` [build] include = [ "test", ] exclude = ["test", ] @@ -196,7 +195,7 @@ exclude = ["test", ] when("the descriptor has builder", func() { it.Before(func() { - ioutil.WriteFile(projectToml, []byte(` + os.WriteFile(projectToml, []byte(` [build] builder = "my-super-cool-builder" `), 0644) @@ -209,7 +208,7 @@ builder = "my-super-cool-builder" when("the descriptor has buildpacks", func() { it.Before(func() { - ioutil.WriteFile(projectToml, []byte(` + os.WriteFile(projectToml, []byte(` [[build.buildpacks]] id = "cool-buildpack" version = "v4.2" @@ -229,7 +228,7 @@ uri = "check-this-out.com" when("io.buildpacks.env.build format is used", func() { when("format is valid", func() { it.Before(func() { - ioutil.WriteFile(projectToml, []byte(` + os.WriteFile(projectToml, []byte(` [_] schema-version = "0.2" [[io.buildpacks.env.build]] @@ -257,7 +256,7 @@ value = "valueAnotherC" when("format is invalid", func() { when("'value' is invalid", func() { it.Before(func() { - ioutil.WriteFile(projectToml, []byte(` + os.WriteFile(projectToml, []byte(` [_] schema-version = "0.2" [[io.buildpacks.env.build]] @@ -272,7 +271,7 @@ value = 1 }) when("'name' is invalid", func() { it.Before(func() { - ioutil.WriteFile(projectToml, []byte(` + os.WriteFile(projectToml, []byte(` [_] schema-version = "0.2" [[io.buildpacks.env.build]] @@ -291,7 +290,7 @@ value = "ValueA" when("io.buildpacks.env format is used", func() { when("format is valid", func() { it.Before(func() { - ioutil.WriteFile(projectToml, []byte(` + os.WriteFile(projectToml, []byte(` [_] schema-version = "0.2" [[io.buildpacks.env]] @@ -318,7 +317,7 @@ value = "valueAnotherC"`), 0644) when("format is invalid", func() { when("'value' is invalid", func() { it.Before(func() { - ioutil.WriteFile(projectToml, []byte(` + os.WriteFile(projectToml, []byte(` [_] schema-version = "0.2" [[io.buildpacks.env]] @@ -333,7 +332,7 @@ value = 1 }) when("'name' is invalid", func() { it.Before(func() { - ioutil.WriteFile(projectToml, []byte(` + os.WriteFile(projectToml, []byte(` [_] schema-version = "0.2" [[io.buildpacks.env]] @@ -352,7 +351,7 @@ value = "ValueA" when("vars where set with new v0.2 project.toml", func() { it.Before(func() { - ioutil.WriteFile(projectToml, []byte(` + os.WriteFile(projectToml, []byte(` [_] schema-version = "0.2" [[io.buildpacks.build.env]] @@ -380,7 +379,7 @@ value = "valueAnotherC" when("vars where set with both versions of v0.2 project.toml", func() { it.Before(func() { - ioutil.WriteFile(projectToml, []byte(` + os.WriteFile(projectToml, []byte(` [_] schema-version = "0.2" [[io.buildpacks.env.build]] @@ -419,38 +418,38 @@ value = "newValueA" err = os.Mkdir(filepath.Join(appDir, "secrets"), 0755) assert.Nil(t, err) - err = ioutil.WriteFile(filepath.Join(appDir, "secrets", "api_keys.json"), []byte("{}"), 0755) + err = os.WriteFile(filepath.Join(appDir, "secrets", "api_keys.json"), []byte("{}"), 0755) assert.Nil(t, err) - err = ioutil.WriteFile(filepath.Join(appDir, "secrets", "user_token"), []byte("token"), 0755) + err = os.WriteFile(filepath.Join(appDir, "secrets", "user_token"), []byte("token"), 0755) assert.Nil(t, err) err = os.Mkdir(filepath.Join(appDir, "nested"), 0755) assert.Nil(t, err) - err = ioutil.WriteFile(filepath.Join(appDir, "nested", "nested-cookie.jar"), []byte("chocolate chip"), 0755) + err = os.WriteFile(filepath.Join(appDir, "nested", "nested-cookie.jar"), []byte("chocolate chip"), 0755) assert.Nil(t, err) - err = ioutil.WriteFile(filepath.Join(appDir, "other-cookie.jar"), []byte("chocolate chip"), 0755) + err = os.WriteFile(filepath.Join(appDir, "other-cookie.jar"), []byte("chocolate chip"), 0755) assert.Nil(t, err) - err = ioutil.WriteFile(filepath.Join(appDir, "nested-cookie.jar"), []byte("chocolate chip"), 0755) + err = os.WriteFile(filepath.Join(appDir, "nested-cookie.jar"), []byte("chocolate chip"), 0755) assert.Nil(t, err) err = os.Mkdir(filepath.Join(appDir, "media"), 0755) assert.Nil(t, err) - err = ioutil.WriteFile(filepath.Join(appDir, "media", "mountain.jpg"), []byte("fake image bytes"), 0755) + err = os.WriteFile(filepath.Join(appDir, "media", "mountain.jpg"), []byte("fake image bytes"), 0755) assert.Nil(t, err) - err = ioutil.WriteFile(filepath.Join(appDir, "media", "person.png"), []byte("fake image bytes"), 0755) + err = os.WriteFile(filepath.Join(appDir, "media", "person.png"), []byte("fake image bytes"), 0755) assert.Nil(t, err) - err = ioutil.WriteFile(filepath.Join(appDir, "cookie.jar"), []byte("chocolate chip"), 0755) + err = os.WriteFile(filepath.Join(appDir, "cookie.jar"), []byte("chocolate chip"), 0755) assert.Nil(t, err) - err = ioutil.WriteFile(filepath.Join(appDir, "test.sh"), []byte("echo test"), 0755) + err = os.WriteFile(filepath.Join(appDir, "test.sh"), []byte("echo test"), 0755) assert.Nil(t, err) }) when("it has excludes", func() { it.Before(func() { - ioutil.WriteFile(projectToml, []byte(` + os.WriteFile(projectToml, []byte(` [_] schema-version = "0.2" [io.buildpacks] @@ -473,7 +472,7 @@ exclude = ["*.sh", "secrets/", "media/metadata", "/other-cookie.jar" ,"/nested-c when("it has includes", func() { it.Before(func() { - ioutil.WriteFile(projectToml, []byte(` + os.WriteFile(projectToml, []byte(` [_] schema-version = "0.2" [io.buildpacks] @@ -497,7 +496,7 @@ include = [ "*.jar", "media/mountain.jpg", "/media/person.png", ] when("it has both excludes and includes", func() { it.Before(func() { - ioutil.WriteFile(projectToml, []byte(` + os.WriteFile(projectToml, []byte(` [_] schema-version = "0.2" [io.buildpacks] @@ -514,7 +513,7 @@ exclude = ["test", ] when("the descriptor has builder", func() { it.Before(func() { - ioutil.WriteFile(projectToml, []byte(` + os.WriteFile(projectToml, []byte(` [_] schema-version = "0.2" [io.buildpacks] @@ -529,7 +528,7 @@ builder = "my-super-cool-builder" when("the descriptor has buildpack groups", func() { it.Before(func() { - ioutil.WriteFile(projectToml, []byte(` + os.WriteFile(projectToml, []byte(` [_] schema-version = "0.2" [[io.buildpacks.group]] @@ -548,7 +547,7 @@ uri = "check-this-out.com" when("the descriptor path is set", func() { it.Before(func() { projectToml = filepath.Join(appDir, "some-project.toml") - err := ioutil.WriteFile(projectToml, []byte(` + err := os.WriteFile(projectToml, []byte(` [[build.env]] name = "keyA" value = "valueA" diff --git a/pkg/cosign/image_signer.go b/pkg/cosign/image_signer.go index 53b214831..f873043c2 100644 --- a/pkg/cosign/image_signer.go +++ b/pkg/cosign/image_signer.go @@ -1,37 +1,46 @@ package cosign import ( + "context" "fmt" - "log" "os" "github.com/buildpacks/lifecycle/platform/files" + "io/ioutil" + + cosignutil "github.com/pivotal/kpack/pkg/cosign/util" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" "github.com/pkg/errors" - "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" + cosignoptions "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" + cosignremote "github.com/sigstore/cosign/v2/pkg/oci/remote" + corev1 "k8s.io/api/core/v1" ) -type SignFunc func( - ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignOptions, imgs []string, -) error +type SignFunc func(*cosignoptions.RootOptions, cosignoptions.KeyOpts, cosignoptions.SignOptions, []string) error -type ImageSigner struct { - Logger *log.Logger - signFunc SignFunc +type FetchSignatureFunc func(name.Reference, ...cosignremote.Option) (name.Tag, error) + +type BuilderSigner interface { + SignBuilder(context.Context, string, []*corev1.Secret, authn.Keychain) ([]v1alpha2.CosignSignature, error) } -const ( - cosignRepositoryEnv = "COSIGN_REPOSITORY" - cosignDockerMediaTypesEnv = "COSIGN_DOCKER_MEDIA_TYPES" -) +type ImageSigner struct { + signFunc SignFunc + fetchSignatureFunc FetchSignatureFunc +} -func NewImageSigner(logger *log.Logger, signFunc SignFunc) *ImageSigner { +func NewImageSigner(signFunc SignFunc, fetchSignatureFunc FetchSignatureFunc) *ImageSigner { return &ImageSigner{ - Logger: logger, - signFunc: signFunc, + signFunc: signFunc, + fetchSignatureFunc: fetchSignatureFunc, } } -func (s *ImageSigner) Sign(ro *options.RootOptions, report files.Report, secretLocation string, annotations, cosignRepositories, cosignDockerMediaTypes map[string]interface{}) error { +func (s *ImageSigner) Sign(ro *cosignoptions.RootOptions, report files.Report, secretLocation string, annotations, cosignRepositories, cosignDockerMediaTypes map[string]interface{}) error { cosignSecrets, err := findCosignSecrets(secretLocation) if err != nil { return errors.Errorf("no keys found for cosign signing: %v\n", err) @@ -60,12 +69,12 @@ func (s *ImageSigner) Sign(ro *options.RootOptions, report files.Report, secretL return nil } -func (s *ImageSigner) sign(ro *options.RootOptions, refImage, digest, secretLocation, cosignSecret string, annotations, cosignRepositories, cosignDockerMediaTypes map[string]interface{}) error { +func (s *ImageSigner) sign(ro *cosignoptions.RootOptions, refImage, digest, secretLocation, cosignSecret string, annotations, cosignRepositories, cosignDockerMediaTypes map[string]interface{}) error { cosignKeyFile := fmt.Sprintf("%s/%s/cosign.key", secretLocation, cosignSecret) cosignPasswordFile := fmt.Sprintf("%s/%s/cosign.password", secretLocation, cosignSecret) - ko := options.KeyOpts{KeyRef: cosignKeyFile, PassFunc: func(bool) ([]byte, error) { - content, err := os.ReadFile(cosignPasswordFile) + ko := cosignoptions.KeyOpts{KeyRef: cosignKeyFile, PassFunc: func(bool) ([]byte, error) { + content, err := ioutil.ReadFile(cosignPasswordFile) // When password file is not available, default empty password is used if err != nil { return []byte(""), nil @@ -75,17 +84,17 @@ func (s *ImageSigner) sign(ro *options.RootOptions, refImage, digest, secretLoca }} if cosignRepository, ok := cosignRepositories[cosignSecret]; ok { - if err := os.Setenv(cosignRepositoryEnv, fmt.Sprintf("%s", cosignRepository)); err != nil { - return errors.Errorf("failed setting %s env variable: %v", cosignRepositoryEnv, err) + if err := os.Setenv(cosignutil.CosignRepositoryEnv, fmt.Sprintf("%s", cosignRepository)); err != nil { + return errors.Errorf("failed setting %s env variable: %v", cosignutil.CosignRepositoryEnv, err) } - defer os.Unsetenv(cosignRepositoryEnv) + defer os.Unsetenv(cosignutil.CosignRepositoryEnv) } if cosignDockerMediaType, ok := cosignDockerMediaTypes[cosignSecret]; ok { - if err := os.Setenv(cosignDockerMediaTypesEnv, fmt.Sprintf("%s", cosignDockerMediaType)); err != nil { + if err := os.Setenv(cosignutil.CosignDockerMediaTypesEnv, fmt.Sprintf("%s", cosignDockerMediaType)); err != nil { return errors.Errorf("failed setting COSIGN_DOCKER_MEDIA_TYPES env variable: %v", err) } - defer os.Unsetenv(cosignDockerMediaTypesEnv) + defer os.Unsetenv(cosignutil.CosignDockerMediaTypesEnv) } var cosignAnnotations []string @@ -93,9 +102,9 @@ func (s *ImageSigner) sign(ro *options.RootOptions, refImage, digest, secretLoca cosignAnnotations = append(cosignAnnotations, fmt.Sprintf("%s=%s", key, value)) } - signOptions := options.SignOptions{ - Registry: options.RegistryOptions{KubernetesKeychain: true}, - AnnotationOptions: options.AnnotationOptions{ + signOptions := cosignoptions.SignOptions{ + Registry: cosignoptions.RegistryOptions{KubernetesKeychain: true}, + AnnotationOptions: cosignoptions.AnnotationOptions{ Annotations: cosignAnnotations, }, Upload: true, @@ -114,6 +123,127 @@ func (s *ImageSigner) sign(ro *options.RootOptions, refImage, digest, secretLoca return nil } +func (s *ImageSigner) SignBuilder( + ctx context.Context, + imageReference string, + serviceAccountSecrets []*corev1.Secret, + builderKeychain authn.Keychain, +) ([]v1alpha2.CosignSignature, error) { + signaturePaths := make([]v1alpha2.CosignSignature, 0) + cosignSecrets := filterCosignSecrets(serviceAccountSecrets) + + for _, cosignSecret := range cosignSecrets { + keyRef := fmt.Sprintf("k8s://%s/%s", cosignSecret.Namespace, cosignSecret.Name) + keyOpts := cosignoptions.KeyOpts{ + KeyRef: keyRef, + PassFunc: func(bool) ([]byte, error) { + if password, ok := cosignSecret.Data[cosignutil.SecretDataCosignPassword]; ok { + return password, nil + } + + return []byte(""), nil + }, + } + + if cosignRepository, ok := cosignSecret.Annotations[cosignutil.RepositoryAnnotationPrefix]; ok { + if err := os.Setenv(cosignutil.CosignRepositoryEnv, cosignRepository); err != nil { + return nil, fmt.Errorf("failed setting %s env variable: %w", cosignutil.CosignRepositoryEnv, err) + } + } + + if cosignDockerMediaType, ok := cosignSecret.Annotations[cosignutil.DockerMediaTypesAnnotationPrefix]; ok { + if err := os.Setenv(cosignutil.CosignDockerMediaTypesEnv, cosignDockerMediaType); err != nil { + return nil, fmt.Errorf("failed setting %s env variable: %w", cosignutil.CosignDockerMediaTypesEnv, err) + } + } + + registryOptions := cosignoptions.RegistryOptions{KubernetesKeychain: true, Keychain: builderKeychain} + + signOptions := cosignoptions.SignOptions{ + Registry: registryOptions, + AnnotationOptions: cosignoptions.AnnotationOptions{}, + Upload: true, + Recursive: false, + TlogUpload: false, + } + + rootOptions := cosignoptions.RootOptions{Timeout: cosignoptions.DefaultTimeout} + + if err := s.signFunc( + &rootOptions, + keyOpts, + signOptions, + []string{imageReference}); err != nil { + return nil, fmt.Errorf("unable to sign image with specified key from secret %s in namespace %s: %w", cosignSecret.Name, cosignSecret.Namespace, err) + } + + reference, err := name.ParseReference(imageReference) + if err != nil { + return nil, fmt.Errorf("failed to parse reference: %w", err) + } + + registryOpts, err := registryOptions.ClientOpts(ctx) + if err != nil { + return nil, err + } + + signatureTag, err := s.fetchSignatureFunc(reference, registryOpts...) + if err != nil { + return nil, err + } + + image, err := remote.Image(signatureTag, remote.WithAuthFromKeychain(builderKeychain)) + if err != nil { + return nil, err + } + + digest, err := image.Digest() + if err != nil { + return nil, err + } + + signaturePaths = append( + signaturePaths, + v1alpha2.CosignSignature{ + SigningSecret: keyRef, + TargetDigest: signatureTag.Digest(digest.String()).String(), + }, + ) + + if _, found := os.LookupEnv(cosignutil.CosignDockerMediaTypesEnv); found { + err = os.Unsetenv(cosignutil.CosignDockerMediaTypesEnv) + if err != nil { + return nil, fmt.Errorf("failed to cleanup environment variable %s: %w", cosignutil.CosignDockerMediaTypesEnv, err) + } + } + + if _, found := os.LookupEnv(cosignutil.CosignRepositoryEnv); found { + err = os.Unsetenv(cosignutil.CosignRepositoryEnv) + if err != nil { + return nil, fmt.Errorf("failed to cleanup environment variable %s: %w", cosignutil.CosignRepositoryEnv, err) + } + } + } + + return signaturePaths, nil +} + +func filterCosignSecrets(serviceAccountSecrets []*corev1.Secret) []*corev1.Secret { + cosignSecrets := make([]*corev1.Secret, 0) + + for _, cosignSecret := range serviceAccountSecrets { + _, passwordOk := cosignSecret.Data[cosignutil.SecretDataCosignPassword] + _, keyOk := cosignSecret.Data[cosignutil.SecretDataCosignKey] + + if passwordOk && keyOk { + cosignSecrets = append(cosignSecrets, cosignSecret) + } + } + + // successful + return cosignSecrets +} + func findCosignSecrets(secretLocation string) ([]string, error) { var result []string diff --git a/pkg/cosign/image_signer_test.go b/pkg/cosign/image_signer_test.go index 6a4f91a49..24a023601 100644 --- a/pkg/cosign/image_signer_test.go +++ b/pkg/cosign/image_signer_test.go @@ -3,8 +3,8 @@ package cosign import ( "bufio" "context" - "crypto" "fmt" + "io/ioutil" "log" "net/http/httptest" "net/url" @@ -14,25 +14,36 @@ import ( "strings" "testing" + cosigntesting "github.com/pivotal/kpack/pkg/cosign/testing" + cosignutil "github.com/pivotal/kpack/pkg/cosign/util" + "github.com/BurntSushi/toml" "github.com/buildpacks/lifecycle/platform/files" "github.com/google/go-containerregistry/pkg/authn" "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/random" "github.com/google/go-containerregistry/pkg/v1/remote" + registry2 "github.com/pivotal/kpack/pkg/registry" + "github.com/pivotal/kpack/pkg/registry/registryfakes" "github.com/sclevine/spec" "github.com/sigstore/cosign/v2/cmd/cosign/cli/download" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/cmd/cosign/cli/sign" - verifypkg "github.com/sigstore/cosign/v2/cmd/cosign/cli/verify" sigstoreCosign "github.com/sigstore/cosign/v2/pkg/cosign" ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote" - "github.com/sigstore/cosign/v2/pkg/signature" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +var fetchSignatureFunc = func(_ name.Reference, options ...ociremote.Option) (name.Tag, error) { + tag, _ := name.NewTag("test", nil) + return tag, nil +} + func TestImageSigner(t *testing.T) { spec.Run(t, "Test Cosign Image Signer Main", testImageSigner) } @@ -43,20 +54,22 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { report files.Report reader *os.File writer *os.File - expectedImageName string imageDigest string + hash v1.Hash stopRegistry func() imageCleanup func() repo string + expectedImageName string ) it.Before(func() { _, reader, writer = mockLogger(t) - repo, stopRegistry = reg(t) + repo, stopRegistry = fakeRegistry(t) expectedImageName = path.Join(repo, "test-cosign-image") - imageDigest, imageCleanup = pushRandomImage(t, expectedImageName) + hash, imageCleanup = pushRandomImage(t, expectedImageName) + imageDigest = hash.String() }) it.After(func() { @@ -80,16 +93,16 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { // Override secretLocation for test secretLocation = createCosignKeyFiles(t) - secretKey1 = path.Join(secretLocation, "secret-name-1", "cosign.key") - publicKey1 = path.Join(secretLocation, "secret-name-1", "cosign.pub") - publicKey2 = path.Join(secretLocation, "secret-name-2", "cosign.pub") - passwordFile1 = path.Join(secretLocation, "secret-name-1", "cosign.password") - passwordFile2 = path.Join(secretLocation, "secret-name-2", "cosign.password") + secretKey1 = path.Join(secretLocation, "secret-name-1", cosignutil.SecretDataCosignKey) + publicKey1 = path.Join(secretLocation, "secret-name-1", cosignutil.SecretDataCosignPublicKey) + publicKey2 = path.Join(secretLocation, "secret-name-2", cosignutil.SecretDataCosignPublicKey) + passwordFile1 = path.Join(secretLocation, "secret-name-1", cosignutil.SecretDataCosignPassword) + passwordFile2 = path.Join(secretLocation, "secret-name-2", cosignutil.SecretDataCosignPassword) report = createReportToml(t, expectedImageName, imageDigest) - os.Unsetenv(cosignRepositoryEnv) - os.Unsetenv(cosignDockerMediaTypesEnv) + os.Unsetenv(cosignutil.CosignRepositoryEnv) + os.Unsetenv(cosignutil.CosignDockerMediaTypesEnv) }) it("signs images", func() { @@ -132,7 +145,7 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { imgs) } - signer := NewImageSigner(log.New(writer, "", 0), cliSignCmd) + signer := NewImageSigner(cliSignCmd, fetchSignatureFunc) err := signer.Sign(ro, report, secretLocation, nil, nil, nil) assert.Nil(t, err) @@ -140,10 +153,10 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { assert.Equal(t, 1, password1Count) assert.Equal(t, 1, password2Count) - err = verify(publicKey1, expectedImageName, nil) + err = cosigntesting.Verify(t, publicKey1, expectedImageName, nil) assert.Nil(t, err) - err = verify(publicKey2, expectedImageName, nil) + err = cosigntesting.Verify(t, publicKey2, expectedImageName, nil) assert.Nil(t, err) err = download.SignatureCmd(context.Background(), options.RegistryOptions{}, expectedImageName) @@ -178,28 +191,28 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { ) } - signer := NewImageSigner(log.New(writer, "", 0), cliSignCmd) + signer := NewImageSigner(cliSignCmd, fetchSignatureFunc) err := signer.Sign(ro, report, secretLocation, expectedAnnotation, nil, nil) assert.Nil(t, err) assert.Equal(t, 2, cliSignCmdCallCount) // Should error when validating annotations that dont exist - err = verify(publicKey1, expectedImageName, unexpectedAnnotation) + err = cosigntesting.Verify(t, publicKey1, expectedImageName, unexpectedAnnotation) assert.Error(t, err) - err = verify(publicKey2, expectedImageName, unexpectedAnnotation) + err = cosigntesting.Verify(t, publicKey2, expectedImageName, unexpectedAnnotation) assert.Error(t, err) // Should not error when validating annotations that exist - err = verify(publicKey1, expectedImageName, expectedAnnotation) + err = cosigntesting.Verify(t, publicKey1, expectedImageName, expectedAnnotation) assert.Nil(t, err) - err = verify(publicKey2, expectedImageName, expectedAnnotation) + err = cosigntesting.Verify(t, publicKey2, expectedImageName, expectedAnnotation) assert.Nil(t, err) // Should not error when not validating annotations - err = verify(publicKey1, expectedImageName, nil) + err = cosigntesting.Verify(t, publicKey1, expectedImageName, nil) assert.Nil(t, err) - err = verify(publicKey2, expectedImageName, nil) + err = cosigntesting.Verify(t, publicKey2, expectedImageName, nil) assert.Nil(t, err) err = download.SignatureCmd(context.Background(), options.RegistryOptions{}, expectedImageName) @@ -224,7 +237,7 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { os.Mkdir(filepath.Join(secretLocation, "secret-name-0"), 0700) expectedErrorMessage := fmt.Sprintf("unable to sign image with %s/cosign.key: getting signer: reading key: open %s/cosign.key: no such file or directory", emptyKey, emptyKey) - signer := NewImageSigner(log.New(writer, "", 0), cliSignCmd) + signer := NewImageSigner(cliSignCmd, fetchSignatureFunc) err := signer.Sign(ro, report, secretLocation, nil, nil, nil) assert.Error(t, err) assert.Equal(t, expectedErrorMessage, err.Error()) @@ -250,7 +263,7 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { os.Mkdir(filepath.Join(secretLocation, "secret-name-3"), 0700) expectedErrorMessage := fmt.Sprintf("unable to sign image with %s/cosign.key: getting signer: reading key: open %s/cosign.key: no such file or directory", emptyKey, emptyKey) - signer := NewImageSigner(log.New(writer, "", 0), cliSignCmd) + signer := NewImageSigner(cliSignCmd, fetchSignatureFunc) err := signer.Sign(ro, report, secretLocation, nil, nil, nil) assert.Error(t, err) assert.Equal(t, expectedErrorMessage, err.Error()) @@ -258,21 +271,21 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { }) it("sets COSIGN_REPOSITORY environment variable", func() { - altRepo, altStopRegistry := reg(t) + altRepo, altStopRegistry := fakeRegistry(t) defer altStopRegistry() altImageName := path.Join(altRepo, "test-cosign-image-alt") cliSignCmdCallCount := 0 - assert.Empty(t, len(os.Getenv(cosignRepositoryEnv))) + assert.Empty(t, len(os.Getenv(cosignutil.CosignRepositoryEnv))) cliSignCmd := func( ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignOptions, imgs []string, ) error { t.Helper() if strings.Contains(ko.KeyRef, "secret-name-2") { - assert.Equal(t, altImageName, os.Getenv(cosignRepositoryEnv)) + assert.Equal(t, altImageName, os.Getenv(cosignutil.CosignRepositoryEnv)) } else { - assertUnset(t, cosignRepositoryEnv) + assertUnset(t, cosignutil.CosignRepositoryEnv) } cliSignCmdCallCount++ @@ -288,42 +301,42 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { "secret-name-2": altImageName, } - signer := NewImageSigner(log.New(writer, "", 0), cliSignCmd) + signer := NewImageSigner(cliSignCmd, fetchSignatureFunc) err := signer.Sign(ro, report, secretLocation, nil, cosignRepositories, nil) assert.Nil(t, err) assert.Equal(t, 2, cliSignCmdCallCount) - assertUnset(t, cosignRepositoryEnv) + assertUnset(t, cosignutil.CosignRepositoryEnv) - err = verify(publicKey1, expectedImageName, nil) + err = cosigntesting.Verify(t, publicKey1, expectedImageName, nil) assert.Nil(t, err) - err = verify(publicKey2, expectedImageName, nil) + err = cosigntesting.Verify(t, publicKey2, expectedImageName, nil) assert.Error(t, err) err = download.SignatureCmd(context.Background(), options.RegistryOptions{}, expectedImageName) assert.Nil(t, err) // Required to set COSIGN_REPOSITORY env variable to validate signature // on a registry that does not contain the image - os.Setenv(cosignRepositoryEnv, altImageName) - defer os.Unsetenv(cosignRepositoryEnv) - err = verify(publicKey1, expectedImageName, nil) + os.Setenv(cosignutil.CosignRepositoryEnv, altImageName) + defer os.Unsetenv(cosignutil.CosignRepositoryEnv) + err = cosigntesting.Verify(t, publicKey1, expectedImageName, nil) assert.Error(t, err) - err = verify(publicKey2, expectedImageName, nil) + err = cosigntesting.Verify(t, publicKey2, expectedImageName, nil) assert.Nil(t, err) }) it("sets COSIGN_DOCKER_MEDIA_TYPES environment variable", func() { cliSignCmdCallCount := 0 - assertUnset(t, cosignDockerMediaTypesEnv) + assertUnset(t, cosignutil.CosignDockerMediaTypesEnv) cliSignCmd := func( ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignOptions, imgs []string, ) error { t.Helper() if strings.Contains(ko.KeyRef, "secret-name-1") { - assert.Equal(t, "1", os.Getenv(cosignDockerMediaTypesEnv)) + assert.Equal(t, "1", os.Getenv(cosignutil.CosignDockerMediaTypesEnv)) } else { - assertUnset(t, cosignDockerMediaTypesEnv) + assertUnset(t, cosignutil.CosignDockerMediaTypesEnv) } cliSignCmdCallCount++ @@ -334,25 +347,25 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { "secret-name-1": "1", } - signer := NewImageSigner(log.New(writer, "", 0), cliSignCmd) + signer := NewImageSigner(cliSignCmd, fetchSignatureFunc) err := signer.Sign(ro, report, secretLocation, nil, nil, cosignDockerMediaTypes) assert.Nil(t, err) assert.Equal(t, 2, cliSignCmdCallCount) - assertUnset(t, cosignDockerMediaTypesEnv) + assertUnset(t, cosignutil.CosignDockerMediaTypesEnv) }) it("sets both COSIGN_REPOSITORY and COSIGN_DOCKER_MEDIA_TYPES environment variable", func() { cliSignCmdCallCount := 0 - assertUnset(t, cosignDockerMediaTypesEnv) - assertUnset(t, cosignRepositoryEnv) + assertUnset(t, cosignutil.CosignDockerMediaTypesEnv) + assertUnset(t, cosignutil.CosignRepositoryEnv) cliSignCmd := func( ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignOptions, imgs []string, ) error { t.Helper() - assert.Equal(t, "1", os.Getenv(cosignDockerMediaTypesEnv)) - assert.Equal(t, "registry.example.com/fakeproject", os.Getenv(cosignRepositoryEnv)) + assert.Equal(t, "1", os.Getenv(cosignutil.CosignDockerMediaTypesEnv)) + assert.Equal(t, "registry.example.com/fakeproject", os.Getenv(cosignutil.CosignRepositoryEnv)) cliSignCmdCallCount++ return nil } @@ -367,13 +380,13 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { "secret-name-2": "1", } - signer := NewImageSigner(log.New(writer, "", 0), cliSignCmd) + signer := NewImageSigner(cliSignCmd, fetchSignatureFunc) err := signer.Sign(ro, report, secretLocation, nil, cosignRepositories, cosignDockerMediaTypes) assert.Nil(t, err) assert.Equal(t, 2, cliSignCmdCallCount) - assertUnset(t, cosignDockerMediaTypesEnv) - assertUnset(t, cosignRepositoryEnv) + assertUnset(t, cosignutil.CosignDockerMediaTypesEnv) + assertUnset(t, cosignutil.CosignRepositoryEnv) }) }) @@ -391,7 +404,7 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { return nil } - signer := NewImageSigner(log.New(writer, "", 0), cliSignCmd) + signer := NewImageSigner(cliSignCmd, fetchSignatureFunc) err := signer.Sign(ro, report, secretLocation, nil, nil, nil) require.Error(t, err, "no keys found for cosign signing") assert.Equal(t, 0, cliSignCmdCallCount) @@ -409,7 +422,7 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { return nil } - signer := NewImageSigner(log.New(writer, "", 0), cliSignCmd) + signer := NewImageSigner(cliSignCmd, fetchSignatureFunc) err := signer.Sign(ro, report, secretLocation, nil, nil, nil) require.Error(t, err, "no keys found for cosign signing: open /fake/location/that/doesnt/exist: no such file or directory") assert.Equal(t, 0, cliSignCmdCallCount) @@ -427,7 +440,7 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { return nil } - signer := NewImageSigner(log.New(writer, "", 0), cliSignCmd) + signer := NewImageSigner(cliSignCmd, fetchSignatureFunc) err := signer.Sign(ro, report, secretLocation, nil, nil, nil) require.Error(t, err, "no image found in report to sign") assert.Equal(t, 0, cliSignCmdCallCount) @@ -439,7 +452,7 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { it("signs an image", func() { secretLocation := t.TempDir() - repo, stop := reg(t) + repo, stop := fakeRegistry(t) defer stop() imgName := path.Join(repo, "cosign-e2e") @@ -449,12 +462,12 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { password := "" keypair(t, secretLocation, "secret-name-1", password) - privKeyPath := path.Join(secretLocation, "secret-name-1", "cosign.key") - pubKeyPath := path.Join(secretLocation, "secret-name-1", "cosign.pub") + privKeyPath := path.Join(secretLocation, "secret-name-1", cosignutil.SecretDataCosignKey) + pubKeyPath := path.Join(secretLocation, "secret-name-1", cosignutil.SecretDataCosignPublicKey) ctx := context.Background() // Verify+download should fail at first - err := verify(pubKeyPath, imgName, nil) + err := cosigntesting.Verify(t, pubKeyPath, imgName, nil) assert.Error(t, err) err = download.SignatureCmd(ctx, options.RegistryOptions{}, imgName) assert.Error(t, err) @@ -479,12 +492,159 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { assert.Nil(t, err) // Verify+download should pass - err = verify(pubKeyPath, imgName, nil) + err = cosigntesting.Verify(t, pubKeyPath, imgName, nil) assert.Nil(t, err) err = download.SignatureCmd(ctx, options.RegistryOptions{}, imgName) assert.Nil(t, err) }) }) + + when("#SignBuilder", func() { + const ( + cosignSecretName = "cosign-creds" + testNamespaceName = "test-namespace" + cosignServiceAccountName = "cosign-sa" + ) + + it("resolves the digest of a signature correctly", func() { + var ( + signCallCount = 0 + fetchSignatureCallCount = 0 + ) + + fakeImageSignatureTag := fmt.Sprintf("%s:%s", expectedImageName, "test.sig") + digest, cleanup := pushRandomImage(t, fakeImageSignatureTag) + defer cleanup() + + fakeImageSigner := &ImageSigner{ + signFunc: func(rootOptions *options.RootOptions, opts options.KeyOpts, signOptions options.SignOptions, i []string) error { + t.Helper() + + signCallCount++ + return nil + }, + fetchSignatureFunc: func(reference name.Reference, option ...ociremote.Option) (name.Tag, error) { + t.Helper() + + fetchSignatureCallCount++ + return name.NewTag(fakeImageSignatureTag) + }, + } + + fakeSecret := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName, testNamespaceName, "", nil) + cosignCreds := []*corev1.Secret{&fakeSecret} + cosignSA := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: cosignServiceAccountName, + Namespace: testNamespaceName, + }, + Secrets: []corev1.ObjectReference{ + { + Name: fakeSecret.Name, + }, + }, + } + + secretRef := registry2.SecretRef{ + ServiceAccount: cosignSA.Name, + Namespace: cosignSA.Namespace, + } + + keychainFactory := ®istryfakes.FakeKeychainFactory{} + fakeKeychain := ®istryfakes.FakeKeychain{} + keychainFactory.AddKeychainForSecretRef(t, secretRef, fakeKeychain) + + signaturePaths, err := fakeImageSigner.SignBuilder(context.Background(), expectedImageName, cosignCreds, fakeKeychain) + require.NoError(t, err) + require.NotEmpty(t, signaturePaths) + require.NotNil(t, signaturePaths[0]) + + assert.Contains(t, signaturePaths[0].TargetDigest, digest.String()) + assert.Contains(t, signaturePaths[0].SigningSecret, fakeSecret.Namespace) + assert.Contains(t, signaturePaths[0].SigningSecret, fakeSecret.Name) + + require.Equal(t, 1, signCallCount) + require.Equal(t, 1, fetchSignatureCallCount) + }) + + it("sets environment variables when needed", func() { + var ( + signCallCount = 0 + fetchSignatureCallCount = 0 + signaturesPath = path.Join(repo, "signatures") + dockerMediaTypesValue = "1" + ) + + fakeImageSignatureTag := fmt.Sprintf("%s:%s", signaturesPath, "test.sig") + digest, cleanup := pushRandomImage(t, fakeImageSignatureTag) + defer cleanup() + + fakeImageSigner := &ImageSigner{ + signFunc: func(rootOptions *options.RootOptions, opts options.KeyOpts, signOptions options.SignOptions, i []string) error { + t.Helper() + + value, found := os.LookupEnv(cosignutil.CosignRepositoryEnv) + require.True(t, found) + require.NotNil(t, value) + assert.Equal(t, signaturesPath, value) + + value, found = os.LookupEnv(cosignutil.CosignDockerMediaTypesEnv) + require.True(t, found) + require.NotNil(t, value) + assert.Equal(t, dockerMediaTypesValue, value) + + signCallCount++ + return nil + }, + fetchSignatureFunc: func(reference name.Reference, option ...ociremote.Option) (name.Tag, error) { + t.Helper() + + fetchSignatureCallCount++ + return name.NewTag(fakeImageSignatureTag) + }, + } + + annotations := map[string]string{ + "kpack.io/cosign.repository": signaturesPath, + "kpack.io/cosign.docker-media-types": dockerMediaTypesValue, + } + + fakeSecret := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName, testNamespaceName, "", annotations) + cosignCreds := []*corev1.Secret{&fakeSecret} + cosignSA := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: cosignServiceAccountName, + Namespace: testNamespaceName, + }, + Secrets: []corev1.ObjectReference{ + { + Name: fakeSecret.Name, + }, + }, + } + + secretRef := registry2.SecretRef{ + ServiceAccount: cosignSA.Name, + Namespace: cosignSA.Namespace, + } + + keychainFactory := ®istryfakes.FakeKeychainFactory{} + fakeKeychain := ®istryfakes.FakeKeychain{} + keychainFactory.AddKeychainForSecretRef(t, secretRef, fakeKeychain) + + signaturePaths, err := fakeImageSigner.SignBuilder(context.Background(), expectedImageName, cosignCreds, fakeKeychain) + require.NoError(t, err) + require.NotEmpty(t, signaturePaths) + require.NotNil(t, signaturePaths[0]) + + assert.Contains(t, signaturePaths[0].TargetDigest, digest.String()) + assert.Contains(t, signaturePaths[0].SigningSecret, fakeSecret.Namespace) + assert.Contains(t, signaturePaths[0].SigningSecret, fakeSecret.Name) + + require.Equal(t, 1, signCallCount) + require.Equal(t, 1, fetchSignatureCallCount) + }) + }) } func mockLogger(t *testing.T) (*bufio.Scanner, *os.File, *os.File) { @@ -540,7 +700,7 @@ func assertUnset(t *testing.T, envName string, msg ...string) { assert.Equal(t, "", value) } -func reg(t *testing.T) (string, func()) { +func fakeRegistry(t *testing.T) (string, func()) { r := httptest.NewServer(registry.New()) u, err := url.Parse(r.URL) assert.Nil(t, err) @@ -548,24 +708,20 @@ func reg(t *testing.T) (string, func()) { return u.Host, r.Close } -func pushRandomImage(t *testing.T, imageRef string) (string, func()) { +func pushRandomImage(t *testing.T, imageRef string) (v1.Hash, func()) { ref, err := name.ParseReference(imageRef, name.WeakValidation) - assert.Nil(t, err) + require.NoError(t, err) img, err := random.Image(512, 5) - assert.Nil(t, err) + require.NoError(t, err) regClientOpts := registryClientOpts(context.Background()) err = remote.Write(ref, img, regClientOpts...) - assert.Nil(t, err) - - _, err = remote.Get(ref, regClientOpts...) - assert.Nil(t, err) + require.NoError(t, err) - imgHash, err := img.Digest() - assert.Nil(t, err) - imgDigest := imgHash.String() + resp, err := remote.Get(ref, regClientOpts...) + require.NoError(t, err) cleanup := func() { _ = remote.Delete(ref, regClientOpts...) @@ -573,7 +729,7 @@ func pushRandomImage(t *testing.T, imageRef string) (string, func()) { _ = remote.Delete(ref, regClientOpts...) } - return imgDigest, cleanup + return resp.Digest, cleanup } func registryClientOpts(ctx context.Context) []remote.Option { @@ -584,6 +740,8 @@ func registryClientOpts(ctx context.Context) []remote.Option { } func keypair(t *testing.T, dirPath, secretName, password string) { + t.Helper() + passFunc := func(_ bool) ([]byte, error) { return []byte(password), nil } @@ -594,30 +752,16 @@ func keypair(t *testing.T, dirPath, secretName, password string) { err = os.Mkdir(filepath.Join(dirPath, secretName), 0700) assert.Nil(t, err) - privKeyPath := filepath.Join(dirPath, secretName, "cosign.key") - err = os.WriteFile(privKeyPath, keys.PrivateBytes, 0600) + privKeyPath := filepath.Join(dirPath, secretName, cosignutil.SecretDataCosignKey) + err = ioutil.WriteFile(privKeyPath, keys.PrivateBytes, 0600) assert.Nil(t, err) - pubKeyPath := filepath.Join(dirPath, secretName, "cosign.pub") - err = os.WriteFile(pubKeyPath, keys.PublicBytes, 0600) + pubKeyPath := filepath.Join(dirPath, secretName, cosignutil.SecretDataCosignPublicKey) + err = ioutil.WriteFile(pubKeyPath, keys.PublicBytes, 0600) assert.Nil(t, err) - passwordPath := filepath.Join(dirPath, secretName, "cosign.password") + passwordPath := filepath.Join(dirPath, secretName, cosignutil.SecretDataCosignPassword) passwordBytes, _ := passFunc(true) err = os.WriteFile(passwordPath, passwordBytes, 0600) assert.Nil(t, err) } - -func verify(keyRef, imageRef string, annotations map[string]interface{}) error { - cmd := verifypkg.VerifyCommand{ - KeyRef: keyRef, - Annotations: signature.AnnotationsMap{Annotations: annotations}, - CheckClaims: true, - HashAlgorithm: crypto.SHA256, - IgnoreTlog: true, - } - - args := []string{imageRef} - - return cmd.Exec(context.Background(), args) -} diff --git a/pkg/cosign/testing/test_util.go b/pkg/cosign/testing/test_util.go new file mode 100644 index 000000000..90dc2c854 --- /dev/null +++ b/pkg/cosign/testing/test_util.go @@ -0,0 +1,60 @@ +package testing + +import ( + "context" + "crypto" + "testing" + + cosignutil "github.com/pivotal/kpack/pkg/cosign/util" + + cosignVerify "github.com/sigstore/cosign/v2/cmd/cosign/cli/verify" + "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/sigstore/cosign/v2/pkg/signature" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func GenerateFakeKeyPair(t *testing.T, secretName string, secretNamespace string, password string, annotations map[string]string) corev1.Secret { + t.Helper() + + passFunc := func(_ bool) ([]byte, error) { + return []byte(password), nil + } + + keys, err := cosign.GenerateKeyPair(passFunc) + require.NoError(t, err) + + data := map[string][]byte{ + cosignutil.SecretDataCosignPublicKey: keys.PublicBytes, + cosignutil.SecretDataCosignKey: keys.PrivateBytes, + cosignutil.SecretDataCosignPassword: []byte(password), + } + + secret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: secretNamespace, + Annotations: annotations, + }, + Data: data, + } + + return secret +} + +func Verify(t *testing.T, keyRef, imageRef string, annotations map[string]interface{}) error { + t.Helper() + + cmd := cosignVerify.VerifyCommand{ + KeyRef: keyRef, + Annotations: signature.AnnotationsMap{Annotations: annotations}, + CheckClaims: true, + HashAlgorithm: crypto.SHA256, + IgnoreTlog: true, + } + + args := []string{imageRef} + + return cmd.Exec(context.Background(), args) +} diff --git a/pkg/cosign/util/constants.go b/pkg/cosign/util/constants.go new file mode 100644 index 000000000..96efabd5f --- /dev/null +++ b/pkg/cosign/util/constants.go @@ -0,0 +1,12 @@ +package cosignutil + +const ( + CosignRepositoryEnv = "COSIGN_REPOSITORY" + CosignDockerMediaTypesEnv = "COSIGN_DOCKER_MEDIA_TYPES" + + SecretDataCosignKey = "cosign.key" + SecretDataCosignPassword = "cosign.password" + SecretDataCosignPublicKey = "cosign.pub" + DockerMediaTypesAnnotationPrefix = "kpack.io/cosign.docker-media-types" + RepositoryAnnotationPrefix = "kpack.io/cosign.repository" +) diff --git a/pkg/dockercreds/docker_creds_test.go b/pkg/dockercreds/docker_creds_test.go index 37da50898..0806c39e8 100644 --- a/pkg/dockercreds/docker_creds_test.go +++ b/pkg/dockercreds/docker_creds_test.go @@ -1,7 +1,6 @@ package dockercreds import ( - "io/ioutil" "os" "path/filepath" "testing" @@ -22,7 +21,7 @@ func testDockerCreds(t *testing.T, when spec.G, it spec.S) { it.Before(func() { var err error - testPullSecretsDir, err = ioutil.TempDir("", "test.append") + testPullSecretsDir, err = os.MkdirTemp("", "test.append") require.NoError(t, err) }) @@ -52,7 +51,7 @@ func testDockerCreds(t *testing.T, when spec.G, it spec.S) { err := creds.Save(filepath.Join(testPullSecretsDir, "config.json")) require.NoError(t, err) - configJsonBytes, err := ioutil.ReadFile(filepath.Join(testPullSecretsDir, "config.json")) + configJsonBytes, err := os.ReadFile(filepath.Join(testPullSecretsDir, "config.json")) require.NoError(t, err) assert.JSONEq(t, expectedConfigJsonContents, string(configJsonBytes)) diff --git a/pkg/dockercreds/parse_secrets.go b/pkg/dockercreds/parse_secrets.go index b6d0e3c90..a61de6ffe 100644 --- a/pkg/dockercreds/parse_secrets.go +++ b/pkg/dockercreds/parse_secrets.go @@ -2,7 +2,6 @@ package dockercreds import ( "encoding/json" - "io/ioutil" "os" "path/filepath" "strings" @@ -59,7 +58,7 @@ func parseDockerCfg(path string) (DockerCreds, error) { if !cfgExists { return creds, nil } - cfgFile, err := ioutil.ReadFile(path) + cfgFile, err := os.ReadFile(path) if err != nil { return nil, err } @@ -84,7 +83,7 @@ func parseDockerConfigJson(path string) (DockerCreds, error) { return config.Auths, nil } - configJsonFile, err := ioutil.ReadFile(path) + configJsonFile, err := os.ReadFile(path) if err != nil { return nil, err } diff --git a/pkg/dockercreds/parse_secrets_test.go b/pkg/dockercreds/parse_secrets_test.go index e5b9b7295..20d0a2aee 100644 --- a/pkg/dockercreds/parse_secrets_test.go +++ b/pkg/dockercreds/parse_secrets_test.go @@ -1,7 +1,6 @@ package dockercreds import ( - "io/ioutil" "os" "path" "path/filepath" @@ -26,7 +25,7 @@ func parseDockerConfigSecret(t *testing.T, when spec.G, it spec.S) { it.Before(func() { var err error - testSecretsDir, err = ioutil.TempDir("", "test.pullsecrets") + testSecretsDir, err = os.MkdirTemp("", "test.pullsecrets") require.NoError(t, err) }) @@ -35,7 +34,7 @@ func parseDockerConfigSecret(t *testing.T, when spec.G, it spec.S) { }) it("parses .dockerconfigjson favoring auth key", func() { - err := ioutil.WriteFile(filepath.Join(testSecretsDir, ".dockerconfigjson"), []byte(`{ + err := os.WriteFile(filepath.Join(testSecretsDir, ".dockerconfigjson"), []byte(`{ "auths": { "https://index.docker.io/v1/": { "auth": "dGVzdHVzZXJuYW1lOnRlc3RwYXNzd29yZHNpbGxpbmVzcwo=", @@ -61,7 +60,7 @@ func parseDockerConfigSecret(t *testing.T, when spec.G, it spec.S) { }) it("parses .dockerconfigjson setting auth key to username/password if unset", func() { - err := ioutil.WriteFile(filepath.Join(testSecretsDir, ".dockerconfigjson"), []byte(`{ + err := os.WriteFile(filepath.Join(testSecretsDir, ".dockerconfigjson"), []byte(`{ "auths": { "https://index.docker.io/v1/": { "username": "testusername", @@ -86,7 +85,7 @@ func parseDockerConfigSecret(t *testing.T, when spec.G, it spec.S) { }) it("parses .dockercfg favoring auth key", func() { - err := ioutil.WriteFile(filepath.Join(testSecretsDir, ".dockercfg"), []byte(`{ + err := os.WriteFile(filepath.Join(testSecretsDir, ".dockercfg"), []byte(`{ "https://index.docker.io/v1/": { "auth": "dGVzdHVzZXJuYW1lOnRlc3RwYXNzd29yZHNpbGxpbmVzcwo=", "username": "testusername", @@ -110,7 +109,7 @@ func parseDockerConfigSecret(t *testing.T, when spec.G, it spec.S) { }) it("parses .dockercfg setting auth key to username/password if unset", func() { - err := ioutil.WriteFile(filepath.Join(testSecretsDir, ".dockercfg"), []byte(`{ + err := os.WriteFile(filepath.Join(testSecretsDir, ".dockercfg"), []byte(`{ "https://index.docker.io/v1/": { "username": "testusername", "password": "testpassword" @@ -133,7 +132,7 @@ func parseDockerConfigSecret(t *testing.T, when spec.G, it spec.S) { }) it("parses .dockercfg and .dockerconfigjson", func() { - err := ioutil.WriteFile(filepath.Join(testSecretsDir, ".dockerconfigjson"), []byte(`{ + err := os.WriteFile(filepath.Join(testSecretsDir, ".dockerconfigjson"), []byte(`{ "auths": { "https://index.docker.io/v1/": { "auth": "dGVzdHVzZXJuYW1lOnRlc3RwYXNzd29yZHNpbGxpbmVzcwo=" @@ -143,7 +142,7 @@ func parseDockerConfigSecret(t *testing.T, when spec.G, it spec.S) { ), os.ModePerm) require.NoError(t, err) - err = ioutil.WriteFile(filepath.Join(testSecretsDir, ".dockercfg"), []byte(`{ + err = os.WriteFile(filepath.Join(testSecretsDir, ".dockercfg"), []byte(`{ "gcr.io": { "auth": "dGVzdHVzZXJuYW1lOnRlc3RwYXNzd29yZA==", "username": "testusername", @@ -178,17 +177,17 @@ func testParseBasicAuthSecrets(t *testing.T, when spec.G, it spec.S) { var testDir string it.Before(func() { var err error - testDir, err = ioutil.TempDir("", "docker-secret-parse-test") + testDir, err = os.MkdirTemp("", "docker-secret-parse-test") require.NoError(t, err) require.NoError(t, os.MkdirAll(path.Join(testDir, "gcr-creds"), 0777)) require.NoError(t, os.MkdirAll(path.Join(testDir, "dockerhub-creds"), 0777)) - require.NoError(t, ioutil.WriteFile(path.Join(testDir, "gcr-creds", corev1.BasicAuthUsernameKey), []byte("gcr-username"), 0600)) - require.NoError(t, ioutil.WriteFile(path.Join(testDir, "gcr-creds", corev1.BasicAuthPasswordKey), []byte("gcr-password"), 0600)) + require.NoError(t, os.WriteFile(path.Join(testDir, "gcr-creds", corev1.BasicAuthUsernameKey), []byte("gcr-username"), 0600)) + require.NoError(t, os.WriteFile(path.Join(testDir, "gcr-creds", corev1.BasicAuthPasswordKey), []byte("gcr-password"), 0600)) - require.NoError(t, ioutil.WriteFile(path.Join(testDir, "dockerhub-creds", corev1.BasicAuthUsernameKey), []byte("dockerhub-username"), 0600)) - require.NoError(t, ioutil.WriteFile(path.Join(testDir, "dockerhub-creds", corev1.BasicAuthPasswordKey), []byte("dockerhub-password"), 0600)) + require.NoError(t, os.WriteFile(path.Join(testDir, "dockerhub-creds", corev1.BasicAuthUsernameKey), []byte("dockerhub-username"), 0600)) + require.NoError(t, os.WriteFile(path.Join(testDir, "dockerhub-creds", corev1.BasicAuthPasswordKey), []byte("dockerhub-password"), 0600)) }) diff --git a/pkg/dockercreds/volume_secret_keychain_test.go b/pkg/dockercreds/volume_secret_keychain_test.go index 648575b71..950bf86a6 100644 --- a/pkg/dockercreds/volume_secret_keychain_test.go +++ b/pkg/dockercreds/volume_secret_keychain_test.go @@ -1,7 +1,6 @@ package dockercreds_test import ( - "io/ioutil" "os" "path/filepath" "testing" @@ -32,7 +31,7 @@ func testVolumeSecretKeychain(t *testing.T, when spec.G, it spec.S) { ) it.Before(func() { var err error - folderDir, err = ioutil.TempDir("", "secret-volume") + folderDir, err = os.MkdirTemp("", "secret-volume") require.NoError(t, err) resource = fakeDockerResource{registryString: "registry.io"} }) @@ -42,7 +41,7 @@ func testVolumeSecretKeychain(t *testing.T, when spec.G, it spec.S) { }) it("retrieves the configuration from file", func() { - require.NoError(t, ioutil.WriteFile(filepath.Join(folderDir, ".dockerconfigjson"), []byte(` + require.NoError(t, os.WriteFile(filepath.Join(folderDir, ".dockerconfigjson"), []byte(` { "auths": { "registry.io": { diff --git a/pkg/git/fetch.go b/pkg/git/fetch.go index 999ca6568..8725045d3 100644 --- a/pkg/git/fetch.go +++ b/pkg/git/fetch.go @@ -9,6 +9,7 @@ import ( gogit "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/protocol/packp/capability" "github.com/go-git/go-git/v5/plumbing/transport" "github.com/pkg/errors" ) @@ -18,6 +19,13 @@ type Fetcher struct { Keychain GitKeychain } +func init() { + //remove multi_ack and multi_ack_detailed from unsupported capabilities to enable Azure DevOps git support + transport.UnsupportedCapabilities = []capability.Capability{ + capability.ThinPack, + } +} + func (f Fetcher) Fetch(dir, gitURL, gitRevision, metadataDir string) error { f.Logger.Printf("Cloning %q @ %q...", gitURL, gitRevision) auth, err := f.Keychain.Resolve(gitURL) diff --git a/pkg/git/fetch_test.go b/pkg/git/fetch_test.go index 6695372b5..7d32ddd07 100644 --- a/pkg/git/fetch_test.go +++ b/pkg/git/fetch_test.go @@ -90,10 +90,10 @@ func testGitCheckout(t *testing.T, when spec.G, it spec.S) { }) it("preserves executable permission", func() { - err := fetcher.Fetch(testDir, "https://github.com/pivotal/kpack", "main", metadataDir) + err := fetcher.Fetch(testDir, "https://github.com/pivotal/kpack", "b8c0d491135595cc00ab78f6214bef8a7a20afd8", metadataDir) require.NoError(t, err) - fileInfo, err := os.Lstat(path.Join(testDir, "hack", "apply.sh")) + fileInfo, err := os.Lstat(path.Join(testDir, "hack", "local.sh")) require.NoError(t, err) require.True(t, isExecutableByAll(fileInfo.Mode())) diff --git a/pkg/notary/image_signer_test.go b/pkg/notary/image_signer_test.go index 7e7f5783e..9db516e38 100644 --- a/pkg/notary/image_signer_test.go +++ b/pkg/notary/image_signer_test.go @@ -2,7 +2,7 @@ package notary import ( "encoding/hex" - "io/ioutil" + "io" "log" "path/filepath" "testing" @@ -30,7 +30,7 @@ func TestImageSigner(t *testing.T) { func testImageSigner(t *testing.T, when spec.G, it spec.S) { var ( - logger = log.New(ioutil.Discard, "", 0) + logger = log.New(io.Discard, "", 0) client = registryfakes.NewFakeClient() diff --git a/pkg/notary/repository.go b/pkg/notary/repository.go index 992326c1b..b02ed52bb 100644 --- a/pkg/notary/repository.go +++ b/pkg/notary/repository.go @@ -1,7 +1,7 @@ package notary import ( - "io/ioutil" + "os" "path" "github.com/pkg/errors" @@ -17,7 +17,7 @@ type RemoteRepositoryFactory struct { } func (r *RemoteRepositoryFactory) GetRepository(url string, gun data.GUN, remoteStore storage.RemoteStore, cryptoService signed.CryptoService) (Repository, error) { - changeListDir, err := ioutil.TempDir("", "") + changeListDir, err := os.MkdirTemp("", "") if err != nil { return nil, err } diff --git a/pkg/reconciler/builder/builder.go b/pkg/reconciler/builder/builder.go index 4a0f181e6..f081e80ad 100644 --- a/pkg/reconciler/builder/builder.go +++ b/pkg/reconciler/builder/builder.go @@ -3,6 +3,8 @@ package builder import ( "context" + corev1 "k8s.io/api/core/v1" + "go.uber.org/zap" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -33,7 +35,11 @@ const ( ) type BuilderCreator interface { - CreateBuilder(ctx context.Context, builderKeychain authn.Keychain, keychain authn.Keychain, fetcher cnb.RemoteBuildpackFetcher, clusterStack *buildapi.ClusterStack, spec buildapi.BuilderSpec) (buildapi.BuilderRecord, error) + CreateBuilder(ctx context.Context, builderKeychain authn.Keychain, keychain authn.Keychain, fetcher cnb.RemoteBuildpackFetcher, clusterStack *buildapi.ClusterStack, spec buildapi.BuilderSpec, serviceAccountSecrets []*corev1.Secret) (buildapi.BuilderRecord, error) +} + +type Fetcher interface { + SecretsForServiceAccount(context.Context, string, string) ([]*corev1.Secret, error) } func NewController( @@ -48,6 +54,7 @@ func NewController( clusterStackInformer buildinformers.ClusterStackInformer, extensionInformer buildinformers.ExtensionInformer, clusterExtensionInformer buildinformers.ClusterExtensionInformer, + secretFetcher Fetcher, ) (*controller.Impl, func()) { c := &Reconciler{ Client: opt.Client, @@ -60,6 +67,7 @@ func NewController( ClusterStackLister: clusterStackInformer.Lister(), ExtensionLister: extensionInformer.Lister(), ClusterExtensionLister: clusterExtensionInformer.Lister(), + SecretFetcher: secretFetcher, } logger := opt.Logger.With( @@ -114,6 +122,7 @@ type Reconciler struct { ExtensionLister buildlisters.ExtensionLister ClusterExtensionLister buildlisters.ClusterExtensionLister ClusterStackLister buildlisters.ClusterStackLister + SecretFetcher Fetcher } func (c *Reconciler) Reconcile(ctx context.Context, key string) error { @@ -240,7 +249,12 @@ func (c *Reconciler) reconcileBuilder(ctx context.Context, builder *buildapi.Bui fetcher := cnb.NewRemoteBuildpackFetcher(c.KeychainFactory, clusterStore, buildpacks, clusterBuildpacks, extensions, clusterExtensions) - buildRecord, err := c.BuilderCreator.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, clusterStack, builder.Spec.BuilderSpec) + serviceAccountSecrets, err := c.SecretFetcher.SecretsForServiceAccount(ctx, builder.Spec.ServiceAccount(), builder.Namespace) + if err != nil { + return buildapi.BuilderRecord{}, err + } + + buildRecord, err := c.BuilderCreator.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, clusterStack, builder.Spec.BuilderSpec, serviceAccountSecrets) if err != nil { return buildapi.BuilderRecord{}, err } diff --git a/pkg/reconciler/builder/builder_test.go b/pkg/reconciler/builder/builder_test.go index eb21d0326..3f6a0a1d8 100644 --- a/pkg/reconciler/builder/builder_test.go +++ b/pkg/reconciler/builder/builder_test.go @@ -4,6 +4,10 @@ import ( "errors" "testing" + "github.com/pivotal/kpack/pkg/secret/secretfakes" + + k8sfake "k8s.io/client-go/kubernetes/fake" + "github.com/sclevine/spec" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -42,15 +46,19 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { ) var ( - builderCreator = &testhelpers.FakeBuilderCreator{} - keychainFactory = ®istryfakes.FakeKeychainFactory{} - fakeTracker = &testhelpers.FakeTracker{} + builderCreator = &testhelpers.FakeBuilderCreator{} + keychainFactory = ®istryfakes.FakeKeychainFactory{} + fakeTracker = &testhelpers.FakeTracker{} + fakeSecretFetcher = &secretfakes.FakeFetchSecret{ + FakeSecrets: []*corev1.Secret{}, + } ) rt := testhelpers.ReconcilerTester(t, func(t *testing.T, row *rtesting.TableRow) (reconciler controller.Reconciler, lists rtesting.ActionRecorderList, list rtesting.EventList) { listers := testhelpers.NewListers(row.Objects) fakeClient := fake.NewSimpleClientset(listers.BuildServiceObjects()...) + k8sfakeClient := k8sfake.NewSimpleClientset(listers.GetKubeObjects()...) r := &builder.Reconciler{ Client: fakeClient, BuilderLister: listers.GetBuilderLister(), @@ -63,10 +71,30 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { ExtensionLister: listers.GetExtensionLister(), ClusterExtensionLister: listers.GetClusterExtensionLister(), ClusterStackLister: listers.GetClusterStackLister(), + SecretFetcher: fakeSecretFetcher, } - return &kreconciler.NetworkErrorReconciler{Reconciler: r}, rtesting.ActionRecorderList{fakeClient}, rtesting.EventList{Recorder: record.NewFakeRecorder(10)} + return &kreconciler.NetworkErrorReconciler{Reconciler: r}, rtesting.ActionRecorderList{fakeClient, k8sfakeClient}, rtesting.EventList{Recorder: record.NewFakeRecorder(10)} }) + signingSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-secret-name", + Namespace: testNamespace, + }, + } + + serviceAccount := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-sa-name", + Namespace: signingSecret.Namespace, + }, + Secrets: []corev1.ObjectReference{ + { + Name: signingSecret.Name, + }, + }, + } + clusterStore := &buildapi.ClusterStore{ ObjectMeta: metav1.ObjectMeta{ Name: "some-store", @@ -188,18 +216,24 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { }, }}, }, - ServiceAccountName: "some-service-account", + ServiceAccountName: serviceAccount.Name, }, } secretRef := registry.SecretRef{ - ServiceAccount: builder.Spec.ServiceAccount(), - Namespace: builder.Namespace, + ServiceAccount: serviceAccount.Name, + Namespace: serviceAccount.Namespace, + } + + saSecretRef := registry.SecretRef{ + ServiceAccount: "some-service-account", + Namespace: testNamespace, } when("#Reconcile", func() { it.Before(func() { keychainFactory.AddKeychainForSecretRef(t, secretRef, ®istryfakes.FakeKeychain{}) + keychainFactory.AddKeychainForSecretRef(t, saSecretRef, ®istryfakes.FakeKeychain{}) }) it("saves metadata to the status", func() { @@ -275,6 +309,8 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { clusterBuildpack, extension, clusterExtension, + &signingSecret, + &serviceAccount, }, WantErr: false, WantStatusUpdates: []clientgotesting.UpdateActionImpl{ @@ -291,6 +327,7 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { Fetcher: expectedFetcher, ClusterStack: clusterStack, BuilderSpec: builder.Spec.BuilderSpec, + SigningSecrets: []*corev1.Secret{}, }}, builderCreator.CreateBuilderCalls) }) @@ -334,6 +371,8 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { buildpack, clusterBuildpack, expectedBuilder, + &signingSecret, + &serviceAccount, }, WantErr: false, }) @@ -403,6 +442,8 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { clusterStack, clusterStore, builder, + &signingSecret, + &serviceAccount, }, WantErr: false, }) @@ -417,6 +458,8 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { clusterStack, clusterStore, builder, + &signingSecret, + &serviceAccount, }, WantErr: true, WantStatusUpdates: []clientgotesting.UpdateActionImpl{ @@ -470,6 +513,8 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { notReadyClusterStack, clusterStore, builder, + &signingSecret, + &serviceAccount, }, WantErr: true, WantStatusUpdates: []clientgotesting.UpdateActionImpl{ diff --git a/pkg/reconciler/clusterbuilder/clusterbuilder.go b/pkg/reconciler/clusterbuilder/clusterbuilder.go index 6975fb344..683e25970 100644 --- a/pkg/reconciler/clusterbuilder/clusterbuilder.go +++ b/pkg/reconciler/clusterbuilder/clusterbuilder.go @@ -34,7 +34,11 @@ const ( ) type BuilderCreator interface { - CreateBuilder(ctx context.Context, builderKeychain authn.Keychain, stackKeychain authn.Keychain, fetcher cnb.RemoteBuildpackFetcher, clusterStack *buildapi.ClusterStack, spec buildapi.BuilderSpec) (buildapi.BuilderRecord, error) + CreateBuilder(ctx context.Context, builderKeychain authn.Keychain, stackKeychain authn.Keychain, fetcher cnb.RemoteBuildpackFetcher, clusterStack *buildapi.ClusterStack, spec buildapi.BuilderSpec, serviceAccountSecrets []*corev1.Secret) (buildapi.BuilderRecord, error) +} + +type Fetcher interface { + SecretsForServiceAccount(context.Context, string, string) ([]*corev1.Secret, error) } func NewController( @@ -47,6 +51,7 @@ func NewController( clusterBuildpackInformer buildinformers.ClusterBuildpackInformer, clusterStackInformer buildinformers.ClusterStackInformer, clusterExtensionInformer buildinformers.ClusterExtensionInformer, + secretFetcher Fetcher, ) (*controller.Impl, func()) { c := &Reconciler{ Client: opt.Client, @@ -57,6 +62,7 @@ func NewController( ClusterBuildpackLister: clusterBuildpackInformer.Lister(), ClusterStackLister: clusterStackInformer.Lister(), ClusterExtensionLister: clusterExtensionInformer.Lister(), + SecretFetcher: secretFetcher, } logger := opt.Logger.With( @@ -105,6 +111,7 @@ type Reconciler struct { ClusterBuildpackLister buildlisters.ClusterBuildpackLister ClusterExtensionLister buildlisters.ClusterExtensionLister ClusterStackLister buildlisters.ClusterStackLister + SecretFetcher Fetcher } func (c *Reconciler) Reconcile(ctx context.Context, key string) error { @@ -217,7 +224,12 @@ func (c *Reconciler) reconcileBuilder(ctx context.Context, builder *buildapi.Clu fetcher := cnb.NewRemoteBuildpackFetcher(c.KeychainFactory, clusterStore, nil, clusterBuildpacks, nil, clusterExtensions) - buildRecord, err := c.BuilderCreator.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, clusterStack, builder.Spec.BuilderSpec) + serviceAccountSecrets, err := c.SecretFetcher.SecretsForServiceAccount(ctx, builder.Spec.ServiceAccountRef.Name, builder.Spec.ServiceAccountRef.Namespace) + if err != nil { + return buildapi.BuilderRecord{}, err + } + + buildRecord, err := c.BuilderCreator.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, clusterStack, builder.Spec.BuilderSpec, serviceAccountSecrets) if err != nil { return buildapi.BuilderRecord{}, err } diff --git a/pkg/reconciler/clusterbuilder/clusterbuilder_test.go b/pkg/reconciler/clusterbuilder/clusterbuilder_test.go index b8b11581a..1688b082d 100644 --- a/pkg/reconciler/clusterbuilder/clusterbuilder_test.go +++ b/pkg/reconciler/clusterbuilder/clusterbuilder_test.go @@ -5,6 +5,8 @@ import ( "errors" "testing" + "github.com/pivotal/kpack/pkg/secret/secretfakes" + "github.com/sclevine/spec" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -41,9 +43,12 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { ) var ( - builderCreator = &testhelpers.FakeBuilderCreator{} - keychainFactory = ®istryfakes.FakeKeychainFactory{} - fakeTracker = &testhelpers.FakeTracker{} + builderCreator = &testhelpers.FakeBuilderCreator{} + keychainFactory = ®istryfakes.FakeKeychainFactory{} + fakeTracker = &testhelpers.FakeTracker{} + fakeSecretFetcher = &secretfakes.FakeFetchSecret{ + FakeSecrets: []*corev1.Secret{}, + } ) rt := testhelpers.ReconcilerTester(t, @@ -60,10 +65,30 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { ClusterBuildpackLister: listers.GetClusterBuildpackLister(), ClusterExtensionLister: listers.GetClusterExtensionLister(), ClusterStackLister: listers.GetClusterStackLister(), + SecretFetcher: fakeSecretFetcher, } return &kreconciler.NetworkErrorReconciler{Reconciler: r}, rtesting.ActionRecorderList{fakeClient}, rtesting.EventList{Recorder: record.NewFakeRecorder(10)} }) + signingSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-secret-name", + Namespace: "some-sa-namespace", + }, + } + + serviceAccount := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-sa-name", + Namespace: signingSecret.Namespace, + }, + Secrets: []corev1.ObjectReference{ + { + Name: signingSecret.Name, + }, + }, + } + clusterStore := &buildapi.ClusterStore{ ObjectMeta: metav1.ObjectMeta{ Name: "some-store", @@ -258,6 +283,8 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { builder, clusterBuildpack, clusterExtension, + &signingSecret, + &serviceAccount, }, WantErr: false, WantStatusUpdates: []clientgotesting.UpdateActionImpl{ @@ -274,6 +301,7 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { Fetcher: expectedFetcher, ClusterStack: clusterStack, BuilderSpec: builder.Spec.BuilderSpec, + SigningSecrets: []*corev1.Secret{}, }}, builderCreator.CreateBuilderCalls) }) @@ -316,6 +344,8 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { clusterStack, clusterStore, expectedBuilder, + &signingSecret, + &serviceAccount, }, WantErr: false, }) @@ -375,6 +405,8 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { clusterStack, clusterStore, builder, + &signingSecret, + &serviceAccount, }, WantErr: false, }) @@ -407,6 +439,8 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { clusterStack, clusterStore, builder, + &signingSecret, + &serviceAccount, }, WantErr: true, WantStatusUpdates: []clientgotesting.UpdateActionImpl{ diff --git a/pkg/reconciler/testhelpers/fake_builder_creator.go b/pkg/reconciler/testhelpers/fake_builder_creator.go index 65623f5cb..f7a07f53c 100644 --- a/pkg/reconciler/testhelpers/fake_builder_creator.go +++ b/pkg/reconciler/testhelpers/fake_builder_creator.go @@ -25,9 +25,10 @@ type CreateBuilderArgs struct { Fetcher cnb.RemoteBuildpackFetcher ClusterStack *buildapi.ClusterStack BuilderSpec buildapi.BuilderSpec + SigningSecrets []*corev1.Secret } -func (f *FakeBuilderCreator) CreateBuilder(ctx context.Context, builderKeychain authn.Keychain, stackKeychain authn.Keychain, fetcher cnb.RemoteBuildpackFetcher, clusterStack *buildapi.ClusterStack, spec buildapi.BuilderSpec) (buildapi.BuilderRecord, error) { +func (f *FakeBuilderCreator) CreateBuilder(ctx context.Context, builderKeychain authn.Keychain, stackKeychain authn.Keychain, fetcher cnb.RemoteBuildpackFetcher, clusterStack *buildapi.ClusterStack, spec buildapi.BuilderSpec, signingSecrets []*corev1.Secret) (buildapi.BuilderRecord, error) { f.CreateBuilderCalls = append(f.CreateBuilderCalls, CreateBuilderArgs{ Context: ctx, BuilderKeychain: builderKeychain, @@ -35,6 +36,7 @@ func (f *FakeBuilderCreator) CreateBuilder(ctx context.Context, builderKeychain Fetcher: fetcher, ClusterStack: clusterStack, BuilderSpec: spec, + SigningSecrets: signingSecrets, }) return f.Record, f.CreateErr diff --git a/pkg/registry/fetch.go b/pkg/registry/fetch.go index d20a723f2..0e0299ef6 100644 --- a/pkg/registry/fetch.go +++ b/pkg/registry/fetch.go @@ -1,7 +1,6 @@ package registry import ( - "io/ioutil" "log" "os" "path/filepath" @@ -104,7 +103,7 @@ func handleSource(img v1.Image, dir string) error { } func handleZip(img v1.Image, dir string) error { - tmpDir, err := ioutil.TempDir("", "") + tmpDir, err := os.MkdirTemp("", "") if err != nil { return err } @@ -129,7 +128,7 @@ func handleZip(img v1.Image, dir string) error { } func handleTar(img v1.Image, dir string) error { - tmpDir, err := ioutil.TempDir("", "") + tmpDir, err := os.MkdirTemp("", "") if err != nil { return err } @@ -149,7 +148,7 @@ func handleTar(img v1.Image, dir string) error { } func handleTarGZ(img v1.Image, dir string) error { - tmpDir, err := ioutil.TempDir("", "") + tmpDir, err := os.MkdirTemp("", "") if err != nil { return err } @@ -179,7 +178,7 @@ func getSourceFile(img v1.Image, dir string) (*os.File, error) { return nil, err } - infos, err := ioutil.ReadDir(dir) + infos, err := os.ReadDir(dir) if err != nil { return nil, err } diff --git a/pkg/registry/fetch_test.go b/pkg/registry/fetch_test.go index 0037a6d1d..b66b091a3 100644 --- a/pkg/registry/fetch_test.go +++ b/pkg/registry/fetch_test.go @@ -4,7 +4,6 @@ import ( "bytes" "errors" "fmt" - "io/ioutil" "log" "os" "path/filepath" @@ -42,7 +41,7 @@ func testRegistrySourceFetcher(t *testing.T, when spec.G, it spec.S) { it.Before(func() { var err error - dir, err = ioutil.TempDir("", "") + dir, err = os.MkdirTemp("", "") require.NoError(t, err) }) @@ -56,7 +55,7 @@ func testRegistrySourceFetcher(t *testing.T, when spec.G, it spec.S) { testContentType := testCases[i+1] it("handles unexploded file types: "+testFile, func() { - buf, err := ioutil.ReadFile(filepath.Join("testdata", testFile)) + buf, err := os.ReadFile(filepath.Join("testdata", testFile)) require.NoError(t, err) img := createSourceImage(t, buf, testContentType) @@ -67,7 +66,7 @@ func testRegistrySourceFetcher(t *testing.T, when spec.G, it spec.S) { err = fetcher.Fetch(dir, repoName) require.NoError(t, err) - files, err := ioutil.ReadDir(dir) + files, err := os.ReadDir(dir) require.NoError(t, err) require.Len(t, files, 1) @@ -75,7 +74,7 @@ func testRegistrySourceFetcher(t *testing.T, when spec.G, it spec.S) { require.Equal(t, "testdir", testDir.Name()) require.True(t, testDir.IsDir()) - files, err = ioutil.ReadDir(filepath.Join(dir, testDir.Name())) + files, err = os.ReadDir(filepath.Join(dir, testDir.Name())) require.NoError(t, err) require.Len(t, files, 1) @@ -83,7 +82,7 @@ func testRegistrySourceFetcher(t *testing.T, when spec.G, it spec.S) { require.Equal(t, "testfile", testFile.Name()) require.False(t, testFile.IsDir()) - file, err := ioutil.ReadFile(filepath.Join(dir, testDir.Name(), testFile.Name())) + file, err := os.ReadFile(filepath.Join(dir, testDir.Name(), testFile.Name())) require.NoError(t, err) require.Equal(t, "test file contents", string(file)) @@ -92,7 +91,7 @@ func testRegistrySourceFetcher(t *testing.T, when spec.G, it spec.S) { } it("handles regular source images", func() { - buf, err := ioutil.ReadFile(filepath.Join("testdata", "reg.tar")) + buf, err := os.ReadFile(filepath.Join("testdata", "reg.tar")) require.NoError(t, err) img := createSourceImage(t, buf, "") @@ -103,7 +102,7 @@ func testRegistrySourceFetcher(t *testing.T, when spec.G, it spec.S) { err = fetcher.Fetch(dir, repoName) require.NoError(t, err) - files, err := ioutil.ReadDir(dir) + files, err := os.ReadDir(dir) require.NoError(t, err) require.Len(t, files, 1) @@ -111,7 +110,7 @@ func testRegistrySourceFetcher(t *testing.T, when spec.G, it spec.S) { require.Equal(t, "test.txt", testFile.Name()) require.False(t, testFile.IsDir()) - file, err := ioutil.ReadFile(filepath.Join(dir, testFile.Name())) + file, err := os.ReadFile(filepath.Join(dir, testFile.Name())) require.NoError(t, err) require.Equal(t, "test file contents\n", string(file)) @@ -119,7 +118,7 @@ func testRegistrySourceFetcher(t *testing.T, when spec.G, it spec.S) { }) it("handles directories with improper headers", func() { - buf, err := ioutil.ReadFile(filepath.Join("testdata", "layer.tar")) + buf, err := os.ReadFile(filepath.Join("testdata", "layer.tar")) require.NoError(t, err) img := createSourceImage(t, buf, "") @@ -131,14 +130,14 @@ func testRegistrySourceFetcher(t *testing.T, when spec.G, it spec.S) { require.NoError(t, err) // the vendor/cache directory doesnt have proper headers - _, err = ioutil.ReadFile(filepath.Join(dir, "/vendor/cache/diff-lcs-1.4.2.gem")) + _, err = os.ReadFile(filepath.Join(dir, "/vendor/cache/diff-lcs-1.4.2.gem")) require.NoError(t, err) require.Contains(t, output.String(), "Successfully pulled") }) it("sets the correct file mode", func() { - buf, err := ioutil.ReadFile(filepath.Join("testdata", "tarexe.tar")) + buf, err := os.ReadFile(filepath.Join("testdata", "tarexe.tar")) require.NoError(t, err) img := createSourceImage(t, buf, "tar") @@ -149,7 +148,7 @@ func testRegistrySourceFetcher(t *testing.T, when spec.G, it spec.S) { err = fetcher.Fetch(dir, repoName) require.NoError(t, err) - files, err := ioutil.ReadDir(dir) + files, err := os.ReadDir(dir) require.NoError(t, err) require.Len(t, files, 1) @@ -157,7 +156,7 @@ func testRegistrySourceFetcher(t *testing.T, when spec.G, it spec.S) { require.Equal(t, "test-exe", testDir.Name()) require.True(t, testDir.IsDir()) - files, err = ioutil.ReadDir(filepath.Join(dir, testDir.Name())) + files, err = os.ReadDir(filepath.Join(dir, testDir.Name())) require.NoError(t, err) require.Len(t, files, 1) diff --git a/pkg/secret/secretfakes/fake_fetcher.go b/pkg/secret/secretfakes/fake_fetcher.go new file mode 100644 index 000000000..29656db3b --- /dev/null +++ b/pkg/secret/secretfakes/fake_fetcher.go @@ -0,0 +1,27 @@ +package secretfakes + +import ( + "context" + + corev1 "k8s.io/api/core/v1" +) + +type FakeFetchSecret struct { + FakeSecrets []*corev1.Secret + ShouldError bool + ErrorOut error + + SecretsForServiceAccountFunc func(context.Context, string, string) ([]*corev1.Secret, error) +} + +func (f *FakeFetchSecret) SecretsForServiceAccount(ctx context.Context, serviceAccount, namespace string) ([]*corev1.Secret, error) { + if f.SecretsForServiceAccountFunc != nil { + return f.SecretsForServiceAccount(ctx, serviceAccount, namespace) + } + + if f.ShouldError { + return nil, f.ErrorOut + } + + return f.FakeSecrets, nil +} diff --git a/pkg/secret/volume_secret_reader.go b/pkg/secret/volume_secret_reader.go index a1027b927..41275972f 100644 --- a/pkg/secret/volume_secret_reader.go +++ b/pkg/secret/volume_secret_reader.go @@ -2,7 +2,6 @@ package secret import ( "fmt" - "io/ioutil" "os" "path/filepath" @@ -11,13 +10,13 @@ import ( func ReadBasicAuthSecret(secretVolume, secretName string) (BasicAuth, error) { secretPath := volumeName(secretVolume, secretName) - ub, err := ioutil.ReadFile(filepath.Join(secretPath, corev1.BasicAuthUsernameKey)) + ub, err := os.ReadFile(filepath.Join(secretPath, corev1.BasicAuthUsernameKey)) if err != nil { return BasicAuth{}, err } username := string(ub) - pb, err := ioutil.ReadFile(filepath.Join(secretPath, corev1.BasicAuthPasswordKey)) + pb, err := os.ReadFile(filepath.Join(secretPath, corev1.BasicAuthPasswordKey)) if err != nil { return BasicAuth{}, err } @@ -31,7 +30,7 @@ func ReadBasicAuthSecret(secretVolume, secretName string) (BasicAuth, error) { func ReadSshSecret(secretVolume, secretName string) (SSH, error) { secretPath := volumeName(secretVolume, secretName) - privateKey, err := ioutil.ReadFile(filepath.Join(secretPath, corev1.SSHAuthPrivateKey)) + privateKey, err := os.ReadFile(filepath.Join(secretPath, corev1.SSHAuthPrivateKey)) if err != nil { return SSH{}, err } @@ -39,7 +38,7 @@ func ReadSshSecret(secretVolume, secretName string) (SSH, error) { var knownHosts []byte = nil knownHostsPath := filepath.Join(secretPath, SSHAuthKnownHostsKey) if _, err := os.Stat(knownHostsPath); !os.IsNotExist(err) { - knownHosts, err = ioutil.ReadFile(knownHostsPath) + knownHosts, err = os.ReadFile(knownHostsPath) if err != nil { return SSH{}, err } diff --git a/pkg/secret/volume_secret_reader_test.go b/pkg/secret/volume_secret_reader_test.go index 59bd68aec..da4793010 100644 --- a/pkg/secret/volume_secret_reader_test.go +++ b/pkg/secret/volume_secret_reader_test.go @@ -1,7 +1,6 @@ package secret_test import ( - "io/ioutil" "os" "path" "testing" @@ -21,7 +20,7 @@ func TestVolumeSecretReader(t *testing.T) { func testVolumeSecretReader(t *testing.T, when spec.G, it spec.S) { when("#readBasicAuthSecret", func() { it("returns the username and password from the secret", func() { - testDir, err := ioutil.TempDir("", "secret-volume") + testDir, err := os.MkdirTemp("", "secret-volume") require.NoError(t, err) defer func() { @@ -30,8 +29,8 @@ func testVolumeSecretReader(t *testing.T, when spec.G, it spec.S) { require.NoError(t, os.MkdirAll(path.Join(testDir, "creds"), 0777)) - require.NoError(t, ioutil.WriteFile(path.Join(testDir, "creds", corev1.BasicAuthUsernameKey), []byte("saved-username"), 0600)) - require.NoError(t, ioutil.WriteFile(path.Join(testDir, "creds", corev1.BasicAuthPasswordKey), []byte("saved-password"), 0600)) + require.NoError(t, os.WriteFile(path.Join(testDir, "creds", corev1.BasicAuthUsernameKey), []byte("saved-username"), 0600)) + require.NoError(t, os.WriteFile(path.Join(testDir, "creds", corev1.BasicAuthPasswordKey), []byte("saved-password"), 0600)) auth, err := secret.ReadBasicAuthSecret(testDir, "creds") require.NoError(t, err) @@ -45,7 +44,7 @@ func testVolumeSecretReader(t *testing.T, when spec.G, it spec.S) { when("#readSshSecret", func() { it("returns the private key and known hosts from the secret", func() { - testDir, err := ioutil.TempDir("", "secret-volume") + testDir, err := os.MkdirTemp("", "secret-volume") require.NoError(t, err) defer func() { @@ -54,8 +53,8 @@ func testVolumeSecretReader(t *testing.T, when spec.G, it spec.S) { require.NoError(t, os.MkdirAll(path.Join(testDir, "creds"), 0777)) - require.NoError(t, ioutil.WriteFile(path.Join(testDir, "creds", corev1.SSHAuthPrivateKey), []byte("foobar"), 0600)) - require.NoError(t, ioutil.WriteFile(path.Join(testDir, "creds", secret.SSHAuthKnownHostsKey), []byte("ssh-keyscan"), 0600)) + require.NoError(t, os.WriteFile(path.Join(testDir, "creds", corev1.SSHAuthPrivateKey), []byte("foobar"), 0600)) + require.NoError(t, os.WriteFile(path.Join(testDir, "creds", secret.SSHAuthKnownHostsKey), []byte("ssh-keyscan"), 0600)) auth, err := secret.ReadSshSecret(testDir, "creds") require.NoError(t, err) diff --git a/test/cosign_e2e_test.go b/test/cosign_e2e_test.go new file mode 100644 index 000000000..665d9b1e8 --- /dev/null +++ b/test/cosign_e2e_test.go @@ -0,0 +1,1123 @@ +package test + +import ( + "context" + "fmt" + "testing" + + cosigntesting "github.com/pivotal/kpack/pkg/cosign/testing" + cosignutil "github.com/pivotal/kpack/pkg/cosign/util" + + buildapi "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" + corev1alpha1 "github.com/pivotal/kpack/pkg/apis/core/v1alpha1" + "github.com/sclevine/spec" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func testSignBuilder(t *testing.T, _ spec.G, it spec.S) { + const ( + testNamespace = "test" + dockerSecret = "docker-secret" + serviceAccountName = "image-service-account" + clusterStoreName = "store" + buildpackName = "buildpack" + clusterBuildpackName = "cluster-buildpack" + clusterStackName = "stack" + builderName = "custom-signed-builder" + clusterBuilderName = "custom-signed-cluster-builder" + cosignSecretName = "cosign-creds" + secretRefFormat = "k8s://%s/%s" + ) + + var ( + cfg config + clients *clients + ctx = context.Background() + builtImages map[string]struct{} + ) + + it.Before(func() { + cfg = loadConfig(t) + builtImages = map[string]struct{}{} + + var err error + clients, err = newClients(t) + require.NoError(t, err) + + err = clients.client.KpackV1alpha2().ClusterStores().Delete(ctx, clusterStoreName, metav1.DeleteOptions{}) + if !errors.IsNotFound(err) { + require.NoError(t, err) + } + + err = clients.client.KpackV1alpha2().Buildpacks(testNamespace).Delete(ctx, buildpackName, metav1.DeleteOptions{}) + if !errors.IsNotFound(err) { + require.NoError(t, err) + } + + err = clients.client.KpackV1alpha2().ClusterBuildpacks().Delete(ctx, clusterBuildpackName, metav1.DeleteOptions{}) + if !errors.IsNotFound(err) { + require.NoError(t, err) + } + + err = clients.client.KpackV1alpha2().ClusterStacks().Delete(ctx, clusterStackName, metav1.DeleteOptions{}) + if !errors.IsNotFound(err) { + require.NoError(t, err) + } + + err = clients.client.KpackV1alpha2().ClusterBuilders().Delete(ctx, clusterBuilderName, metav1.DeleteOptions{}) + if !errors.IsNotFound(err) { + require.NoError(t, err) + } + + deleteNamespace(t, ctx, clients, testNamespace) + + _, err = clients.k8sClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNamespace, + Labels: readNamespaceLabelsFromEnv(), + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + }) + + it.After(func() { + for tag := range builtImages { + deleteImageTag(t, tag) + } + }) + + it.Before(func() { + secret, err := cfg.makeRegistrySecret(dockerSecret, testNamespace) + require.NoError(t, err) + + _, err = clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, secret, metav1.CreateOptions{}) + require.NoError(t, err) + + _, err = clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Create(ctx, &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceAccountName, + }, + Secrets: []corev1.ObjectReference{ + { + Name: dockerSecret, + }, + }, + ImagePullSecrets: []corev1.LocalObjectReference{ + { + Name: dockerSecret, + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + _, err = clients.client.KpackV1alpha2().ClusterStores().Create(ctx, &buildapi.ClusterStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterStoreName, + }, + Spec: buildapi.ClusterStoreSpec{ + Sources: []corev1alpha1.ImageSource{ + {Image: "gcr.io/paketo-buildpacks/bellsoft-liberica"}, + {Image: "gcr.io/paketo-buildpacks/gradle"}, + {Image: "gcr.io/paketo-buildpacks/syft"}, + {Image: "gcr.io/paketo-buildpacks/executable-jar"}, + {Image: "gcr.io/paketo-buildpacks/dist-zip"}, + {Image: "gcr.io/paketo-buildpacks/spring-boot"}, + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + _, err = clients.client.KpackV1alpha2().Buildpacks(testNamespace).Create(ctx, &buildapi.Buildpack{ + ObjectMeta: metav1.ObjectMeta{ + Name: buildpackName, + }, + Spec: buildapi.BuildpackSpec{ + ImageSource: corev1alpha1.ImageSource{ + Image: "gcr.io/paketo-buildpacks/bellsoft-liberica", + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + _, err = clients.client.KpackV1alpha2().ClusterBuildpacks().Create(ctx, &buildapi.ClusterBuildpack{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterBuildpackName, + }, + Spec: buildapi.ClusterBuildpackSpec{ + ImageSource: corev1alpha1.ImageSource{ + Image: "gcr.io/paketo-buildpacks/nodejs", + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + _, err = clients.client.KpackV1alpha2().ClusterStacks().Create(ctx, &buildapi.ClusterStack{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterStackName, + }, + Spec: buildapi.ClusterStackSpec{ + Id: "io.buildpacks.stacks.bionic", + BuildImage: buildapi.ClusterStackSpecImage{ + Image: "gcr.io/paketo-buildpacks/build:base-cnb", + }, + RunImage: buildapi.ClusterStackSpecImage{ + Image: "gcr.io/paketo-buildpacks/run:base-cnb", + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + }) + + it("Signs a Builder image successfully when the key is not password-protected", func() { + cosignCredSecret := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName, testNamespace, "", nil) + _, err := clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, &cosignCredSecret, metav1.CreateOptions{}) + require.NoError(t, err) + + serviceAccount, err := clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Get(ctx, serviceAccountName, metav1.GetOptions{}) + require.NoError(t, err) + + if serviceAccount.Secrets == nil { + serviceAccount.Secrets = make([]corev1.ObjectReference, 0) + } + serviceAccount.Secrets = append(serviceAccount.Secrets, corev1.ObjectReference{Name: cosignCredSecret.Name}) + + _, err = clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Update(ctx, serviceAccount, metav1.UpdateOptions{}) + require.NoError(t, err) + + builder, err := clients.client.KpackV1alpha2().Builders(testNamespace).Create(ctx, &buildapi.Builder{ + ObjectMeta: metav1.ObjectMeta{ + Name: builderName, + Namespace: testNamespace, + }, + Spec: buildapi.NamespacedBuilderSpec{ + BuilderSpec: buildapi.BuilderSpec{ + Tag: cfg.newImageTag(), + Stack: corev1.ObjectReference{ + Name: clusterStackName, + Kind: "ClusterStack", + }, + Store: corev1.ObjectReference{ + Name: clusterStoreName, + Kind: "ClusterStore", + }, + Order: []buildapi.BuilderOrderEntry{ + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/nodejs", + }, + }, + }, + }, + }, + { + Group: []buildapi.BuilderBuildpackRef{ + { + ObjectReference: corev1.ObjectReference{ + Name: buildpackName, + Kind: "Buildpack", + }, + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/bellsoft-liberica", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/gradle", + }, + Optional: true, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/syft", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/executable-jar", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/dist-zip", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/spring-boot", + }, + }, + }, + }, + }, + }, + }, + ServiceAccountName: serviceAccountName, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + waitUntilReady(t, ctx, clients, builder) + + updatedBuilder, err := clients.client.KpackV1alpha2().Builders(testNamespace).Get(ctx, builderName, metav1.GetOptions{}) + require.NoError(t, err) + + assert.NotEmpty(t, updatedBuilder.Status.SignaturePaths) + assert.NotNil(t, updatedBuilder.Status.SignaturePaths[0]) + + err = cosigntesting.Verify(t, fmt.Sprintf(secretRefFormat, testNamespace, cosignSecretName), updatedBuilder.Status.LatestImage, nil) + require.NoError(t, err) + }) + + it("Signs a Builder image successfully when the key is password-protected", func() { + const CosignKeyPassword = "password" + + cosignCredSecret := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName, testNamespace, CosignKeyPassword, nil) + _, err := clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, &cosignCredSecret, metav1.CreateOptions{}) + require.NoError(t, err) + + serviceAccount, err := clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Get(ctx, serviceAccountName, metav1.GetOptions{}) + require.NoError(t, err) + + if serviceAccount.Secrets == nil { + serviceAccount.Secrets = make([]corev1.ObjectReference, 0) + } + serviceAccount.Secrets = append(serviceAccount.Secrets, corev1.ObjectReference{Name: cosignCredSecret.Name}) + + _, err = clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Update(ctx, serviceAccount, metav1.UpdateOptions{}) + require.NoError(t, err) + + builder, err := clients.client.KpackV1alpha2().Builders(testNamespace).Create(ctx, &buildapi.Builder{ + ObjectMeta: metav1.ObjectMeta{ + Name: builderName, + Namespace: testNamespace, + }, + Spec: buildapi.NamespacedBuilderSpec{ + BuilderSpec: buildapi.BuilderSpec{ + Tag: cfg.newImageTag(), + Stack: corev1.ObjectReference{ + Name: clusterStackName, + Kind: "ClusterStack", + }, + Store: corev1.ObjectReference{ + Name: clusterStoreName, + Kind: "ClusterStore", + }, + Order: []buildapi.BuilderOrderEntry{ + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/nodejs", + }, + }, + }, + }, + }, + { + Group: []buildapi.BuilderBuildpackRef{ + { + ObjectReference: corev1.ObjectReference{ + Name: buildpackName, + Kind: "Buildpack", + }, + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/bellsoft-liberica", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/gradle", + }, + Optional: true, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/syft", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/executable-jar", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/dist-zip", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/spring-boot", + }, + }, + }, + }, + }, + }, + }, + ServiceAccountName: serviceAccountName, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + waitUntilReady(t, ctx, clients, builder) + + updatedBuilder, err := clients.client.KpackV1alpha2().Builders(testNamespace).Get(ctx, builderName, metav1.GetOptions{}) + require.NoError(t, err) + + assert.NotEmpty(t, updatedBuilder.Status.SignaturePaths) + assert.NotNil(t, updatedBuilder.Status.SignaturePaths[0]) + + err = cosigntesting.Verify(t, fmt.Sprintf(secretRefFormat, testNamespace, cosignSecretName), updatedBuilder.Status.LatestImage, nil) + require.NoError(t, err) + }) + + it("Generates more than one signature for a Builder image successfully when multiple secrets are present", func() { + const CosignKeyPassword = "password" + const cosignSecretName1 = "cosign-credentials-1" + const cosignSecretName2 = "cosign-credentials-2" + + cosignCredSecret1 := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName1, testNamespace, CosignKeyPassword, nil) + _, err := clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, &cosignCredSecret1, metav1.CreateOptions{}) + require.NoError(t, err) + + cosignCredSecret2 := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName2, testNamespace, CosignKeyPassword, nil) + _, err = clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, &cosignCredSecret2, metav1.CreateOptions{}) + require.NoError(t, err) + + serviceAccount, err := clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Get(ctx, serviceAccountName, metav1.GetOptions{}) + require.NoError(t, err) + + if serviceAccount.Secrets == nil { + serviceAccount.Secrets = make([]corev1.ObjectReference, 0) + } + serviceAccount.Secrets = append(serviceAccount.Secrets, + corev1.ObjectReference{Name: cosignCredSecret1.Name}, + corev1.ObjectReference{Name: cosignCredSecret2.Name}) + + _, err = clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Update(ctx, serviceAccount, metav1.UpdateOptions{}) + require.NoError(t, err) + + builder, err := clients.client.KpackV1alpha2().Builders(testNamespace).Create(ctx, &buildapi.Builder{ + ObjectMeta: metav1.ObjectMeta{ + Name: builderName, + Namespace: testNamespace, + }, + Spec: buildapi.NamespacedBuilderSpec{ + BuilderSpec: buildapi.BuilderSpec{ + Tag: cfg.newImageTag(), + Stack: corev1.ObjectReference{ + Name: clusterStackName, + Kind: "ClusterStack", + }, + Store: corev1.ObjectReference{ + Name: clusterStoreName, + Kind: "ClusterStore", + }, + Order: []buildapi.BuilderOrderEntry{ + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/nodejs", + }, + }, + }, + }, + }, + { + Group: []buildapi.BuilderBuildpackRef{ + { + ObjectReference: corev1.ObjectReference{ + Name: buildpackName, + Kind: "Buildpack", + }, + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/bellsoft-liberica", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/gradle", + }, + Optional: true, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/syft", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/executable-jar", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/dist-zip", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/spring-boot", + }, + }, + }, + }, + }, + }, + }, + ServiceAccountName: serviceAccountName, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + waitUntilReady(t, ctx, clients, builder) + + updatedBuilder, err := clients.client.KpackV1alpha2().Builders(testNamespace).Get(ctx, builderName, metav1.GetOptions{}) + require.NoError(t, err) + + assert.NotEmpty(t, updatedBuilder.Status.SignaturePaths) + assert.Equal(t, 2, len(updatedBuilder.Status.SignaturePaths)) + assert.NotNil(t, updatedBuilder.Status.SignaturePaths[0]) + assert.NotNil(t, updatedBuilder.Status.SignaturePaths[1]) + + // tag is assigned to a single signature, but both are still verifiable + err = cosigntesting.Verify(t, fmt.Sprintf(secretRefFormat, testNamespace, cosignSecretName1), updatedBuilder.Status.LatestImage, nil) + require.NoError(t, err) + + err = cosigntesting.Verify(t, fmt.Sprintf(secretRefFormat, testNamespace, cosignSecretName2), updatedBuilder.Status.LatestImage, nil) + require.NoError(t, err) + }) + + it("Saves a failure in the Builder record when signing fails", func() { + const CosignKeyPassword = "password" + const invalidPassword = "wrong-password" + const expectedErrorMessage = "unable to sign" + + cosignCredSecret := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName, testNamespace, CosignKeyPassword, nil) + cosignCredSecret.Data[cosignutil.SecretDataCosignPassword] = []byte(invalidPassword) + + _, err := clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, &cosignCredSecret, metav1.CreateOptions{}) + require.NoError(t, err) + + serviceAccount, err := clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Get(ctx, serviceAccountName, metav1.GetOptions{}) + require.NoError(t, err) + + if serviceAccount.Secrets == nil { + serviceAccount.Secrets = make([]corev1.ObjectReference, 0) + } + serviceAccount.Secrets = append(serviceAccount.Secrets, + corev1.ObjectReference{Name: cosignCredSecret.Name}) + + _, err = clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Update(ctx, serviceAccount, metav1.UpdateOptions{}) + require.NoError(t, err) + + builder, err := clients.client.KpackV1alpha2().Builders(testNamespace).Create(ctx, &buildapi.Builder{ + ObjectMeta: metav1.ObjectMeta{ + Name: builderName, + Namespace: testNamespace, + }, + Spec: buildapi.NamespacedBuilderSpec{ + BuilderSpec: buildapi.BuilderSpec{ + Tag: cfg.newImageTag(), + Stack: corev1.ObjectReference{ + Name: clusterStackName, + Kind: "ClusterStack", + }, + Store: corev1.ObjectReference{ + Name: clusterStoreName, + Kind: "ClusterStore", + }, + Order: []buildapi.BuilderOrderEntry{ + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/nodejs", + }, + }, + }, + }, + }, + { + Group: []buildapi.BuilderBuildpackRef{ + { + ObjectReference: corev1.ObjectReference{ + Name: buildpackName, + Kind: "Buildpack", + }, + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/bellsoft-liberica", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/gradle", + }, + Optional: true, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/syft", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/executable-jar", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/dist-zip", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/spring-boot", + }, + }, + }, + }, + }, + }, + }, + ServiceAccountName: serviceAccountName, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + waitUntilFailed(t, ctx, clients, expectedErrorMessage, builder) + + updatedBuilder, err := clients.client.KpackV1alpha2().Builders(testNamespace).Get(ctx, builderName, metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, updatedBuilder.Status) + + readyConditionBuilder := updatedBuilder.Status.GetCondition(corev1alpha1.ConditionReady) + require.False(t, readyConditionBuilder.IsTrue()) + require.Contains(t, readyConditionBuilder.Message, expectedErrorMessage) + }) + + it("Signs a ClusterBuilder image successfully when the key is not password-protected", func() { + cosignCredSecret := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName, testNamespace, "", nil) + _, err := clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, &cosignCredSecret, metav1.CreateOptions{}) + require.NoError(t, err) + + serviceAccount, err := clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Get(ctx, serviceAccountName, metav1.GetOptions{}) + require.NoError(t, err) + + if serviceAccount.Secrets == nil { + serviceAccount.Secrets = make([]corev1.ObjectReference, 0) + } + serviceAccount.Secrets = append(serviceAccount.Secrets, corev1.ObjectReference{Name: cosignCredSecret.Name}) + + _, err = clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Update(ctx, serviceAccount, metav1.UpdateOptions{}) + require.NoError(t, err) + + clusterBuilder, err := clients.client.KpackV1alpha2().ClusterBuilders().Create(ctx, &buildapi.ClusterBuilder{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterBuilderName, + }, + Spec: buildapi.ClusterBuilderSpec{ + BuilderSpec: buildapi.BuilderSpec{ + Tag: cfg.newImageTag(), + Stack: corev1.ObjectReference{ + Name: clusterStackName, + Kind: "ClusterStack", + }, + Store: corev1.ObjectReference{ + Name: clusterStoreName, + Kind: "ClusterStore", + }, + Order: []buildapi.BuilderOrderEntry{ + { + Group: []buildapi.BuilderBuildpackRef{ + { + ObjectReference: corev1.ObjectReference{ + Name: clusterBuildpackName, + Kind: "ClusterBuildpack", + }, + }, + }, + }, + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/bellsoft-liberica", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/gradle", + }, + Optional: true, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/syft", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/executable-jar", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/dist-zip", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/spring-boot", + }, + }, + }, + }, + }, + }, + }, + ServiceAccountRef: corev1.ObjectReference{ + Namespace: testNamespace, + Name: serviceAccountName, + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + waitUntilReady(t, ctx, clients, clusterBuilder) + + updatedBuilder, err := clients.client.KpackV1alpha2().ClusterBuilders().Get(ctx, clusterBuilderName, metav1.GetOptions{}) + require.NoError(t, err) + + assert.NotEmpty(t, updatedBuilder.Status.SignaturePaths) + assert.NotNil(t, updatedBuilder.Status.SignaturePaths[0]) + + err = cosigntesting.Verify(t, fmt.Sprintf(secretRefFormat, testNamespace, cosignSecretName), updatedBuilder.Status.LatestImage, nil) + require.NoError(t, err) + }) + + it("Signs a ClusterBuilder image successfully when the key is password-protected", func() { + const CosignKeyPassword = "password" + + cosignCredSecret := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName, testNamespace, CosignKeyPassword, nil) + _, err := clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, &cosignCredSecret, metav1.CreateOptions{}) + require.NoError(t, err) + + serviceAccount, err := clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Get(ctx, serviceAccountName, metav1.GetOptions{}) + require.NoError(t, err) + + if serviceAccount.Secrets == nil { + serviceAccount.Secrets = make([]corev1.ObjectReference, 0) + } + serviceAccount.Secrets = append(serviceAccount.Secrets, corev1.ObjectReference{Name: cosignCredSecret.Name}) + + _, err = clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Update(ctx, serviceAccount, metav1.UpdateOptions{}) + require.NoError(t, err) + + clusterBuilder, err := clients.client.KpackV1alpha2().ClusterBuilders().Create(ctx, &buildapi.ClusterBuilder{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterBuilderName, + }, + Spec: buildapi.ClusterBuilderSpec{ + BuilderSpec: buildapi.BuilderSpec{ + Tag: cfg.newImageTag(), + Stack: corev1.ObjectReference{ + Name: clusterStackName, + Kind: "ClusterStack", + }, + Store: corev1.ObjectReference{ + Name: clusterStoreName, + Kind: "ClusterStore", + }, + Order: []buildapi.BuilderOrderEntry{ + { + Group: []buildapi.BuilderBuildpackRef{ + { + ObjectReference: corev1.ObjectReference{ + Name: clusterBuildpackName, + Kind: "ClusterBuildpack", + }, + }, + }, + }, + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/bellsoft-liberica", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/gradle", + }, + Optional: true, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/syft", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/executable-jar", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/dist-zip", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/spring-boot", + }, + }, + }, + }, + }, + }, + }, + ServiceAccountRef: corev1.ObjectReference{ + Namespace: testNamespace, + Name: serviceAccountName, + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + waitUntilReady(t, ctx, clients, clusterBuilder) + + updatedBuilder, err := clients.client.KpackV1alpha2().ClusterBuilders().Get(ctx, clusterBuilderName, metav1.GetOptions{}) + require.NoError(t, err) + + assert.NotEmpty(t, updatedBuilder.Status.SignaturePaths) + assert.NotNil(t, updatedBuilder.Status.SignaturePaths[0]) + + err = cosigntesting.Verify(t, fmt.Sprintf(secretRefFormat, testNamespace, cosignSecretName), updatedBuilder.Status.LatestImage, nil) + require.NoError(t, err) + }) + + it("Generates more than one signature for a ClusterBuilder image successfully when multiple secrets are present", func() { + const cosignKeyPassword = "password" + const cosignSecretName1 = "cosign-credentials-1" + const cosignSecretName2 = "cosign-credentials-2" + + cosignCredSecret1 := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName1, testNamespace, cosignKeyPassword, nil) + _, err := clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, &cosignCredSecret1, metav1.CreateOptions{}) + require.NoError(t, err) + + cosignCredSecret2 := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName2, testNamespace, cosignKeyPassword, nil) + _, err = clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, &cosignCredSecret2, metav1.CreateOptions{}) + require.NoError(t, err) + + serviceAccount, err := clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Get(ctx, serviceAccountName, metav1.GetOptions{}) + require.NoError(t, err) + + if serviceAccount.Secrets == nil { + serviceAccount.Secrets = make([]corev1.ObjectReference, 0) + } + serviceAccount.Secrets = append( + serviceAccount.Secrets, + corev1.ObjectReference{Name: cosignCredSecret1.Name}, + corev1.ObjectReference{Name: cosignCredSecret2.Name}) + + _, err = clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Update(ctx, serviceAccount, metav1.UpdateOptions{}) + require.NoError(t, err) + + clusterBuilder, err := clients.client.KpackV1alpha2().ClusterBuilders().Create(ctx, &buildapi.ClusterBuilder{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterBuilderName, + }, + Spec: buildapi.ClusterBuilderSpec{ + BuilderSpec: buildapi.BuilderSpec{ + Tag: cfg.newImageTag(), + Stack: corev1.ObjectReference{ + Name: clusterStackName, + Kind: "ClusterStack", + }, + Store: corev1.ObjectReference{ + Name: clusterStoreName, + Kind: "ClusterStore", + }, + Order: []buildapi.BuilderOrderEntry{ + { + Group: []buildapi.BuilderBuildpackRef{ + { + ObjectReference: corev1.ObjectReference{ + Name: clusterBuildpackName, + Kind: "ClusterBuildpack", + }, + }, + }, + }, + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/bellsoft-liberica", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/gradle", + }, + Optional: true, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/syft", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/executable-jar", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/dist-zip", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/spring-boot", + }, + }, + }, + }, + }, + }, + }, + ServiceAccountRef: corev1.ObjectReference{ + Namespace: testNamespace, + Name: serviceAccountName, + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + waitUntilReady(t, ctx, clients, clusterBuilder) + + updatedBuilder, err := clients.client.KpackV1alpha2().ClusterBuilders().Get(ctx, clusterBuilderName, metav1.GetOptions{}) + require.NoError(t, err) + + assert.NotEmpty(t, updatedBuilder.Status.SignaturePaths) + assert.Equal(t, 2, len(updatedBuilder.Status.SignaturePaths)) + assert.NotNil(t, updatedBuilder.Status.SignaturePaths[0]) + assert.NotNil(t, updatedBuilder.Status.SignaturePaths[1]) + + err = cosigntesting.Verify(t, fmt.Sprintf(secretRefFormat, testNamespace, cosignSecretName1), updatedBuilder.Status.LatestImage, nil) + require.NoError(t, err) + + err = cosigntesting.Verify(t, fmt.Sprintf(secretRefFormat, testNamespace, cosignSecretName2), updatedBuilder.Status.LatestImage, nil) + require.NoError(t, err) + }) + + it("Saves a failure in the ClusterBuilder record when signing fails", func() { + const cosignKeyPassword = "password" + const invalidPassword = "wrong-password" + const expectedErrorMessage = "unable to sign" + + cosignCredSecret := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName, testNamespace, cosignKeyPassword, nil) + cosignCredSecret.Data[cosignutil.SecretDataCosignPassword] = []byte(invalidPassword) + + _, err = clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, &cosignCredSecret, metav1.CreateOptions{}) + require.NoError(t, err) + + serviceAccount, err := clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Get(ctx, serviceAccountName, metav1.GetOptions{}) + require.NoError(t, err) + + if serviceAccount.Secrets == nil { + serviceAccount.Secrets = make([]corev1.ObjectReference, 0) + } + serviceAccount.Secrets = append( + serviceAccount.Secrets, + corev1.ObjectReference{Name: cosignCredSecret.Name}) + + _, err = clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Update(ctx, serviceAccount, metav1.UpdateOptions{}) + require.NoError(t, err) + + clusterBuilder, err := clients.client.KpackV1alpha2().ClusterBuilders().Create(ctx, &buildapi.ClusterBuilder{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterBuilderName, + }, + Spec: buildapi.ClusterBuilderSpec{ + BuilderSpec: buildapi.BuilderSpec{ + Tag: cfg.newImageTag(), + Stack: corev1.ObjectReference{ + Name: clusterStackName, + Kind: "ClusterStack", + }, + Store: corev1.ObjectReference{ + Name: clusterStoreName, + Kind: "ClusterStore", + }, + Order: []buildapi.BuilderOrderEntry{ + { + Group: []buildapi.BuilderBuildpackRef{ + { + ObjectReference: corev1.ObjectReference{ + Name: clusterBuildpackName, + Kind: "ClusterBuildpack", + }, + }, + }, + }, + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/bellsoft-liberica", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/gradle", + }, + Optional: true, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/syft", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/executable-jar", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/dist-zip", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/spring-boot", + }, + }, + }, + }, + }, + }, + }, + ServiceAccountRef: corev1.ObjectReference{ + Namespace: testNamespace, + Name: serviceAccountName, + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + waitUntilFailed(t, ctx, clients, expectedErrorMessage, clusterBuilder) + + updatedBuilder, err := clients.client.KpackV1alpha2().ClusterBuilders().Get(ctx, clusterBuilderName, metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, updatedBuilder.Status) + + readyConditionBuilder := updatedBuilder.Status.GetCondition(corev1alpha1.ConditionReady) + require.False(t, readyConditionBuilder.IsTrue()) + require.Contains(t, readyConditionBuilder.Message, expectedErrorMessage) + }) +} diff --git a/test/execute_build_test.go b/test/execute_build_test.go index 4da628868..d00617f38 100644 --- a/test/execute_build_test.go +++ b/test/execute_build_test.go @@ -37,13 +37,14 @@ import ( "github.com/pivotal/kpack/pkg/registry" ) -func TestCreateImage(t *testing.T) { +func TestKpackE2E(t *testing.T) { rand.Seed(time.Now().Unix()) spec.Run(t, "CreateImage", testCreateImage) + spec.Run(t, "SignBuilder", testSignBuilder) } -func testCreateImage(t *testing.T, when spec.G, it spec.S) { +func testCreateImage(t *testing.T, _ spec.G, it spec.S) { const ( testNamespace = "test" dockerSecret = "docker-secret" @@ -949,6 +950,26 @@ func waitUntilReady(t *testing.T, ctx context.Context, clients *clients, objects } } +func waitUntilFailed(t *testing.T, ctx context.Context, clients *clients, expectedMessage string, objects ...kmeta.OwnerRefable) { + for _, ob := range objects { + namespace := ob.GetObjectMeta().GetNamespace() + name := ob.GetObjectMeta().GetName() + gvr, _ := meta.UnsafeGuessKindToResource(ob.GetGroupVersionKind()) + + eventually(t, func() bool { + unstructured, err := clients.dynamicClient.Resource(gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) + require.NoError(t, err) + + kResource := &duckv1.KResource{} + err = duck.FromUnstructured(unstructured, kResource) + require.NoError(t, err) + + condition := kResource.Status.GetCondition(apis.ConditionReady) + return condition.IsFalse() && "" != condition.Message && strings.Contains(condition.Message, expectedMessage) + }, 1*time.Second, 8*time.Minute) + } +} + func validateImageCreate(t *testing.T, clients *clients, image *buildapi.Image, expectedResources corev1.ResourceRequirements, expectImage func(*testing.T, v1.Image), expectLogs func(*testing.T, string)) string { ctx, cancel := context.WithCancel(context.Background()) defer cancel()