From 2aa7a677d0224c44b82bd745b622d29b1df8d162 Mon Sep 17 00:00:00 2001 From: Vincent Shen Date: Fri, 12 Dec 2025 07:45:04 -0800 Subject: [PATCH] Add PR bundle image This commit introduces a new task to the Tekton pipeline that waits for the operator image to become available. The task includes parameters for the operator image, maximum wait time, and check interval, along with a script to handle the waiting logic. The init task is updated to run after this new task, ensuring proper sequencing in the pipeline. --- ...rity-operator-bundle-dev-pull-request.yaml | 24 ++++ .../tasks/wait-for-operator-image-task.yaml | 127 ++++++++++++++++++ bundle-hack/update_csv.go | 52 ++++--- bundle.openshift.Dockerfile | 4 +- 4 files changed, 188 insertions(+), 19 deletions(-) create mode 100644 .tekton/tasks/wait-for-operator-image-task.yaml diff --git a/.tekton/file-integrity-operator-bundle-dev-pull-request.yaml b/.tekton/file-integrity-operator-bundle-dev-pull-request.yaml index 68fe38e95..7e9ea361e 100644 --- a/.tekton/file-integrity-operator-bundle-dev-pull-request.yaml +++ b/.tekton/file-integrity-operator-bundle-dev-pull-request.yaml @@ -135,6 +135,25 @@ spec: name: CHAINS-GIT_COMMIT value: $(tasks.clone-repository.results.commit) tasks: + - name: wait-for-operator-image + params: + - name: OPERATOR_IMAGE + value: quay.io/redhat-user-workloads/ocp-isc-tenant/file-integrity-operator-dev:on-pr-$(params.revision) + - name: ARCHITECTURES + value: "linux/amd64,linux/ppc64le,linux/s390x" + - name: MAX_WAIT_TIME + value: "30m" + - name: CHECK_INTERVAL + value: "30s" + taskRef: + resolver: git + params: + - name: url + value: $(params.git-url) + - name: revision + value: $(params.revision) + - name: pathInRepo + value: ".tekton/tasks/wait-for-operator-image-task.yaml" - name: init params: - name: image-url @@ -145,6 +164,8 @@ spec: value: $(params.skip-checks) - name: enable-cache-proxy value: $(params.enable-cache-proxy) + runAfter: + - wait-for-operator-image taskRef: params: - name: name @@ -230,6 +251,7 @@ spec: - name: BUILD_ARGS value: - $(params.build-args[*]) + - OPERATOR_IMAGE=$(tasks.wait-for-operator-image.results.OPERATOR_IMAGE_WITH_DIGEST) - name: BUILD_ARGS_FILE value: $(params.build-args-file) - name: PRIVILEGED_NESTED @@ -246,6 +268,7 @@ spec: value: $(tasks.init.results.no-proxy) runAfter: - prefetch-dependencies + - wait-for-operator-image taskRef: params: - name: name @@ -458,6 +481,7 @@ spec: - name: BUILD_ARGS value: - $(params.build-args[*]) + - OPERATOR_IMAGE=$(tasks.wait-for-operator-image.results.OPERATOR_IMAGE_WITH_DIGEST) - name: BUILD_ARGS_FILE value: $(params.build-args-file) - name: SOURCE_ARTIFACT diff --git a/.tekton/tasks/wait-for-operator-image-task.yaml b/.tekton/tasks/wait-for-operator-image-task.yaml new file mode 100644 index 000000000..766a3ed4d --- /dev/null +++ b/.tekton/tasks/wait-for-operator-image-task.yaml @@ -0,0 +1,127 @@ +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: wait-for-operator-image +spec: + description: | + Waits for an operator image to be available in the registry with all required architectures. + Returns the full image reference with digest (registry/repo/image@sha256:digest). + params: + - name: OPERATOR_IMAGE + description: The operator image to wait for (with tag, e.g., quay.io/.../image:tag) + type: string + - name: ARCHITECTURES + description: Comma-separated list of architectures to wait for (e.g., "linux/amd64,linux/ppc64le,linux/s390x") + type: string + default: "linux/amd64,linux/ppc64le,linux/s390x" + - name: MAX_WAIT_TIME + description: Maximum time to wait (e.g., 30m, 1h) + type: string + default: "30m" + - name: CHECK_INTERVAL + description: Interval between checks (e.g., 30s, 1m) + type: string + default: "30s" + results: + - name: OPERATOR_IMAGE_WITH_DIGEST + description: Full operator image reference with digest (registry/repo/image@sha256:digest) + steps: + - name: wait + image: quay.io/openshift/origin-cli:latest + script: | + #!/bin/bash + set -e + + OPERATOR_IMAGE=$(params.OPERATOR_IMAGE) + ARCHITECTURES_PARAM=$(params.ARCHITECTURES) + MAX_WAIT_TIME=$(params.MAX_WAIT_TIME) + CHECK_INTERVAL=$(params.CHECK_INTERVAL) + + echo "Waiting for operator image to be available: ${OPERATOR_IMAGE}" + echo "Required architectures: ${ARCHITECTURES_PARAM}" + echo "Max wait time: ${MAX_WAIT_TIME}, Check interval: ${CHECK_INTERVAL}" + + # Convert comma-separated architectures to array + IFS=',' read -ra ARCHITECTURES <<< "$ARCHITECTURES_PARAM" + + # Convert MAX_WAIT_TIME to seconds + MAX_SECONDS=$(echo "$MAX_WAIT_TIME" | awk '{ + if ($0 ~ /[0-9]+s/) { print int($0) } + else if ($0 ~ /[0-9]+m/) { print int($0) * 60 } + else if ($0 ~ /[0-9]+h/) { print int($0) * 3600 } + else { print int($0) } + }') + + # Convert CHECK_INTERVAL to seconds + INTERVAL_SECONDS=$(echo "$CHECK_INTERVAL" | awk '{ + if ($0 ~ /[0-9]+s/) { print int($0) } + else if ($0 ~ /[0-9]+m/) { print int($0) * 60 } + else if ($0 ~ /[0-9]+h/) { print int($0) * 3600 } + else { print int($0) } + }') + + START_TIME=$(date +%s) + ELAPSED=0 + + while [ $ELAPSED -lt $MAX_SECONDS ]; do + # Check if all required architectures are available + ALL_ARCH_AVAILABLE=true + MISSING_ARCHS=() + + for ARCH in "${ARCHITECTURES[@]}"; do + if ! oc image info "$OPERATOR_IMAGE" --filter-by-os="$ARCH" &>/dev/null; then + ALL_ARCH_AVAILABLE=false + MISSING_ARCHS+=("$ARCH") + fi + done + + if [ "$ALL_ARCH_AVAILABLE" = true ]; then + echo "✓ Operator image is now available with all architectures: ${OPERATOR_IMAGE}" + echo "Available architectures:" + for ARCH in "${ARCHITECTURES[@]}"; do + echo " - ${ARCH}" + done + + # Get the manifest list digest (this is what we need for the bundle) + # The manifest list digest is shown when we query without --filter-by-os + MANIFEST_DIGEST=$(oc image info "$OPERATOR_IMAGE" 2>&1 | grep -i "manifest list:" | awk '{print $3}' || echo "") + if [ -z "$MANIFEST_DIGEST" ]; then + # Try alternative method - get digest from image info + MANIFEST_DIGEST=$(oc image info "$OPERATOR_IMAGE" --filter-by-os=linux/amd64 2>&1 | grep -i "manifest list:" | awk '{print $3}' || echo "") + fi + + if [ -z "$MANIFEST_DIGEST" ] || [[ ! "$MANIFEST_DIGEST" =~ ^sha256: ]]; then + echo "Error: Could not get manifest list digest from image: ${OPERATOR_IMAGE}" + exit 1 + fi + + # Extract the base image name (remove tag and any existing digest) + # OPERATOR_IMAGE format: registry/repo/image:tag + # We want: registry/repo/image@sha256:digest + BASE_IMAGE="${OPERATOR_IMAGE%%:*}" # Remove everything after first : + if [ "$BASE_IMAGE" = "$OPERATOR_IMAGE" ]; then + # No tag found, try removing @digest if present + BASE_IMAGE="${OPERATOR_IMAGE%%@*}" + fi + + OPERATOR_IMAGE_WITH_DIGEST="${BASE_IMAGE}@${MANIFEST_DIGEST}" + echo "Manifest list digest: ${MANIFEST_DIGEST}" + echo "Operator image with digest: ${OPERATOR_IMAGE_WITH_DIGEST}" + echo "${OPERATOR_IMAGE_WITH_DIGEST}" > $(results.OPERATOR_IMAGE_WITH_DIGEST.path) + + exit 0 + else + echo "Waiting for image architectures... (elapsed: ${ELAPSED}s / ${MAX_SECONDS}s)" + echo "Missing architectures: ${MISSING_ARCHS[*]}" + fi + sleep $INTERVAL_SECONDS + + CURRENT_TIME=$(date +%s) + ELAPSED=$((CURRENT_TIME - START_TIME)) + done + + echo "✗ Timeout: Operator image not available after ${MAX_WAIT_TIME}" + echo "Image: ${OPERATOR_IMAGE}" + echo "This may indicate the operator pipeline failed or is still running." + exit 1 + diff --git a/bundle-hack/update_csv.go b/bundle-hack/update_csv.go index 698a2ef24..d9f5c9f8d 100644 --- a/bundle-hack/update_csv.go +++ b/bundle-hack/update_csv.go @@ -1,13 +1,14 @@ package main import ( - "encoding/base64" - "fmt" - "gopkg.in/yaml.v3" - "log" - "os" - "path/filepath" - "strings" + "encoding/base64" + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "gopkg.in/yaml.v3" ) func readCSV(csvFilename string, csv *map[string]interface{}) { @@ -51,7 +52,7 @@ func getInputCSVFilePath(dir string) string { for _, filename := range filenames { if strings.HasSuffix(filename.Name(), "clusterserviceversion.yaml") { - return filepath.Join(dir,filename.Name()) + return filepath.Join(dir, filename.Name()) } } @@ -63,15 +64,15 @@ func getOutputCSVFilePath(dir string, version string) string { return filepath.Join(dir, fmt.Sprintf("file-integrity-operator.v%s.clusterserviceversion.yaml", version)) } -func addRequiredAnnotations(csv map[string]interface{}){ +func addRequiredAnnotations(csv map[string]interface{}) { requiredAnnotations := map[string]string{ - "features.operators.openshift.io/disconnected": "true", - "features.operators.openshift.io/fips-compliant": "true", - "features.operators.openshift.io/proxy-aware": "false", - "features.operators.openshift.io/tls-profiles": "false", - "features.operators.openshift.io/token-auth-aws": "false", + "features.operators.openshift.io/disconnected": "true", + "features.operators.openshift.io/fips-compliant": "true", + "features.operators.openshift.io/proxy-aware": "false", + "features.operators.openshift.io/tls-profiles": "false", + "features.operators.openshift.io/token-auth-aws": "false", "features.operators.openshift.io/token-auth-azure": "false", - "features.operators.openshift.io/token-auth-gcp": "false", + "features.operators.openshift.io/token-auth-gcp": "false", } annotations, ok := csv["metadata"].(map[string]interface{})["annotations"].(map[string]interface{}) @@ -114,7 +115,7 @@ func replaceIcon(csv map[string]interface{}) { spec := s.(map[string]interface{}) iconPath := "../bundle/icons/icon.png" - iconData,err := os.ReadFile(iconPath) + iconData, err := os.ReadFile(iconPath) if err != nil { log.Fatal(fmt.Sprintf("Error: Failed to read icon file '%s'", iconPath)) } @@ -136,7 +137,7 @@ func recoverFromReplaceImages() { } } -func replaceImages(csv map[string]interface{}) { +func replaceImages(csv map[string]interface{}, operatorImage string) { defer recoverFromReplaceImages() // Konflux will automatically update the image sha based on the most @@ -144,11 +145,18 @@ func replaceImages(csv map[string]interface{}) { // Hat registry so that the bundle image will work when it's available // there. konfluxPullSpec := "quay.io/redhat-user-workloads/ocp-isc-tenant/file-integrity-operator-dev@sha256:57d8cf654bfa556d9c488edad96e78f0db3e1c99d57790dcc0f195a7ec0569a8" + if operatorImage != "" { + konfluxPullSpec = operatorImage + } + delimiter := "@" parts := strings.Split(konfluxPullSpec, delimiter) if len(parts) > 2 { log.Fatalf("Error: Failed to safely determine image SHA from Konflux pull spec: %s", konfluxPullSpec) } + if len(parts) < 2 { + log.Fatalf("Error: Operator image must include digest: %s", konfluxPullSpec) + } imageSha := parts[1] registry := "registry.redhat.io/compliance/openshift-file-integrity-rhel8-operator" redHatPullSpec := registry + delimiter + imageSha @@ -184,9 +192,17 @@ func removeRelated(csv map[string]interface{}) { func main() { var csv map[string]interface{} + if len(os.Args) < 4 { + log.Fatal("Usage: update_csv.go [operator-image]") + } + manifestsDir := os.Args[1] oldVersion := os.Args[2] newVersion := os.Args[3] + operatorImage := "" + if len(os.Args) > 4 { + operatorImage = os.Args[4] + } csvFilename := getInputCSVFilePath(manifestsDir) fmt.Println(fmt.Sprintf("Found manifest in %s", csvFilename)) @@ -196,7 +212,7 @@ func main() { addRequiredAnnotations(csv) replaceVersion(oldVersion, newVersion, csv) replaceIcon(csv) - replaceImages(csv) + replaceImages(csv, operatorImage) removeRelated(csv) outputCSVFilename := getOutputCSVFilePath(manifestsDir, newVersion) diff --git a/bundle.openshift.Dockerfile b/bundle.openshift.Dockerfile index 09fafb481..bca4a1f88 100644 --- a/bundle.openshift.Dockerfile +++ b/bundle.openshift.Dockerfile @@ -1,5 +1,6 @@ ARG FIO_OLD_VERSION="1.3.5" ARG FIO_NEW_VERSION="1.3.6" +ARG OPERATOR_IMAGE="" FROM brew.registry.redhat.io/rh-osbs/openshift-golang-builder:v1.22 as builder @@ -10,8 +11,9 @@ WORKDIR bundle-hack # Bring the version variables into scope ARG FIO_OLD_VERSION ARG FIO_NEW_VERSION +ARG OPERATOR_IMAGE -RUN go run ./update_csv.go ../bundle/manifests ${FIO_OLD_VERSION} ${FIO_NEW_VERSION} +RUN go run ./update_csv.go ../bundle/manifests ${FIO_OLD_VERSION} ${FIO_NEW_VERSION} "${OPERATOR_IMAGE}" RUN ./update_bundle_annotations.sh FROM scratch