From 859f873692ffad19d6a95e12922db081e2c2d5cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbyn=C4=9Bk=20Dr=C3=A1pela?= Date: Fri, 26 Jun 2026 09:31:32 +0200 Subject: [PATCH] feat(ci): add disconnected OCP smoke test for Helm and Operator Add end-to-end disconnected CI pipeline handlers that deploy RHDH in an isolated OCP cluster and run a Playwright smoke test. Helm path uses oc-mirror v2 (downloaded at runtime) for chart + image mirroring, following the documented air-gapped workflow: - GA (registry.redhat.io): chart from charts.openshift.io, oc-mirror discovers and mirrors default images automatically - CI/upstream: chart from oci://quay.io/rhdh/chart via helm.local, override images added as additionalImages - oc-mirror generates IDMS, patched with cross-registry entries for both quay.io and registry.redhat.io hub image sources - Chart installed from local tgz in oc-mirror workspace Operator path uses prepare-restricted-environment.sh from rhdh-operator for operator+operand mirroring and installation (documented approach). Both paths share: - Auth setup (REGISTRY_AUTH_FILE + XDG_RUNTIME_DIR/containers/auth.json) - Plugin mirroring via mirror-plugins.sh with registries.conf covering registry.access.redhat.com/rhdh, quay.io/rhdh, and ghcr.io/redhat-developer/rhdh-plugin-export-overlays (6 CI plugins) - Helm overrides for registries.conf volume mount (avoids array clobber) - CATALOG_INDEX_IMAGE override support for CI build verification Dispatcher routing in openshift-ci-tests.sh for *ocp*disconnected*helm*nightly* and *ocp*disconnected*operator*nightly* patterns, positioned before generic *ocp*helm*nightly* to prevent false matches. Assisted-by: OpenCode --- .ci/pipelines/jobs/ocp-disconnected-helm.sh | 196 ++++++++++++ .../jobs/ocp-disconnected-operator.sh | 129 ++++++++ .ci/pipelines/lib/disconnected.sh | 287 ++++++++++++++++++ .ci/pipelines/openshift-ci-tests.sh | 14 + .../disconnected/helm-overrides.yaml | 52 ++++ .../disconnected/plugin-mirror-configmap.yaml | 17 ++ 6 files changed, 695 insertions(+) create mode 100644 .ci/pipelines/jobs/ocp-disconnected-helm.sh create mode 100644 .ci/pipelines/jobs/ocp-disconnected-operator.sh create mode 100644 .ci/pipelines/lib/disconnected.sh create mode 100644 .ci/pipelines/resources/disconnected/helm-overrides.yaml create mode 100644 .ci/pipelines/resources/disconnected/plugin-mirror-configmap.yaml diff --git a/.ci/pipelines/jobs/ocp-disconnected-helm.sh b/.ci/pipelines/jobs/ocp-disconnected-helm.sh new file mode 100644 index 0000000000..4925e410d1 --- /dev/null +++ b/.ci/pipelines/jobs/ocp-disconnected-helm.sh @@ -0,0 +1,196 @@ +#!/bin/bash + +# shellcheck source=.ci/pipelines/lib/log.sh +source "$DIR"/lib/log.sh +# shellcheck source=.ci/pipelines/lib/common.sh +source "$DIR"/lib/common.sh +# shellcheck source=.ci/pipelines/utils.sh +source "$DIR"/utils.sh +# shellcheck source=.ci/pipelines/lib/testing.sh +source "$DIR"/lib/testing.sh +# shellcheck source=.ci/pipelines/playwright-projects.sh +source "$DIR"/playwright-projects.sh +# shellcheck source=.ci/pipelines/lib/disconnected.sh +source "$DIR"/lib/disconnected.sh + +export INSTALL_METHOD="helm" + +handle_ocp_disconnected_helm() { + export NAME_SPACE="${NAME_SPACE:-showcase-ci-disconnected}" + + disconnected::require_env + disconnected::setup_auth + + common::oc_login + + K8S_CLUSTER_ROUTER_BASE=$(oc get route console -n openshift-console -o=jsonpath='{.spec.host}' | sed 's/^[^.]*\.//') + export K8S_CLUSTER_ROUTER_BASE + + # --- Section A: Install oc-mirror --- + log::section "oc-mirror Setup" + + disconnected::install_oc_mirror || { + log::error "Failed to install oc-mirror — aborting" + return 1 + } + + # --- Section B: Resolve chart source and pull locally --- + log::section "Chart Resolution" + + local is_ga="false" + if [[ "${IMAGE_REGISTRY}" == "registry.redhat.io" ]]; then + is_ga="true" + fi + + if [[ "${is_ga}" == "true" ]]; then + # GA: pull chart from charts.openshift.io + helm repo add openshift-helm-charts https://charts.openshift.io 2> /dev/null || true + helm repo update openshift-helm-charts + log::info "Pulling GA chart from charts.openshift.io (version: ${RELEASE_VERSION})" + helm pull openshift-helm-charts/redhat-developer-hub \ + --version "${RELEASE_VERSION}" \ + -d "${DISCONNECTED_TMPDIR}" || { + log::error "Failed to pull chart from charts.openshift.io" + return 1 + } + else + # CI/upstream: pull chart from OCI registry + log::info "Pulling CI chart from ${HELM_CHART_URL} (version: ${CHART_VERSION})" + helm pull "${HELM_CHART_URL}" --version "${CHART_VERSION}" \ + -d "${DISCONNECTED_TMPDIR}" || { + log::error "Failed to pull chart from ${HELM_CHART_URL}" + return 1 + } + fi + + CHART_LOCAL_TGZ=$(find "${DISCONNECTED_TMPDIR}" -maxdepth 1 -name '*.tgz' | head -1) + export CHART_LOCAL_TGZ + + if [[ -z "${CHART_LOCAL_TGZ}" ]]; then + log::error "No chart .tgz found in ${DISCONNECTED_TMPDIR}" + return 1 + fi + log::success "Chart pulled: ${CHART_LOCAL_TGZ}" + + # --- Section C: Resolve PostgreSQL image from chart --- + local helm_values + helm_values=$(helm show values "${CHART_LOCAL_TGZ}" 2> /dev/null || true) + + export PG_REGISTRY PG_REPO PG_TAG + PG_REGISTRY=$(echo "${helm_values}" | yq '.upstream.postgresql.image.registry' || true) + PG_REPO=$(echo "${helm_values}" | yq '.upstream.postgresql.image.repository' || true) + PG_TAG=$(echo "${helm_values}" | yq '.upstream.postgresql.image.tag' || true) + PG_REGISTRY="${PG_REGISTRY:-registry.redhat.io}" + PG_REPO="${PG_REPO:-rhel9/postgresql-15}" + PG_TAG="${PG_TAG:-latest}" + + log::info "PostgreSQL image from chart: ${PG_REGISTRY}/${PG_REPO}:${PG_TAG}" + + # --- Section D: Build ImageSetConfiguration --- + log::section "Image Mirroring" + + local imageset_config="${DISCONNECTED_TMPDIR}/imageset-config.yaml" + disconnected::build_imageset_config "${imageset_config}" || { + log::error "Failed to build ImageSetConfiguration" + return 1 + } + + # --- Section E: Run oc-mirror --- + local workspace="${DISCONNECTED_TMPDIR}/oc-mirror-workspace" + disconnected::run_oc_mirror "${imageset_config}" "${workspace}" || { + log::error "oc-mirror failed — aborting" + return 1 + } + + # --- Section F: Patch and apply IDMS --- + log::section "Cluster Resources" + + disconnected::patch_idms "${OC_MIRROR_IDMS_FILE}" + + oc apply -f "${OC_MIRROR_IDMS_FILE}" || { + log::error "Failed to apply IDMS — aborting" + return 1 + } + log::success "ImageDigestMirrorSet applied" + + if [[ -n "${OC_MIRROR_ITMS_FILE:-}" ]]; then + oc apply -f "${OC_MIRROR_ITMS_FILE}" || { + log::error "Failed to apply ITMS — aborting" + return 1 + } + log::success "ImageTagMirrorSet applied" + fi + + # --- Section G: Plugin mirroring --- + log::section "Plugin Mirroring" + + disconnected::fetch_script "mirror-plugins.sh" "${DISCONNECTED_TMPDIR}/mirror-plugins.sh" || { + log::error "Failed to fetch mirror-plugins.sh — aborting" + return 1 + } + + local plugin_index="oci://registry.access.redhat.com/rhdh/plugin-catalog-index:${RELEASE_VERSION}" + if [[ -n "${CATALOG_INDEX_IMAGE:-}" ]]; then + plugin_index="oci://${CATALOG_INDEX_IMAGE}" + fi + + bash "${DISCONNECTED_TMPDIR}/mirror-plugins.sh" \ + --plugin-index "${plugin_index}" \ + --to-registry "${MIRROR_REGISTRY_URL}" || { + log::error "mirror-plugins.sh failed — aborting" + return 1 + } + + # --- Section H: Namespace + registries.conf ConfigMap --- + namespace::configure "${NAME_SPACE}" + + envsubst < "${DIR}/resources/disconnected/plugin-mirror-configmap.yaml" \ + | oc apply -n "${NAME_SPACE}" -f - || { + log::error "Failed to create registries.conf ConfigMap — aborting" + return 1 + } + log::success "ConfigMap rhdh-plugin-mirror-conf created in ${NAME_SPACE}" + + # --- Section I: Helm deployment from mirrored chart --- + log::section "Helm Deployment" + + # Prefer the chart from oc-mirror workspace, fall back to the pulled tgz + local chart_install_path + chart_install_path="${OC_MIRROR_CHART_PATH:-${CHART_LOCAL_TGZ}}" + log::info "Installing chart from: ${chart_install_path}" + + local helm_set_flags=( + --set global.clusterRouterBase="${K8S_CLUSTER_ROUTER_BASE}" + --set upstream.backstage.image.registry="${MIRROR_REGISTRY_URL}" + --set upstream.backstage.image.repository="${IMAGE_REPO}" + --set upstream.backstage.image.tag="${TAG_NAME}" + --set upstream.postgresql.image.registry="${MIRROR_REGISTRY_URL}" + ) + + if [[ -n "${CATALOG_INDEX_IMAGE:-}" ]]; then + helm_set_flags+=( + --set global.catalogIndex.image.registry="${MIRROR_REGISTRY_URL}" + --set global.catalogIndex.image.repository="${CATALOG_INDEX_REPO}" + --set global.catalogIndex.image.tag="${CATALOG_INDEX_TAG}" + ) + fi + + helm upgrade -i "${RELEASE_NAME}" -n "${NAME_SPACE}" \ + "${chart_install_path}" \ + -f "${DIR}/value_files/${HELM_CHART_VALUE_FILE_NAME}" \ + -f "${DIR}/resources/disconnected/helm-overrides.yaml" \ + "${helm_set_flags[@]}" || { + log::error "Helm deployment failed" + return 1 + } + + log::success "RHDH deployed via Helm with mirrored images" + + # --- Section J: Smoke test --- + log::section "Smoke Test" + + local url="https://${RELEASE_NAME}-developer-hub-${NAME_SPACE}.${K8S_CLUSTER_ROUTER_BASE}" + testing::check_and_test "${RELEASE_NAME}" "${NAME_SPACE}" "${PW_PROJECT_SMOKE_TEST}" "${url}" + + log::success "Disconnected Helm smoke test completed" +} diff --git a/.ci/pipelines/jobs/ocp-disconnected-operator.sh b/.ci/pipelines/jobs/ocp-disconnected-operator.sh new file mode 100644 index 0000000000..0c6093a63b --- /dev/null +++ b/.ci/pipelines/jobs/ocp-disconnected-operator.sh @@ -0,0 +1,129 @@ +#!/bin/bash + +# shellcheck source=.ci/pipelines/lib/log.sh +source "$DIR"/lib/log.sh +# shellcheck source=.ci/pipelines/lib/common.sh +source "$DIR"/lib/common.sh +# shellcheck source=.ci/pipelines/utils.sh +source "$DIR"/utils.sh +# shellcheck source=.ci/pipelines/install-methods/operator.sh +source "$DIR"/install-methods/operator.sh +# shellcheck source=.ci/pipelines/lib/testing.sh +source "$DIR"/lib/testing.sh +# shellcheck source=.ci/pipelines/playwright-projects.sh +source "$DIR"/playwright-projects.sh +# shellcheck source=.ci/pipelines/lib/disconnected.sh +source "$DIR"/lib/disconnected.sh + +export INSTALL_METHOD="operator" + +handle_ocp_disconnected_operator() { + export NAME_SPACE="${NAME_SPACE:-showcase-disconnected}" + + disconnected::require_env + disconnected::setup_auth + + common::oc_login + + K8S_CLUSTER_ROUTER_BASE=$(oc get route console -n openshift-console -o=jsonpath='{.spec.host}' | sed 's/^[^.]*\.//') + export K8S_CLUSTER_ROUTER_BASE + + # --- Section A: Operator Mirroring + Installation --- + # Uses prepare-restricted-environment.sh from rhdh-operator, which handles + # mirroring operator/operand images and installing the operator CatalogSource. + log::section "Operator Mirroring and Installation" + + disconnected::fetch_script "prepare-restricted-environment.sh" "${DISCONNECTED_TMPDIR}/prepare-restricted-environment.sh" \ + || { + log::error "Failed to fetch prepare-restricted-environment.sh — aborting" + return 1 + } + + local prepare_args=( + --to-registry "${MIRROR_REGISTRY_URL}" + --filter-versions "${RELEASE_VERSION}" + ) + if [[ -n "${CATALOG_INDEX_IMAGE:-}" ]]; then + prepare_args=( + --to-registry "${MIRROR_REGISTRY_URL}" + --index-image "${CATALOG_INDEX_IMAGE}" + --ci-index true + --filter-versions "${RELEASE_VERSION}" + ) + fi + + bash "${DISCONNECTED_TMPDIR}/prepare-restricted-environment.sh" "${prepare_args[@]}" \ + || { + log::error "prepare-restricted-environment.sh failed — aborting" + return 1 + } + log::success "Operator installed via prepare-restricted-environment.sh" + + # --- Section B: Wait for Operator CRD --- + k8s_wait::crd "backstages.rhdh.redhat.com" 300 10 || { + log::error "Backstage CRD not available after operator installation" + return 1 + } + + # --- Section C: Plugin Mirroring --- + log::section "Plugin Mirroring" + + disconnected::fetch_script "mirror-plugins.sh" "${DISCONNECTED_TMPDIR}/mirror-plugins.sh" \ + || { + log::error "Failed to fetch mirror-plugins.sh — aborting" + return 1 + } + + local plugin_index="oci://registry.access.redhat.com/rhdh/plugin-catalog-index:${RELEASE_VERSION}" + if [[ -n "${CATALOG_INDEX_IMAGE:-}" ]]; then + plugin_index="oci://${CATALOG_INDEX_IMAGE}" + fi + + bash "${DISCONNECTED_TMPDIR}/mirror-plugins.sh" \ + --plugin-index "${plugin_index}" \ + --to-registry "${MIRROR_REGISTRY_URL}" || { + log::error "mirror-plugins.sh failed — aborting" + return 1 + } + + # --- Section D: Namespace + registries.conf ConfigMap --- + log::section "Cluster Resources" + + namespace::configure "${NAME_SPACE}" + + envsubst < "${DIR}/resources/disconnected/plugin-mirror-configmap.yaml" \ + | oc apply -n "${NAME_SPACE}" -f - || { + log::error "Failed to create registries.conf ConfigMap — aborting" + return 1 + } + log::success "ConfigMap rhdh-plugin-mirror-conf created in ${NAME_SPACE}" + + # --- Section E: Backstage CR Deployment --- + log::section "Backstage CR Deployment" + + local rendered_cr + rendered_cr=$(envsubst < "${DIR}/resources/rhdh-operator/rhdh-start.yaml") + rendered_cr=$(echo "$rendered_cr" | yq eval \ + '.spec.application.extraFiles.configMaps = [ + { + "name": "rhdh-plugin-mirror-conf", + "key": "rhdh-registries.conf", + "mountPath": "/etc/containers/registries.conf.d", + "containers": ["install-dynamic-plugins"] + } + ]' -) + + local cr_temp="${DISCONNECTED_TMPDIR}/backstage-cr-disconnected.yaml" + echo "$rendered_cr" > "${cr_temp}" + + deploy_rhdh_operator "${NAME_SPACE}" "${cr_temp}" + log::success "Backstage CR deployed in ${NAME_SPACE}" + + # --- Section F: Smoke Test --- + log::section "Smoke Test" + + local url="https://backstage-${RELEASE_NAME}-${NAME_SPACE}.${K8S_CLUSTER_ROUTER_BASE}" + testing::check_and_test "${RELEASE_NAME}" "${NAME_SPACE}" "${PW_PROJECT_SMOKE_TEST}" "${url}" + + log::success "Disconnected Operator smoke test completed" +} diff --git a/.ci/pipelines/lib/disconnected.sh b/.ci/pipelines/lib/disconnected.sh new file mode 100644 index 0000000000..619a54c85d --- /dev/null +++ b/.ci/pipelines/lib/disconnected.sh @@ -0,0 +1,287 @@ +#!/usr/bin/env bash + +# Shared utility functions for disconnected CI pipeline handlers. +# Provides environment validation, oc-mirror-based image mirroring, +# auth setup, and external script fetching. +# +# Dependencies: lib/log.sh, lib/common.sh +# Consumers: jobs/ocp-disconnected-helm.sh, jobs/ocp-disconnected-operator.sh + +# Prevent re-sourcing +if [[ -n "${DISCONNECTED_LIB_SOURCED:-}" ]]; then + return 0 +fi +readonly DISCONNECTED_LIB_SOURCED=1 + +# shellcheck source=.ci/pipelines/lib/log.sh +source "${DIR}/lib/log.sh" +# shellcheck source=.ci/pipelines/lib/common.sh +source "${DIR}/lib/common.sh" + +# Create a dedicated temp directory for disconnected CI artifacts. +DISCONNECTED_TMPDIR=$(mktemp -d) +export DISCONNECTED_TMPDIR + +# oc-mirror binary path, set by disconnected::install_oc_mirror. +OC_MIRROR_BIN="" +export OC_MIRROR_BIN + +# Validate that all required disconnected environment variables are set. +# These are exported by the step-registry commands.sh before calling +# openshift-ci-tests.sh. +disconnected::require_env() { + if [[ "${DISCONNECTED:-}" != "true" ]]; then + log::error "DISCONNECTED is not set to 'true'. This handler requires a disconnected environment." + log::error "Ensure the step-registry commands.sh has run before this handler." + return 1 + fi + + common::require_vars \ + MIRROR_REGISTRY_URL \ + MIRROR_REGISTRY_PULL_SECRET \ + MIRROR_REGISTRY_CA +} + +# Configure container-tools authentication for skopeo, oc-mirror, and +# mirror-plugins.sh. Places the combined pull secret (which contains +# credentials for both source registries and the mirror registry) in +# the standard locations expected by these tools. +disconnected::setup_auth() { + export HOME="${HOME:-/tmp/home}" + export XDG_RUNTIME_DIR="${HOME}/run" + mkdir -p "${XDG_RUNTIME_DIR}/containers" + + # oc-mirror and skopeo read auth from ${XDG_RUNTIME_DIR}/containers/auth.json + cp "${MIRROR_REGISTRY_PULL_SECRET}" "${XDG_RUNTIME_DIR}/containers/auth.json" + + # REGISTRY_AUTH_FILE is respected by skopeo as an explicit override + export REGISTRY_AUTH_FILE="${MIRROR_REGISTRY_PULL_SECRET}" + + # oc-mirror requires this to be unset + unset REGISTRY_AUTH_PREFERENCE + + log::info "Container auth configured from ${MIRROR_REGISTRY_PULL_SECRET}" +} + +# Download the oc-mirror binary at runtime from mirror.openshift.com. +# Uses CONTAINER_PLATFORM_VERSION to select the matching version. +disconnected::install_oc_mirror() { + local arch + arch=$(uname -m) + case ${arch} in + x86_64) arch="amd64" ;; + aarch64) arch="arm64" ;; + esac + + local ocp_version="${CONTAINER_PLATFORM_VERSION:-4.21}" + local oc_mirror_version="" + + # Try stable channel for the OCP minor version, fall back to latest + local stable_url="https://mirror.openshift.com/pub/openshift-v4/${arch}/clients/ocp/stable-${ocp_version}/" + if curl -sf --head --connect-timeout 10 "${stable_url}" > /dev/null 2>&1; then + oc_mirror_version="stable-${ocp_version}" + log::info "Using oc-mirror from stable-${ocp_version} channel" + else + oc_mirror_version="latest" + log::info "stable-${ocp_version} not available, using oc-mirror from latest channel" + fi + + local download_dir="${DISCONNECTED_TMPDIR}/oc-mirror-download" + mkdir -p "${download_dir}" + + local base_url="https://mirror.openshift.com/pub/openshift-v4/${arch}/clients/ocp/${oc_mirror_version}" + + log::info "Downloading oc-mirror from ${base_url}/" + if ! curl -fL --retry 5 --connect-timeout 30 -o "${download_dir}/oc-mirror.tar.gz" "${base_url}/oc-mirror.tar.gz"; then + log::error "Failed to download oc-mirror" + return 1 + fi + + # Verify checksum + if curl -fL --retry 3 --connect-timeout 30 -o "${download_dir}/sha256sum.txt" "${base_url}/sha256sum.txt" 2> /dev/null; then + if grep "oc-mirror.tar.gz" "${download_dir}/sha256sum.txt" | (cd "${download_dir}" && sha256sum -c -); then + log::info "oc-mirror checksum verified" + else + log::warn "oc-mirror checksum verification failed — continuing anyway" + fi + fi + + tar -xzf "${download_dir}/oc-mirror.tar.gz" -C "${download_dir}" + chmod +x "${download_dir}/oc-mirror" + OC_MIRROR_BIN="${download_dir}/oc-mirror" + export OC_MIRROR_BIN + + log::success "oc-mirror installed: $(${OC_MIRROR_BIN} version --output=yaml 2>&1 | head -1)" +} + +# Build an ImageSetConfiguration for oc-mirror. +# The configuration is dynamically generated based on IMAGE_REGISTRY: +# - registry.redhat.io (GA): uses helm.local with chart pulled from charts.openshift.io +# - anything else (CI/upstream): uses helm.local with chart pulled from OCI +# Args: +# $1 - output_path: Path to write the ImageSetConfiguration YAML +disconnected::build_imageset_config() { + local output_path=$1 + + # Start with the base config + cat > "${output_path}" << EOF +kind: ImageSetConfiguration +apiVersion: mirror.openshift.io/v2alpha1 +mirror: + helm: + local: + - name: redhat-developer-hub + path: ${CHART_LOCAL_TGZ} +EOF + + # Add additional images that need mirroring beyond what the chart references. + # The chart's default images are discovered automatically by oc-mirror. + local additional_images=() + + # When the hub image is overridden (different from chart defaults), add it + # so oc-mirror mirrors the actual image we'll deploy with. + if [[ "${IMAGE_REGISTRY}" != "registry.redhat.io" ]]; then + # CI/upstream: chart defaults to quay.io/rhdh/rhdh-hub-rhel9@sha256:..., + # but we may deploy with a different tag (e.g., rhdh-community/rhdh:next) + additional_images+=("${IMAGE_REGISTRY}/${IMAGE_REPO}:${TAG_NAME}") + fi + + # PG image: CI charts may use quay.io/fedora/postgresql-15 instead of + # registry.redhat.io/rhel9/postgresql-15. Add it if not from registry.redhat.io. + if [[ "${PG_REGISTRY:-registry.redhat.io}" != "registry.redhat.io" ]]; then + additional_images+=("${PG_REGISTRY}/${PG_REPO}:${PG_TAG}") + fi + + if [[ ${#additional_images[@]} -gt 0 ]]; then + { + echo " additionalImages:" + for img in "${additional_images[@]}"; do + echo " - name: ${img}" + done + } >> "${output_path}" + fi + + log::info "ImageSetConfiguration written to ${output_path}" + log::debug "$(cat "${output_path}")" +} + +# Run oc-mirror to mirror images to the disconnected mirror registry. +# Sets OC_MIRROR_IDMS_FILE, OC_MIRROR_ITMS_FILE, and OC_MIRROR_CHART_PATH. +# Args: +# $1 - imageset_config: Path to the ImageSetConfiguration YAML +# $2 - workspace_dir: Path to the oc-mirror workspace directory +disconnected::run_oc_mirror() { + local imageset_config=$1 + local workspace_dir=$2 + + mkdir -p "${workspace_dir}" + + log::info "Running oc-mirror --v2 → ${MIRROR_REGISTRY_URL}" + if ! "${OC_MIRROR_BIN}" \ + -c "${imageset_config}" \ + "docker://${MIRROR_REGISTRY_URL}" \ + --dest-tls-verify=false \ + --v2 \ + --workspace "file://${workspace_dir}"; then + log::error "oc-mirror failed" + return 1 + fi + + local result_dir="${workspace_dir}/working-dir" + + # IDMS (required) + OC_MIRROR_IDMS_FILE="${result_dir}/cluster-resources/idms-oc-mirror.yaml" + if [[ ! -s "${OC_MIRROR_IDMS_FILE}" ]]; then + log::error "oc-mirror did not generate IDMS at ${OC_MIRROR_IDMS_FILE}" + return 1 + fi + export OC_MIRROR_IDMS_FILE + + # ITMS (optional) + OC_MIRROR_ITMS_FILE="${result_dir}/cluster-resources/itms-oc-mirror.yaml" + if [[ ! -s "${OC_MIRROR_ITMS_FILE}" ]]; then + OC_MIRROR_ITMS_FILE="" + fi + export OC_MIRROR_ITMS_FILE + + # Chart path (in the workspace) + OC_MIRROR_CHART_PATH=$(find "${result_dir}/helm/charts" -name '*.tgz' 2> /dev/null | head -1) + export OC_MIRROR_CHART_PATH + + log::success "oc-mirror completed successfully" + log::info "IDMS: ${OC_MIRROR_IDMS_FILE}" + [[ -n "${OC_MIRROR_ITMS_FILE}" ]] && log::info "ITMS: ${OC_MIRROR_ITMS_FILE}" + [[ -n "${OC_MIRROR_CHART_PATH}" ]] && log::info "Chart: ${OC_MIRROR_CHART_PATH}" +} + +# Patch the oc-mirror-generated IDMS to ensure both quay.io and +# registry.redhat.io sources are covered, regardless of what oc-mirror +# discovered from the chart. This is needed because: +# - GA charts reference registry.redhat.io but CI verification may override to quay.io +# - CI charts reference quay.io but post-GA verification uses registry.redhat.io +# Args: +# $1 - idms_file: Path to the IDMS YAML to patch +disconnected::patch_idms() { + local idms_file=$1 + + log::info "Patching IDMS with cross-registry mirror entries" + + # Add mirror entries for the hub image from both registries + for source_registry in "quay.io" "registry.redhat.io"; do + local source="${source_registry}/${IMAGE_REPO}" + local mirror="${MIRROR_REGISTRY_URL}/${IMAGE_REPO}" + + # Skip if this source is already in the IDMS + if yq eval ".spec.imageDigestMirrors[].source" "${idms_file}" 2> /dev/null | grep -qF "${source}"; then + log::debug "IDMS already contains entry for ${source}" + continue + fi + + yq eval -i \ + ".spec.imageDigestMirrors += [{\"mirrors\": [\"${mirror}\"], \"source\": \"${source}\"}]" \ + "${idms_file}" + log::info "Added IDMS entry: ${source} → ${mirror}" + done + + # Add mirror entry for PG image if not already present + if [[ -n "${PG_REGISTRY:-}" && -n "${PG_REPO:-}" ]]; then + local pg_source="${PG_REGISTRY}/${PG_REPO}" + local pg_mirror="${MIRROR_REGISTRY_URL}/${PG_REPO}" + + if ! yq eval ".spec.imageDigestMirrors[].source" "${idms_file}" 2> /dev/null | grep -qF "${pg_source}"; then + yq eval -i \ + ".spec.imageDigestMirrors += [{\"mirrors\": [\"${pg_mirror}\"], \"source\": \"${pg_source}\"}]" \ + "${idms_file}" + log::info "Added IDMS entry: ${pg_source} → ${pg_mirror}" + fi + fi + + log::debug "Patched IDMS:" + log::debug "$(cat "${idms_file}")" +} + +# Fetch an external script from the rhdh-operator repository. +# Args: +# $1 - script_name: Name of the script (e.g., "mirror-plugins.sh") +# $2 - output_path: Local path to save the script +# $3 - branch: (optional) Branch name (defaults to $RELEASE_BRANCH_NAME) +disconnected::fetch_script() { + local script_name=$1 + local output_path=$2 + local branch="${3:-${RELEASE_BRANCH_NAME}}" + + local url="https://raw.githubusercontent.com/redhat-developer/rhdh-operator/refs/heads/${branch}/.rhdh/scripts/${script_name}" + + log::info "Fetching ${script_name} from rhdh-operator (branch: ${branch})..." + if ! curl -fL --max-time 30 -o "${output_path}" "${url}"; then + log::error "Failed to download ${script_name} from ${url}" + return 1 + fi + chmod +x "${output_path}" + log::success "Downloaded ${script_name} to ${output_path}" +} + +# Export functions for subshell usage (e.g., timeout bash -c "...") +export -f disconnected::require_env +export -f disconnected::setup_auth +export -f disconnected::fetch_script diff --git a/.ci/pipelines/openshift-ci-tests.sh b/.ci/pipelines/openshift-ci-tests.sh index 55b3679cbc..f6564e0b5c 100755 --- a/.ci/pipelines/openshift-ci-tests.sh +++ b/.ci/pipelines/openshift-ci-tests.sh @@ -128,6 +128,20 @@ main() { log::info "Calling handle_ocp_localization" handle_ocp_localization ;; + *ocp*disconnected*helm*nightly*) + log::info "Sourcing ocp-disconnected-helm.sh" + # shellcheck source=.ci/pipelines/jobs/ocp-disconnected-helm.sh + source "${DIR}/jobs/ocp-disconnected-helm.sh" + log::info "Calling handle_ocp_disconnected_helm" + handle_ocp_disconnected_helm + ;; + *ocp*disconnected*operator*nightly*) + log::info "Sourcing ocp-disconnected-operator.sh" + # shellcheck source=.ci/pipelines/jobs/ocp-disconnected-operator.sh + source "${DIR}/jobs/ocp-disconnected-operator.sh" + log::info "Calling handle_ocp_disconnected_operator" + handle_ocp_disconnected_operator + ;; *ocp*helm*nightly*) log::info "Sourcing ocp-nightly.sh" # shellcheck source=.ci/pipelines/jobs/ocp-nightly.sh diff --git a/.ci/pipelines/resources/disconnected/helm-overrides.yaml b/.ci/pipelines/resources/disconnected/helm-overrides.yaml new file mode 100644 index 0000000000..83b1fd197c --- /dev/null +++ b/.ci/pipelines/resources/disconnected/helm-overrides.yaml @@ -0,0 +1,52 @@ +# Helm values override for disconnected deployments. +# Adds the registries.conf ConfigMap volume + mount to the +# install-dynamic-plugins init container. +# +# Using a separate -f file avoids the Helm "array clobber" pitfall: +# --set on initContainers[]/extraVolumes[] replaces the entire array, +# losing the chart defaults. The -f merge keeps chart-default volumes +# intact and only adds the ConfigMap volume + mount. +upstream: + backstage: + extraVolumes: + - name: dynamic-plugins-root + emptyDir: {} + - name: dynamic-plugins + configMap: + defaultMode: 420 + name: dynamic-plugins + optional: true + - name: dynamic-plugins-npmrc + secret: + defaultMode: 420 + optional: true + secretName: dynamic-plugins-npmrc + - name: rhdh-plugin-mirror-conf + configMap: + name: rhdh-plugin-mirror-conf + initContainers: + - name: install-dynamic-plugins + command: + - ./install-dynamic-plugins.sh + - /dynamic-plugins-root + env: + - name: NPM_CONFIG_USERCONFIG + value: /opt/app-root/src/.npmrc.dynamic-plugins + image: null + imagePullPolicy: Always + volumeMounts: + - mountPath: /dynamic-plugins-root + name: dynamic-plugins-root + - mountPath: /opt/app-root/src/dynamic-plugins.yaml + name: dynamic-plugins + readOnly: true + subPath: dynamic-plugins.yaml + - mountPath: /opt/app-root/src/.npmrc.dynamic-plugins + name: dynamic-plugins-npmrc + readOnly: true + subPath: .npmrc + - mountPath: /etc/containers/registries.conf.d/rhdh-registries.conf + name: rhdh-plugin-mirror-conf + readOnly: true + subPath: rhdh-registries.conf + workingDir: /opt/app-root/src diff --git a/.ci/pipelines/resources/disconnected/plugin-mirror-configmap.yaml b/.ci/pipelines/resources/disconnected/plugin-mirror-configmap.yaml new file mode 100644 index 0000000000..2e14f555c9 --- /dev/null +++ b/.ci/pipelines/resources/disconnected/plugin-mirror-configmap.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: rhdh-plugin-mirror-conf +data: + rhdh-registries.conf: | + [[registry]] + prefix = "registry.access.redhat.com/rhdh" + location = "${MIRROR_REGISTRY_URL}/rhdh" + + [[registry]] + prefix = "quay.io/rhdh" + location = "${MIRROR_REGISTRY_URL}/rhdh" + + [[registry]] + prefix = "ghcr.io/redhat-developer/rhdh-plugin-export-overlays" + location = "${MIRROR_REGISTRY_URL}/rhdh-plugin-export-overlays"