diff --git a/.github/workflows/konflux-policy.yaml b/.github/workflows/konflux-policy.yaml index d93f5dec..33548dac 100644 --- a/.github/workflows/konflux-policy.yaml +++ b/.github/workflows/konflux-policy.yaml @@ -5,7 +5,11 @@ on: workflow_dispatch: inputs: run_tag: - description: 'Tag tested images' + description: 'Tag tested images as :konflux' + type: boolean + default: true + run_tekton_task_bundle: + description: 'Rebuild tekton-task bundle with updated policy digest' type: boolean default: true run_tekton_catalog: @@ -115,7 +119,9 @@ jobs: run: | set -euo pipefail - echo "$BUNDLE_DIGESTS" | jq -r 'to_entries[] | "\(.key)|\(.value)"' | while IFS='|' read -r url pinned; do + echo "$BUNDLE_DIGESTS" | jq -r ' + to_entries[] | select(.key | test("tekton-task") | not) | + "\(.key)|\(.value)"' | while IFS='|' read -r url pinned; do repo="${url%:*}" echo "Tagging ${pinned} → ${repo}:konflux" skopeo copy --all --digestfile image.digest \ @@ -123,10 +129,10 @@ jobs: echo "Image digest: $(cat image.digest)" done - tekton-catalog-release: - if: inputs.run_tekton_catalog != false + update-task-definitions: + if: ${{ !cancelled() && !failure() && (inputs.run_tekton_task_bundle != false || inputs.run_tekton_catalog != false) }} runs-on: ubuntu-latest - needs: [acceptance-tests] + needs: [acceptance-tests, tag] steps: - uses: actions/checkout@v4 @@ -137,10 +143,85 @@ jobs: - name: Install yq uses: mikefarah/yq@v4 - - name: Extract tested tasks from pinned bundle digests + - name: Log in to quay.io/conforma + env: + CONFORMA_USER: ${{ secrets.BUNDLE_PUSH_USER_CONFORMA }} + CONFORMA_PASS: ${{ secrets.BUNDLE_PUSH_PASS_CONFORMA }} + run: crane auth login quay.io -u "$CONFORMA_USER" -p "$CONFORMA_PASS" + + - name: Extract all tasks from tested bundle env: BUNDLE_DIGESTS: ${{ needs.acceptance-tests.outputs.bundle-digests }} - run: ./hack/extract-tested-tasks.sh /tmp/catalog-tasks + run: | + set -euo pipefail + + BUNDLE=$(echo "$BUNDLE_DIGESTS" | jq -r ' + to_entries[] | select(.key | test("tekton-task")) | .value') + + if [ -z "$BUNDLE" ]; then + echo "ERROR: No tekton-task bundle found in BUNDLE_DIGESTS" + exit 1 + fi + + ./hack/extract-all-bundle-tasks.sh "$BUNDLE" /tmp/catalog-tasks + + - name: Update policy bundle digest in task definitions + run: ./hack/update-policy-digest.sh /tmp/catalog-tasks/tasks/ + + - name: Upload task definitions + uses: actions/upload-artifact@v4 + with: + name: task-definitions + path: /tmp/catalog-tasks/tasks/ + + tekton-task-bundle: + if: ${{ needs.update-task-definitions.result == 'success' && inputs.run_tekton_task_bundle != false }} + runs-on: ubuntu-latest + needs: [update-task-definitions] + + steps: + - name: Install crane + uses: imjasonh/setup-crane@v0.4 + + - name: Install tkn + uses: tektoncd/actions/setup-tektoncd-cli@v1 + with: + version: latest + + - name: Log in to quay.io/conforma + env: + CONFORMA_USER: ${{ secrets.BUNDLE_PUSH_USER_CONFORMA }} + CONFORMA_PASS: ${{ secrets.BUNDLE_PUSH_PASS_CONFORMA }} + run: crane auth login quay.io -u "$CONFORMA_USER" -p "$CONFORMA_PASS" + + - name: Download task definitions + uses: actions/download-artifact@v4 + with: + name: task-definitions + path: /tmp/catalog-tasks/tasks/ + + - name: Push tekton-task bundle + run: | + set -euo pipefail + + TASK_FILES=() + while IFS= read -r f; do + TASK_FILES+=("-f" "$f") + done < <(find /tmp/catalog-tasks/tasks -name '*.yaml' -type f) + + tkn bundle push quay.io/conforma/tekton-task:konflux "${TASK_FILES[@]}" + + tekton-catalog: + if: ${{ needs.update-task-definitions.result == 'success' && inputs.run_tekton_catalog != false }} + runs-on: ubuntu-latest + needs: [update-task-definitions] + + steps: + - name: Download task definitions + uses: actions/download-artifact@v4 + with: + name: task-definitions + path: /tmp/catalog-tasks/tasks/ - name: Generate token id: app-token @@ -165,12 +246,10 @@ jobs: cd tekton-catalog - # Copy extracted tasks into the catalog cp -r /tmp/catalog-tasks/tasks/* tasks/ git add tasks/ - # Check if there are staged changes if git diff --cached --quiet; then echo "No changes to task definitions" exit 0 diff --git a/.shellspec b/.shellspec new file mode 100644 index 00000000..b982ae5a --- /dev/null +++ b/.shellspec @@ -0,0 +1,2 @@ +--shell bash +--require spec_helper diff --git a/hack/extract-all-bundle-tasks.sh b/hack/extract-all-bundle-tasks.sh new file mode 100755 index 00000000..f0b3f4ce --- /dev/null +++ b/hack/extract-all-bundle-tasks.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# Copyright The Conforma Contributors +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +# Extracts all task definitions from a Tekton bundle and arranges them in +# the catalog directory layout: tasks///.yaml +# +# Unlike extract-tested-tasks.sh, this script extracts every task in the +# bundle, not just those referenced in acceptance pipelines. +# +# Usage: +# extract-all-bundle-tasks.sh +# +# Arguments: +# bundle-ref - OCI reference for the Tekton bundle (tag or digest) +# output-dir - Directory to write extracted tasks into + +set -euo pipefail + +BUNDLE="${1:?Usage: extract-all-bundle-tasks.sh }" +OUTPUT_DIR="${2:?Usage: extract-all-bundle-tasks.sh }" + +REPO="${BUNDLE%@*}" +REPO="${REPO%:*}" + +MANIFEST=$(crane manifest "$BUNDLE") + +TASK_LAYERS=$(echo "$MANIFEST" | jq -c ' + .layers[] | select(.annotations["dev.tekton.image.kind"] == "task")') + +if [ -z "$TASK_LAYERS" ]; then + echo "ERROR: No task layers found in bundle ${BUNDLE}" + exit 1 +fi + +COUNT=0 +while IFS= read -r layer; do + [ -z "$layer" ] && continue + + DIGEST=$(echo "$layer" | jq -r '.digest') + NAME=$(echo "$layer" | jq -r '.annotations["dev.tekton.image.name"]') + + echo "Extracting task: $NAME" + + TASK_YAML=$(crane blob "${REPO}@${DIGEST}" | gunzip | tar -xO) + + VERSION=$(echo "$TASK_YAML" | yq eval '.metadata.labels["app.kubernetes.io/version"]' -) + if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then + echo "ERROR: Task $NAME is missing the app.kubernetes.io/version label" + exit 1 + fi + + TASK_DIR="${OUTPUT_DIR}/tasks/${NAME}/${VERSION}" + mkdir -p "$TASK_DIR" + echo "$TASK_YAML" | yq eval '.' -P - > "${TASK_DIR}/${NAME}.yaml" + + echo " Written to ${TASK_DIR}/${NAME}.yaml" + COUNT=$((COUNT + 1)) +done <<< "$TASK_LAYERS" + +echo "" +echo "Done. Extracted ${COUNT} task(s) to ${OUTPUT_DIR}" diff --git a/hack/update-policy-digest.sh b/hack/update-policy-digest.sh new file mode 100755 index 00000000..ca8587e2 --- /dev/null +++ b/hack/update-policy-digest.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Copyright The Conforma Contributors +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +# Updates the POLICY_BUNDLE_DIGEST default value in Tekton task definitions +# to match the current digest of a policy bundle image. +# +# Usage: +# update-policy-digest.sh [] +# +# Arguments: +# tasks-dir - Directory containing task YAML files (searched recursively) +# policy-image - Policy bundle image reference (default: quay.io/conforma/release-policy:konflux) + +set -euo pipefail + +TASKS_DIR="${1:?Usage: update-policy-digest.sh []}" +POLICY_IMAGE="${2:-quay.io/conforma/release-policy:konflux}" + +NEW_DIGEST=$(crane digest "$POLICY_IMAGE") +echo "Release policy digest: ${NEW_DIGEST}" + +TASK_FILES=$(grep -rl 'POLICY_BUNDLE_DIGEST' "$TASKS_DIR" || true) +if [ -z "$TASK_FILES" ]; then + echo "ERROR: No task files contain POLICY_BUNDLE_DIGEST in ${TASKS_DIR}" + exit 1 +fi + +UPDATED=0 +for f in $TASK_FILES; do + OLD_DIGEST=$(yq eval '.spec.params[] | select(.name == "POLICY_BUNDLE_DIGEST") | .default' "$f") + if [ -z "$OLD_DIGEST" ] || [ "$OLD_DIGEST" = "null" ]; then + echo "Warning: could not extract current digest from $f, skipping" + continue + fi + if [ "$OLD_DIGEST" = "$NEW_DIGEST" ]; then + echo "Already up to date in $f" + continue + fi + yq eval '(.spec.params[] | select(.name == "POLICY_BUNDLE_DIGEST") | .default) = "'"${NEW_DIGEST}"'"' -i "$f" + echo "Updated $f: ${OLD_DIGEST} → ${NEW_DIGEST}" + UPDATED=$((UPDATED + 1)) +done + +echo "" +echo "Done. Updated ${UPDATED} file(s)." diff --git a/spec/extract_all_bundle_tasks_spec.sh b/spec/extract_all_bundle_tasks_spec.sh new file mode 100644 index 00000000..850f4f2b --- /dev/null +++ b/spec/extract_all_bundle_tasks_spec.sh @@ -0,0 +1,179 @@ +Describe "extract-all-bundle-tasks.sh" + SCRIPT="./hack/extract-all-bundle-tasks.sh" + + setup() { + setup_tmpdir + create_mock_crane + export CRANE_DATA="${TMPDIR}/crane" + export PATH="${MOCK_BIN}:${PATH}" + } + + cleanup() { + cleanup_tmpdir + } + + Before "setup" + After "cleanup" + + Describe "extracts tasks from a bundle" + setup_bundle() { + cat > "${TMPDIR}/crane/manifest.json" <<'EOF' +{ + "layers": [ + { + "digest": "sha256:aaa111", + "annotations": { + "dev.tekton.image.kind": "task", + "dev.tekton.image.name": "verify-enterprise-contract" + } + }, + { + "digest": "sha256:bbb222", + "annotations": { + "dev.tekton.image.kind": "task", + "dev.tekton.image.name": "collect-keyless-params" + } + } + ] +} +EOF + + create_blob "sha256:aaa111" "apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: verify-enterprise-contract + labels: + app.kubernetes.io/version: \"0.1\" +spec: + params: + - name: POLICY_BUNDLE_DIGEST + default: \"sha256:olddigest\" +" + + create_blob "sha256:bbb222" "apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: collect-keyless-params + labels: + app.kubernetes.io/version: \"0.2\" +spec: + params: + - name: SOME_PARAM + default: value +" + } + + Before "setup_bundle" + + It "creates catalog directory layout" + When run script "$SCRIPT" "quay.io/conforma/tekton-task:latest" "${TMPDIR}/output" + The status should be success + The output should include "Extracted 2 task(s)" + The path "${TMPDIR}/output/tasks/verify-enterprise-contract/0.1/verify-enterprise-contract.yaml" should be file + The path "${TMPDIR}/output/tasks/collect-keyless-params/0.2/collect-keyless-params.yaml" should be file + End + + It "writes valid YAML content" + When run script "$SCRIPT" "quay.io/conforma/tekton-task:latest" "${TMPDIR}/output" + The status should be success + The output should include "Extracted 2 task(s)" + The contents of file "${TMPDIR}/output/tasks/verify-enterprise-contract/0.1/verify-enterprise-contract.yaml" should include "verify-enterprise-contract" + The contents of file "${TMPDIR}/output/tasks/collect-keyless-params/0.2/collect-keyless-params.yaml" should include "collect-keyless-params" + End + End + + Describe "handles digest references" + setup_digest_ref() { + cat > "${TMPDIR}/crane/manifest.json" <<'EOF' +{ + "layers": [ + { + "digest": "sha256:ccc333", + "annotations": { + "dev.tekton.image.kind": "task", + "dev.tekton.image.name": "my-task" + } + } + ] +} +EOF + + create_blob "sha256:ccc333" "apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: my-task + labels: + app.kubernetes.io/version: \"1.0\" +spec: {} +" + } + + Before "setup_digest_ref" + + It "strips digest from repo for blob fetch" + When run script "$SCRIPT" "quay.io/conforma/tekton-task@sha256:abc123" "${TMPDIR}/output" + The status should be success + The output should include "Extracted 1 task(s)" + End + End + + Describe "error cases" + It "fails when no arguments provided" + When run script "$SCRIPT" + The status should be failure + The stderr should include "Usage" + End + + It "fails when output-dir argument is missing" + When run script "$SCRIPT" "quay.io/conforma/tekton-task:latest" + The status should be failure + The stderr should include "Usage" + End + + It "fails when bundle contains no task layers" + cat > "${TMPDIR}/crane/manifest.json" <<'EOF' +{ + "layers": [ + { + "digest": "sha256:notask", + "annotations": { + "dev.tekton.image.kind": "pipeline", + "dev.tekton.image.name": "some-pipeline" + } + } + ] +} +EOF + When run script "$SCRIPT" "quay.io/conforma/tekton-task:latest" "${TMPDIR}/output" + The status should be failure + The output should include "No task layers found" + End + + It "fails when task is missing version label" + cat > "${TMPDIR}/crane/manifest.json" <<'EOF' +{ + "layers": [ + { + "digest": "sha256:nover", + "annotations": { + "dev.tekton.image.kind": "task", + "dev.tekton.image.name": "bad-task" + } + } + ] +} +EOF + + create_blob "sha256:nover" "apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: bad-task +spec: {} +" + + When run script "$SCRIPT" "quay.io/conforma/tekton-task:latest" "${TMPDIR}/output" + The status should be failure + The output should include "missing the app.kubernetes.io/version label" + End + End +End diff --git a/spec/spec_helper.sh b/spec/spec_helper.sh new file mode 100644 index 00000000..012a06ec --- /dev/null +++ b/spec/spec_helper.sh @@ -0,0 +1,61 @@ +set -euo pipefail + +# Create a temporary directory for each test group and clean up after. +setup_tmpdir() { + TMPDIR=$(mktemp -d) + MOCK_BIN="${TMPDIR}/bin" + mkdir -p "$MOCK_BIN" +} + +cleanup_tmpdir() { + rm -rf "$TMPDIR" +} + +# Create a mock crane script that dispatches on subcommand. +# Uses files in $TMPDIR/crane/ to store mock data: +# manifest.json — returned by "crane manifest" +# blobs/.tar.gz — returned by "crane blob repo@" +# digest.txt — returned by "crane digest" +create_mock_crane() { + mkdir -p "${TMPDIR}/crane/blobs" + cat > "${MOCK_BIN}/crane" <<'CRANE_EOF' +#!/usr/bin/env bash +set -euo pipefail +CRANE_DATA="${CRANE_DATA:?CRANE_DATA not set}" +case "${1:-}" in + manifest) + cat "${CRANE_DATA}/manifest.json" + ;; + blob) + ref="$2" + digest="${ref#*@}" + cat "${CRANE_DATA}/blobs/${digest}.tar.gz" + ;; + digest) + cat "${CRANE_DATA}/digest.txt" + ;; + auth) + exit 0 + ;; + *) + echo "mock crane: unknown subcommand: $1" >&2 + exit 1 + ;; +esac +CRANE_EOF + chmod +x "${MOCK_BIN}/crane" +} + +# Create a gzipped tar blob containing a single YAML file. +# Usage: create_blob +create_blob() { + local digest="$1" + local yaml="$2" + local blob_dir="${TMPDIR}/crane/blobs" + local staging="${TMPDIR}/blob-staging" + + mkdir -p "$blob_dir" "$staging" + printf '%s' "$yaml" > "${staging}/task.yaml" + tar -cf - -C "$staging" task.yaml | gzip > "${blob_dir}/${digest}.tar.gz" + rm -rf "$staging" +} diff --git a/spec/update_policy_digest_spec.sh b/spec/update_policy_digest_spec.sh new file mode 100644 index 00000000..a569fef8 --- /dev/null +++ b/spec/update_policy_digest_spec.sh @@ -0,0 +1,160 @@ +Describe "update-policy-digest.sh" + SCRIPT="./hack/update-policy-digest.sh" + MOCK_NEW_DIGEST="sha256:newdigestnewdigestnewdigestnewdigestnewdigestnewdigestnewdigest00" + + setup() { + setup_tmpdir + create_mock_crane + echo "$MOCK_NEW_DIGEST" > "${TMPDIR}/crane/digest.txt" + export CRANE_DATA="${TMPDIR}/crane" + export PATH="${MOCK_BIN}:${PATH}" + } + + cleanup() { + cleanup_tmpdir + } + + Before "setup" + After "cleanup" + + create_task_yaml() { + local dir="$1" + local name="$2" + local digest="$3" + mkdir -p "$dir" + cat > "${dir}/${name}.yaml" < "${dir}/odd-task.yaml" <<'EOF' +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: odd-task +spec: + steps: + - name: run + script: echo POLICY_BUNDLE_DIGEST +EOF + } + + Before "setup_no_param" + + It "warns and skips" + When run script "$SCRIPT" "${TMPDIR}/tasks" + The status should be success + The output should include "Warning: could not extract current digest" + The output should include "Updated 0 file(s)" + End + End + + Describe "custom policy image" + setup_custom() { + create_task_yaml "${TMPDIR}/tasks/verify-ec/0.1" "verify-ec" "sha256:oldoldoldoldoldoldoldoldoldoldoldoldoldoldoldoldoldoldoldoldold0" + } + + Before "setup_custom" + + It "accepts a custom policy image reference" + When run script "$SCRIPT" "${TMPDIR}/tasks" "quay.io/custom/policy:v1" + The status should be success + The output should include "Updated 1 file(s)" + End + End + + Describe "error cases" + It "fails when no arguments provided" + When run script "$SCRIPT" + The status should be failure + The stderr should include "Usage" + End + + It "fails when no task files contain POLICY_BUNDLE_DIGEST" + local dir="${TMPDIR}/tasks/no-digest/0.1" + mkdir -p "$dir" + cat > "${dir}/no-digest.yaml" <<'EOF' +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: no-digest +spec: + params: + - name: SOME_OTHER_PARAM + default: value +EOF + When run script "$SCRIPT" "${TMPDIR}/tasks" + The status should be failure + The output should include "No task files contain POLICY_BUNDLE_DIGEST" + End + End +End