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