diff --git a/release/cloudbuild-nomulus.yaml b/release/cloudbuild-nomulus.yaml index cd0f5bbc588..ef4d9f44de3 100644 --- a/release/cloudbuild-nomulus.yaml +++ b/release/cloudbuild-nomulus.yaml @@ -53,42 +53,14 @@ steps: ./gradlew :jetty:buildNomulusImage :proxy:buildProxyImage :core:buildToolImage\ -PmavenUrl=gcs://domain-registry-maven-repository/maven \ -PpluginsUrl=gcs://domain-registry-maven-repository/plugins - docker tag nomulus gcr.io/${PROJECT_ID}/nomulus:${TAG_NAME} - docker tag nomulus gcr.io/${PROJECT_ID}/nomulus:latest - docker push gcr.io/${PROJECT_ID}/nomulus:${TAG_NAME} - docker push gcr.io/${PROJECT_ID}/nomulus:latest - docker tag proxy gcr.io/${PROJECT_ID}/proxy:${TAG_NAME} - docker tag proxy gcr.io/${PROJECT_ID}/proxy:latest - docker push gcr.io/${PROJECT_ID}/proxy:${TAG_NAME} - docker push gcr.io/${PROJECT_ID}/proxy:latest + docker tag nomulus us-docker.pkg.dev/${PROJECT_ID}/staging/nomulus:${TAG_NAME} + docker tag nomulus us-docker.pkg.dev/${PROJECT_ID}/staging/nomulus:latest + docker tag proxy us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:${TAG_NAME} + docker tag proxy us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:latest docker tag nomulus-tool gcr.io/${PROJECT_ID}/nomulus-tool:${TAG_NAME} docker tag nomulus-tool gcr.io/${PROJECT_ID}/nomulus-tool:latest docker push gcr.io/${PROJECT_ID}/nomulus-tool:${TAG_NAME} docker push gcr.io/${PROJECT_ID}/nomulus-tool:latest -# Sign nomulus and proxy images. -- name: 'gcr.io/${PROJECT_ID}/builder:latest' - entrypoint: /bin/bash - args: - - -c - - | - nomulus_digest=$(gcloud container images list-tags gcr.io/${PROJECT_ID}/nomulus \ - --format="get(digest)" --filter="tags = ${TAG_NAME}") - echo "$nomulus_digest" > /workspace/nomulus_digest - proxy_digest=$(gcloud container images list-tags gcr.io/${PROJECT_ID}/proxy \ - --format="get(digest)" --filter="tags = ${TAG_NAME}") - echo "$proxy_digest" > /workspace/proxy_digest - gcloud --project=${PROJECT_ID} beta container binauthz attestations \ - sign-and-create --artifact-url=gcr.io/${PROJECT_ID}/nomulus@$nomulus_digest \ - --attestor=build-attestor --attestor-project=${PROJECT_ID} \ - --keyversion-project=${PROJECT_ID} --keyversion-location=global \ - --keyversion-keyring=attestor-keys --keyversion-key=signing \ - --keyversion=1 - gcloud --project=${PROJECT_ID} beta container binauthz attestations \ - sign-and-create --artifact-url=gcr.io/${PROJECT_ID}/proxy@$proxy_digest \ - --attestor=build-attestor --attestor-project=${PROJECT_ID} \ - --keyversion-project=${PROJECT_ID} --keyversion-location=global \ - --keyversion-keyring=attestor-keys --keyversion-key=signing \ - --keyversion=1 # Get the tool image digest and substitute in the digest in other GCB files. - name: 'gcr.io/${PROJECT_ID}/builder:latest' entrypoint: /bin/bash @@ -177,35 +149,6 @@ steps: cp db/build/libs/schema.jar output/ cp core/build/libs/nomulus-public.jar output/ cp core/build/libs/nomulus-tests-alldeps.jar output/ -# Create a release in Cloud Deploy to trigger the deployment pipeline -- name: 'gcr.io/${PROJECT_ID}/builder:latest' - entrypoint: /bin/bash - args: - - -c - - | - set -e - echo "=============================================" - echo "Triggering Google Cloud Deploy Release" - echo "=============================================" - echo "Tag Name: ${TAG_NAME}" - echo "Project ID: ${PROJECT_ID}" - pipeline="deploy-nomulus" - region="us-central1" - # Release names must consist of lowercase letters, numbers, and hyphens. - release_name=$(echo "${TAG_NAME}" | tr '[:upper:]' '[:lower:]' | tr '_' '-') - echo "Release Name: $release_name" - echo "=============================================" - # Read the pre-fetched image digest from the workspace file - nomulus_digest=$(cat /workspace/nomulus_digest) - proxy_digest=$(cat /workspace/proxy_digest) - gcloud deploy releases create "$release_name" \ - --delivery-pipeline="$pipeline" \ - --region="$region" \ - --project=${PROJECT_ID} \ - --images="gcr.io/${PROJECT_ID}/nomulus=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest},gcr.io/${PROJECT_ID}/proxy=gcr.io/${PROJECT_ID}/proxy@${proxy_digest}" \ - --source=. \ - --skaffold-file=release/clouddeploy/skaffold.yaml \ - --deploy-parameters="deployed_image=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest},base_image=us-docker.pkg.dev/${PROJECT_ID}/gcr.io/nomulus,tag_name=${TAG_NAME},project_id=${PROJECT_ID}" # The tarballs and jars to upload to GCS. artifacts: objects: @@ -228,11 +171,12 @@ artifacts: - 'release/cloudbuild-restart-proxies-*.yaml' - 'jetty/kubernetes/*.yaml' - 'jetty/kubernetes/gateway/*.yaml' -# The images are already uploaded, but we still need to include them there so that -# the GCB pubsub message contains them (for Spinnaker to consume). +# Images to upload to staging repository for build provenance generation. images: - - 'gcr.io/${PROJECT_ID}/nomulus:${TAG_NAME}' - - 'gcr.io/${PROJECT_ID}/proxy:${TAG_NAME}' + - 'us-docker.pkg.dev/${PROJECT_ID}/staging/nomulus:${TAG_NAME}' + - 'us-docker.pkg.dev/${PROJECT_ID}/staging/nomulus:latest' + - 'us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:${TAG_NAME}' + - 'us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:latest' timeout: 7200s options: machineType: 'E2_HIGHCPU_32' \ No newline at end of file diff --git a/release/cloudbuild-proxy.yaml b/release/cloudbuild-proxy.yaml index f628d2a11be..c73b4029455 100644 --- a/release/cloudbuild-proxy.yaml +++ b/release/cloudbuild-proxy.yaml @@ -18,37 +18,20 @@ steps: - :proxy:buildProxyImage - -PmavenUrl=gcs://domain-registry-maven-repository/maven - -PpluginsUrl=gcs://domain-registry-maven-repository/plugins -# Tag and push the image. We can't let Cloud Build's default processing do that for us -# because we need to push the image before we can sign it in the following step. +# Tag the image for staging repository. - name: 'gcr.io/${PROJECT_ID}/builder:latest' entrypoint: /bin/bash args: - -c - | set -e - docker tag proxy gcr.io/${PROJECT_ID}/proxy:${TAG_NAME} - docker tag proxy gcr.io/${PROJECT_ID}/proxy:latest - docker push gcr.io/${PROJECT_ID}/proxy:${TAG_NAME} - docker push gcr.io/${PROJECT_ID}/proxy:latest + docker tag proxy us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:${TAG_NAME} + docker tag proxy us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:latest dir: 'proxy' -# Get the image digest, sign it and substitute in the digest in the tagging yaml file. -- name: 'gcr.io/${PROJECT_ID}/builder:latest' - entrypoint: /bin/bash - args: - - -c - - | - set -e - digest=$(gcloud container images list-tags gcr.io/${PROJECT_ID}/proxy \ - --format="get(digest)" --filter="tags = ${TAG_NAME}") - gcloud --project=${PROJECT_ID} alpha container binauthz attestations \ - sign-and-create --artifact-url=gcr.io/${PROJECT_ID}/proxy@$digest \ - --attestor=build-attestor --attestor-project=${PROJECT_ID} \ - --keyversion-project=${PROJECT_ID} --keyversion-location=global \ - --keyversion-keyring=attestor-keys --keyversion-key=signing \ - --keyversion=1 -# Images to upload to GCR. Even though the image has already been uploaded, we still include it -# here so that the GCB pubsub message contains it (for Spinnaker to consume). -images: ['gcr.io/${PROJECT_ID}/proxy:${TAG_NAME}'] +# Images to upload to staging repository for build provenance generation. +images: + - 'us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:${TAG_NAME}' + - 'us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:latest' # Config files to upload to GCS. artifacts: objects: diff --git a/release/cloudbuild-release.yaml b/release/cloudbuild-release.yaml index b9453dca264..d3f3074ec03 100644 --- a/release/cloudbuild-release.yaml +++ b/release/cloudbuild-release.yaml @@ -319,11 +319,12 @@ steps: rm ${gradle_bin} sed -i s%services.gradle.org/distributions%storage.googleapis.com/${gcs_loc}% \ gradle/wrapper/gradle-wrapper.properties -# Conditionally trigger the appropriate build based on the tag format. +# Conditionally trigger the appropriate build based on the tag format and promote artifacts. - name: 'gcr.io/cloud-builders/gcloud' entrypoint: 'bash' env: - 'TAG_NAME=${TAG_NAME}' + - 'PROJECT_ID=${PROJECT_ID}' args: - -c - | @@ -332,14 +333,55 @@ steps: if [[ "$TAG_NAME" =~ ^nomulus-20[0-9]{2}[0-1][0-9][0-3][0-9]-RC[0-9]{2}$ ]]; then echo "Tag format matches a nomulus release. Triggering nomulus build..." gcloud builds submit . --config=release/cloudbuild-nomulus.yaml --substitutions="TAG_NAME=$TAG_NAME" + echo "Promoting and signing nomulus release artifacts..." + bash ./release/promote_artifacts.sh "nomulus" "$TAG_NAME" "$PROJECT_ID" # Check for a proxy release tag (e.g., "proxy-v1.2.3") elif [[ "$TAG_NAME" =~ ^proxy-20[0-9]{2}[0-1][0-9][0-3][0-9]-RC[0-9]{2}$ ]]; then echo "Tag format matches a proxy release. Triggering proxy build..." gcloud builds submit . --config=release/cloudbuild-proxy.yaml --substitutions="TAG_NAME=$TAG_NAME" + echo "Promoting and signing proxy release artifacts..." + bash ./release/promote_artifacts.sh "proxy" "$TAG_NAME" "$PROJECT_ID" else echo "Tag format '$TAG_NAME' does not match a known release type. Exiting." exit 1 fi +# Create a release in Cloud Deploy to trigger the deployment pipeline +- name: 'gcr.io/$PROJECT_ID/builder:latest' + entrypoint: /bin/bash + env: + - 'TAG_NAME=${TAG_NAME}' + - 'PROJECT_ID=${PROJECT_ID}' + args: + - -c + - | + set -e + if [[ "$TAG_NAME" =~ ^nomulus-20[0-9]{2}[0-1][0-9][0-3][0-9]-RC[0-9]{2}$ ]]; then + echo "=============================================" + echo "Triggering Google Cloud Deploy Release" + echo "=============================================" + echo "Tag Name: ${TAG_NAME}" + echo "Project ID: ${PROJECT_ID}" + pipeline="deploy-nomulus" + region="us-central1" + # Release names must consist of lowercase letters, numbers, and hyphens. + release_name=$(echo "${TAG_NAME}" | tr '[:upper:]' '[:lower:]' | tr '_' '-') + echo "Release Name: $release_name" + echo "=============================================" + nomulus_digest=$(gcloud artifacts docker images describe \ + "us-docker.pkg.dev/${PROJECT_ID}/gcr.io/nomulus:${TAG_NAME}" \ + --format="value(image_summary.digest)") + proxy_digest=$(gcloud artifacts docker images describe \ + "us-docker.pkg.dev/${PROJECT_ID}/gcr.io/proxy:${TAG_NAME}" \ + --format="value(image_summary.digest)") + gcloud deploy releases create "$release_name" \ + --delivery-pipeline="$pipeline" \ + --region="$region" \ + --project=${PROJECT_ID} \ + --images="gcr.io/${PROJECT_ID}/nomulus=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest},gcr.io/${PROJECT_ID}/proxy=gcr.io/${PROJECT_ID}/proxy@${proxy_digest}" \ + --source=. \ + --skaffold-file=release/clouddeploy/skaffold.yaml \ + --deploy-parameters="deployed_image=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest},base_image=us-docker.pkg.dev/${PROJECT_ID}/gcr.io/nomulus,tag_name=${TAG_NAME},project_id=${PROJECT_ID}" + fi # Run the BEAM smoke test, using the builder and pipeline image just created - name: 'gcr.io/$PROJECT_ID/builder:latest' entrypoint: /bin/bash diff --git a/release/promote_artifacts.sh b/release/promote_artifacts.sh new file mode 100755 index 00000000000..0de793c5caa --- /dev/null +++ b/release/promote_artifacts.sh @@ -0,0 +1,164 @@ +#!/bin/bash +# Copyright 2026 The Nomulus Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This script handles post-build promotion and BinAuthz signing: +# 1. Obtains the image digests from the staging repository. +# 2. Promotes the images from staging to gcr.io via the Artifact Registry +# promoteArtifact API (evaluating BCID exit gate policy and attaching VSA). +# 3. Signs the promoted gcr.io images with Binary Authorization. +# +# Usage: +# release/promote_artifacts.sh +# where is "nomulus" or "proxy". + +set -e + +if [[ $# -ne 3 ]]; then + echo "Usage: $0 " + exit 1 +fi + +RELEASE_TYPE="$1" +TAG_NAME="$2" +PROJECT_ID="$3" + +LOCATION="us" +DEST_REPO="gcr.io" +SRC_REPO="staging" +ENDPOINT="https://artifactregistry.googleapis.com/v1/projects/${PROJECT_ID}/locations/${LOCATION}/repositories/${DEST_REPO}:promoteArtifact" + +promote_artifact() { + local pkg="$1" + local digest="$2" + + echo "================================================================================" + echo "Promoting ${pkg}:${TAG_NAME} (${digest}) to ${DEST_REPO} via Artifact Registry" + echo "================================================================================" + + local token + token=$(gcloud auth print-access-token) + + local payload + payload=$(printf '{"source_repository":"projects/%s/locations/%s/repositories/%s","source_version":"projects/%s/locations/%s/repositories/%s/packages/%s/versions/%s","include_all_tags":true,"overwrite_tags":true,"attachment_behavior":"PUBLIC_BCID_VSA_ONLY"}' \ + "${PROJECT_ID}" "${LOCATION}" "${SRC_REPO}" \ + "${PROJECT_ID}" "${LOCATION}" "${SRC_REPO}" "${pkg}" "${digest}") + + local operation_json + operation_json=$(curl -s --no-progress-meter \ + -H "Authorization: Bearer ${token}" \ + -H "Content-Type: application/json" \ + -X POST "${ENDPOINT}" \ + -d "${payload}") + + local operation_name + operation_name=$(echo "${operation_json}" | python3 -c "import sys, json; d=json.load(sys.stdin); sys.exit(f'ERROR: {d[\"error\"]}') if 'error' in d else print(d.get('name', ''))") + if [[ $? -ne 0 || -z "${operation_name}" ]]; then + echo "Failed to initiate promotion for ${pkg}: ${operation_json}" + exit 1 + fi + + echo "Promotion operation started: ${operation_name}" + echo "Polling operation status until completion..." + + local max_attempts=60 + local attempt=0 + while true; do + attempt=$((attempt + 1)) + if [[ ${attempt} -gt ${max_attempts} ]]; then + echo "ERROR: Timed out waiting for promotion operation on ${pkg} to complete." + exit 1 + fi + + local status_json + status_json=$(gcloud artifacts operations describe "${operation_name}" \ + --project="${PROJECT_ID}" \ + --location="${LOCATION}" \ + --format="json" 2>/dev/null || true) + + if [[ -z "${status_json}" ]]; then + echo "Warning: Failed to query operation status; retrying in 5s..." + sleep 5 + continue + fi + + local result + result=$(echo "${status_json}" | python3 -c "import sys, json; d=json.load(sys.stdin); print('IN_PROGRESS' if not d.get('done') else ('ERROR: ' + json.dumps(d['error']) if 'error' in d else 'SUCCESS'))" 2>/dev/null || echo "RETRY") + + if [[ "${result}" == "SUCCESS" ]]; then + echo "================================================================================" + echo "Artifact promotion succeeded! BCID VSA attached to ${DEST_REPO}/${pkg}." + echo "================================================================================" + break + elif [[ "${result}" =~ ^ERROR: ]]; then + echo "================================================================================" + echo "ERROR: Artifact promotion failed BCID policy evaluation or execution for ${pkg}:" + echo "${result}" + echo "================================================================================" + exit 1 + elif [[ "${result}" == "IN_PROGRESS" || "${result}" == "RETRY" ]]; then + echo "Operation in progress... (attempt ${attempt}/${max_attempts}), retrying in 5s..." + sleep 5 + fi + done +} + +sign_binauthz() { + local image_name="$1" + local digest="$2" + + echo "================================================================================" + echo "Signing ${image_name}@${digest} with Binary Authorization" + echo "================================================================================" + + gcloud --project="${PROJECT_ID}" beta container binauthz attestations \ + sign-and-create --artifact-url="gcr.io/${PROJECT_ID}/${image_name}@${digest}" \ + --attestor=build-attestor --attestor-project="${PROJECT_ID}" \ + --keyversion-project="${PROJECT_ID}" --keyversion-location=global \ + --keyversion-keyring=attestor-keys --keyversion-key=signing \ + --keyversion=1 +} + +if [[ "${RELEASE_TYPE}" == "nomulus" ]]; then + echo "Retrieving digests from staging for nomulus release..." + nomulus_digest=$(gcloud artifacts docker images describe \ + "us-docker.pkg.dev/${PROJECT_ID}/staging/nomulus:${TAG_NAME}" \ + --format="value(image_summary.digest)") + proxy_digest=$(gcloud artifacts docker images describe \ + "us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:${TAG_NAME}" \ + --format="value(image_summary.digest)") + + echo "nomulus digest: ${nomulus_digest}" + echo "proxy digest: ${proxy_digest}" + + promote_artifact "nomulus" "${nomulus_digest}" + promote_artifact "proxy" "${proxy_digest}" + + sign_binauthz "nomulus" "${nomulus_digest}" + sign_binauthz "proxy" "${proxy_digest}" + +elif [[ "${RELEASE_TYPE}" == "proxy" ]]; then + echo "Retrieving digest from staging for proxy release..." + proxy_digest=$(gcloud artifacts docker images describe \ + "us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:${TAG_NAME}" \ + --format="value(image_summary.digest)") + + echo "proxy digest: ${proxy_digest}" + + promote_artifact "proxy" "${proxy_digest}" + sign_binauthz "proxy" "${proxy_digest}" +else + echo "Unknown release type: ${RELEASE_TYPE}" + exit 1 +fi