From 02ddaf16c3063134c155ff4a1f23f9097b68c83d Mon Sep 17 00:00:00 2001 From: Dmitrii Andreev Date: Wed, 9 Sep 2026 21:17:51 -0500 Subject: [PATCH] HYPERFLEET-1412 - feat: Support disconnected installs --- .tekton/hyperfleet-operator-bundle-push.yaml | 2 +- .tekton/hyperfleet-operator-push.yaml | 3 +- Makefile | 37 ++- README.md | 9 +- bundle-hack/update_bundle.sh | 24 -- bundle.konflux.Dockerfile | 12 +- cmd/main.go | 3 +- docs/disconnected-install.md | 203 +++++++++++++ docs/examples/imageset-config-catalog.yaml | 11 + go.mod | 2 + go.sum | 4 + hack/bundle/update_bundle.sh | 53 ++++ hack/oc-mirror.Dockerfile | 35 +++ hack/test-disconnected-mirror.sh | 284 ++++++++++++++++++ hack/test-verify-bundle-related-images.sh | 110 +++++++ hack/verify-bundle-related-images.sh | 25 ++ .../verify-related-images/integration_test.go | 154 ++++++++++ hack/verify-related-images/main.go | 210 +++++++++++++ hack/verify-related-images/main_test.go | 95 ++++++ internal/component/api/api.go | 31 +- internal/component/api/api_test.go | 4 + internal/component/api/render.go | 5 +- tools/go.mod | 14 +- tools/go.sum | 24 ++ 24 files changed, 1290 insertions(+), 64 deletions(-) delete mode 100755 bundle-hack/update_bundle.sh create mode 100644 docs/disconnected-install.md create mode 100644 docs/examples/imageset-config-catalog.yaml create mode 100755 hack/bundle/update_bundle.sh create mode 100644 hack/oc-mirror.Dockerfile create mode 100755 hack/test-disconnected-mirror.sh create mode 100644 hack/test-verify-bundle-related-images.sh create mode 100644 hack/verify-bundle-related-images.sh create mode 100644 hack/verify-related-images/integration_test.go create mode 100644 hack/verify-related-images/main.go create mode 100644 hack/verify-related-images/main_test.go diff --git a/.tekton/hyperfleet-operator-bundle-push.yaml b/.tekton/hyperfleet-operator-bundle-push.yaml index ba6ed1a..38a2750 100644 --- a/.tekton/hyperfleet-operator-bundle-push.yaml +++ b/.tekton/hyperfleet-operator-bundle-push.yaml @@ -9,7 +9,7 @@ metadata: pipelinesascode.tekton.dev/max-keep-runs: "3" pipelinesascode.tekton.dev/on-cel-expression: event == "push" && target_branch == "main" && ( ".tekton/hyperfleet-operator-bundle-push.yaml".pathChanged() || "bundle.konflux.Dockerfile".pathChanged() - || "bundle-hack/***".pathChanged() || "bundle/***".pathChanged() ) + || "hack/bundle/***".pathChanged() || "bundle/***".pathChanged() ) labels: appstudio.openshift.io/application: hyperfleet appstudio.openshift.io/component: hyperfleet-operator-bundle diff --git a/.tekton/hyperfleet-operator-push.yaml b/.tekton/hyperfleet-operator-push.yaml index 6e90ce6..08dbe89 100644 --- a/.tekton/hyperfleet-operator-push.yaml +++ b/.tekton/hyperfleet-operator-push.yaml @@ -14,8 +14,9 @@ metadata: && !(files.all.all(x, x.matches('^bundle\\.konflux\\.Dockerfile$') || x.matches('^bundle\\.Dockerfile$') - || x.matches('^bundle-hack/') + || x.matches('^hack/bundle/') || x.matches('^bundle/') + || x.matches('^hack/test-disconnected-mirror\\.sh$') || x.matches('^\\.tekton/hyperfleet-operator-bundle-push\\.yaml$') )) labels: diff --git a/Makefile b/Makefile index 87fc497..1439240 100644 --- a/Makefile +++ b/Makefile @@ -120,10 +120,23 @@ cleanup-test-e2e: ## Tear down the Kind cluster used for e2e tests ##@ Lint +.PHONY: verify-related-images +verify-related-images: ## Verify a final built bundle CSV (CSV_FILE is required). + @test -n "$(CSV_FILE)" || { echo "Set CSV_FILE to the CSV extracted from the built bundle"; exit 1; } + go run ./hack/verify-related-images -csv "$(CSV_FILE)" + .PHONY: lint -lint: ## Run golangci-lint linter +lint: verify-bundle-related-images ## Check bundle image metadata and run golangci-lint. $(GOLANGCI_LINT) run +.PHONY: verify-bundle-related-images +verify-bundle-related-images: ## Transform the repository bundle CSV and verify its related images. + @set -euo pipefail; \ + yq_path=$$($(call gotool,-n yq)); \ + YQ="$$yq_path" bash ./hack/verify-bundle-related-images.sh; \ + PATH="$$(dirname "$$yq_path"):$$PATH" go test -tags integration ./hack/verify-related-images; \ + YQ="$$yq_path" bash ./hack/test-verify-bundle-related-images.sh + .PHONY: lint-fix lint-fix: ## Run golangci-lint linter and perform fixes $(GOLANGCI_LINT) run --fix @@ -134,6 +147,23 @@ lint-config: ## Verify golangci-lint linter configuration ##@ Build +OC_MIRROR_IMAGE ?= hyperfleet-oc-mirror:local +# Disposable destination registry used only by test-disconnected-mirror to +# simulate the disconnected mirror; it is not the source or production registry. +REGISTRY_IMAGE ?= docker.io/library/registry@sha256:a3d8aaa63ed8681a604f1dea0aa03f100d5895b6a58ace528858a7b332415373 + +.PHONY: build-oc-mirror-image +build-oc-mirror-image: check-container-tool ## Build the containerized oc-mirror runner. + "$(CONTAINER_TOOL)" build --platform "$(PLATFORM)" -f hack/oc-mirror.Dockerfile -t "$(OC_MIRROR_IMAGE)" . + +.PHONY: test-disconnected-mirror +test-disconnected-mirror: build-oc-mirror-image ## Test catalog archive import using a catalog archive (no cluster install). + "$(CONTAINER_TOOL)" pull --platform "$(PLATFORM)" "$(REGISTRY_IMAGE)" + CONTAINER_TOOL="$(CONTAINER_TOOL)" OC_MIRROR_IMAGE="$(OC_MIRROR_IMAGE)" \ + CATALOG_IMG="$(CATALOG_IMG)" \ + REGISTRY_IMAGE="$(REGISTRY_IMAGE)" REGISTRY_AUTH_FILE="$(REGISTRY_AUTH_FILE)" CONTAINER_DNS="$(CONTAINER_DNS)" MIRROR_PLATFORM="$(PLATFORM)" \ + ./hack/test-disconnected-mirror.sh + .PHONY: build build: manifests generate fmt vet ## Build manager binary. go build -o bin/manager cmd/main.go @@ -169,7 +199,7 @@ GIT_DIRTY ?= $(shell [ -z "$$(git status --porcelain 2>/dev/null)" ] || echo "-m # Go build flags (FIPS compliant) CGO_ENABLED ?= 1 -GOEXPERIMENT ?= boringcrypto +GOEXPERIMENT ?= boringcrypto GOFLAGS ?= -trimpath # LDFLAGS := -s -w \ # -X github.com/openshift-hyperfleet/hyperfleet-operator/pkg/version.Version=$(APP_VERSION) \ @@ -283,7 +313,7 @@ undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/confi # Generates dist/install.yaml # Install resources # kubectl apply -f dist/install.yaml -# Uninstall resources +# Uninstall resources # kubectl delete -f dist/install.yaml # For image overrides edit config/manager/kustomization.yaml .PHONY: build-deployer @@ -384,7 +414,6 @@ catalog-build: opm ## Build a catalog image. catalog-push: ## Push a catalog image. $(MAKE) docker-push IMG=$(CATALOG_IMG) - ##@ Dependencies ## Location to install dependencies to diff --git a/README.md b/README.md index da5a05e..cfa5eb8 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,14 @@ A Kubernetes operator for HyperFleet cluster lifecycle management. hyperfleet-operator packages and delivers HyperFleet as a standard Kubernetes operator, installed and managed through OLM. It exposes a single cluster-scoped custom resource, `HyperFleetConfig`, as the entire partner-facing surface: install, configure, and observe HyperFleet through that one CR and its status conditions, with everything else the operator manages kept internal. +## Installation guides + +- [Developer workflow: operator and bundle images](docs/bundle.md) +- [Disconnected OpenShift installation with oc-mirror v2](docs/disconnected-install.md) + +The disconnected workflow mirrors the published catalog. The catalog selects +the OLM bundle and its related images. + ## Getting Started ### Prerequisites @@ -83,4 +91,3 @@ 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. - diff --git a/bundle-hack/update_bundle.sh b/bundle-hack/update_bundle.sh deleted file mode 100755 index 5f0ceed..0000000 --- a/bundle-hack/update_bundle.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -CSV_FILE="${CSV_FILE:-/manifests/hyperfleet-operator.clusterserviceversion.yaml}" - -# Update image references in the CSV file using yq -yq eval ' - # Update operator deployment image - (.spec.install.spec.deployments[].spec.template.spec.containers[] | select(.name == "manager") | .image) = strenv(HYPERFLEET_OPERATOR_IMAGE_PULLSPEC) | - - # Update RELATED_IMAGE_HYPERFLEET_API env var - (.spec.install.spec.deployments[].spec.template.spec.containers[] | select(.name == "manager") | .env[] | select(.name == "RELATED_IMAGE_HYPERFLEET_API") | .value) = strenv(HYPERFLEET_API_IMAGE_PULLSPEC) | - - # Update containerImage annotation - .metadata.annotations.containerImage = strenv(HYPERFLEET_OPERATOR_IMAGE_PULLSPEC) | - - # Update relatedImages - .spec.relatedImages = [ - {"name": "hyperfleet-operator", "image": strenv(HYPERFLEET_OPERATOR_IMAGE_PULLSPEC)}, - {"name": "hyperfleet-api", "image": strenv(HYPERFLEET_API_IMAGE_PULLSPEC)} - ] -' -i "${CSV_FILE}" - -cat "${CSV_FILE}" diff --git a/bundle.konflux.Dockerfile b/bundle.konflux.Dockerfile index f966404..6d5c1c9 100644 --- a/bundle.konflux.Dockerfile +++ b/bundle.konflux.Dockerfile @@ -1,10 +1,12 @@ -# Konflux bundle image build. Unlike the auto-generated bundle.Dockerfile (used -# for local dev with operator-sdk), this runs bundle-hack/update_bundle.sh to +# Konflux bundle image build. Unlike the auto-generated bundle.Dockerfile used +# for local development, this runs hack/bundle/update_bundle.sh to # patch digest-pinned image references into the CSV at build time. FROM registry.access.redhat.com/ubi9/ubi-minimal:latest AS builder-runner RUN microdnf install -y tar gzip && \ - curl -sL https://github.com/mikefarah/yq/releases/download/v4.44.1/yq_linux_amd64.tar.gz | tar xz && \ - mv yq_linux_amd64 /usr/bin/yq + curl -fsSLo /tmp/yq.tar.gz https://github.com/mikefarah/yq/releases/download/v4.44.1/yq_linux_amd64.tar.gz && \ + tar -xzf /tmp/yq.tar.gz && \ + mv yq_linux_amd64 /usr/bin/yq && \ + rm /tmp/yq.tar.gz FROM builder-runner AS builder # Hack to set the operator container image in the deployment @@ -15,7 +17,7 @@ ENV HYPERFLEET_OPERATOR_IMAGE_PULLSPEC=${HYPERFLEET_OPERATOR_IMAGE_PULLSPEC} ARG HYPERFLEET_API_IMAGE_PULLSPEC="quay.io/redhat-services-prod/hyperfleet-tenant/hyperfleet/hyperfleet-api@sha256:99f8cdda580069de21ba0e13b5b171cf82b81b93dc88b12bcaa8294e72e84fc3" ENV HYPERFLEET_API_IMAGE_PULLSPEC=${HYPERFLEET_API_IMAGE_PULLSPEC} -COPY bundle-hack . +COPY hack/bundle . COPY bundle/manifests /manifests/ RUN ./update_bundle.sh diff --git a/cmd/main.go b/cmd/main.go index e7bc2e0..d12dff1 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -42,6 +42,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" hyperfleetv1alpha1 "github.com/openshift-hyperfleet/hyperfleet-operator/api/v1alpha1" + "github.com/openshift-hyperfleet/hyperfleet-operator/internal/component/api" "github.com/openshift-hyperfleet/hyperfleet-operator/internal/controller" // +kubebuilder:scaffold:imports ) @@ -235,7 +236,7 @@ func main() { // it at bundle-build time via the relatedImages convention. Warn — but do not // fail — when it is unset or uses a mutable tag, so `make run` and tag-based // deploys keep working while the risk is surfaced in the logs. - apiImage := os.Getenv("RELATED_IMAGE_HYPERFLEET_API") + apiImage := os.Getenv(api.RelatedImageEnv) switch { case apiImage == "": setupLog.Info("RELATED_IMAGE_HYPERFLEET_API not set; falling back to the " + diff --git a/docs/disconnected-install.md b/docs/disconnected-install.md new file mode 100644 index 0000000..52fb7a9 --- /dev/null +++ b/docs/disconnected-install.md @@ -0,0 +1,203 @@ +# Disconnected installation from a published catalog + +This workflow starts with a **published HyperFleet operator catalog**. The +catalog selects the bundles; their published image metadata determines the +operator and operand images to mirror. The CSV in this repository is a +development/build template, not the published bundle manifest. + +Catalog publication is a prerequisite owned by the catalog publishing workflow. Obtain +the catalog digest, package, channel, and supported OpenShift/oc-mirror versions +from the catalog publisher. The example below uses package +`hyperfleet-operator` and channel `stable`; confirm these against the published +catalog. A bundle image alone cannot substitute for the catalog. + +## 1. Prepare on the connected host + +Requirements: + +- The publisher-supported oc-mirror v2 binary and source registry credentials. +- A published catalog containing the intended release bundle and its related + images, all accessible to the mirroring account. +- A destination registry reachable by the disconnected cluster and import host. +- An existing OpenShift cluster with OLM and its platform images provisioned for + disconnected operation. Platform mirroring is a separate prerequisite. +- Registry authentication and CA trust configured for the import host and + cluster. Configuring the host alone does not configure cluster image pulls. + +Use the same oc-mirror version for export and import. OpenShift 4.17 documents +oc-mirror v2 as Technology Preview; the local +test helper uses 4.18.18. Confirm the supported tool/cluster combination for +your release before customer installation. Use a fresh +workspace to avoid an incremental archive that depends on an earlier transfer: + +```bash +export MIRROR_ROOT="$(mktemp -d)" +cp docs/examples/imageset-config-catalog.yaml "$MIRROR_ROOT/imageset-config.yaml" +export REGISTRY_AUTH_FILE='/path/to/source-auth.json' +``` + +`oc-mirror` requires a containers-style auth file containing inline `auth` +entries. Create the file on the connected host with `skopeo login --authfile +"$REGISTRY_AUTH_FILE" quay.io`, or export an equivalent pull-secret; do not +transfer or commit it. + +Replace the catalog placeholder with the published digest and confirm the +package/channel. The example selects the channel head in that immutable catalog +snapshot. Select a publisher-supported version range if you need older bundles +or an upgrade path. + +Use `mirror.operators`. Do not enumerate the bundle, operator and API under +`additionalImages`: that bypasses catalog bundle and related-image discovery. +The catalog digest is the only release input to `oc-mirror`; it selects the OLM +bundle and its related images. The repository CSV is not an installation input. +List any dependent operator packages explicitly; `oc-mirror` does not infer +inter-operator dependencies. Do not use `skipDependencies` or blocked-image +filters to suppress required release content. + +## 2. Export and transfer + +```bash +oc-mirror --v2 --authfile "$REGISTRY_AUTH_FILE" \ + --config "$MIRROR_ROOT/imageset-config.yaml" \ + file://"$MIRROR_ROOT/archive" +tar -C "$MIRROR_ROOT" -czf /media/transfer/hyperfleet-mirror.tgz \ + archive imageset-config.yaml +sha256sum /media/transfer/hyperfleet-mirror.tgz +``` + +Record the printed archive digest in the publisher's authenticated release +system. A checksum copied only alongside the archive does not authenticate it. +If the publisher provides a signed checksum or manifest instead, verify that +signature with the publisher's release key and use its archive digest. Transfer +the complete archive and configuration; source credentials are not part of the +transfer artifact. Keep the archive digest, catalog digest, configuration, tool +version and export logs as release evidence. + +## 3. Import on the disconnected host + +Configure destination authentication and CA trust on this host. Use a fresh +directory and a destination reachable from every cluster node: + +```bash +export IMPORT_ROOT="$(mktemp -d)" +export EXPECTED_ARCHIVE_SHA256='' +printf '%s %s\n' "$EXPECTED_ARCHIVE_SHA256" \ + /media/transfer/hyperfleet-mirror.tgz | sha256sum --check - && \ + tar -C "$IMPORT_ROOT" -xzf /media/transfer/hyperfleet-mirror.tgz +export DESTINATION='mirror.example.com:8443' +export REGISTRY_AUTH_FILE='/path/to/destination-auth.json' +oc-mirror --v2 --authfile "$REGISTRY_AUTH_FILE" \ + --config "$IMPORT_ROOT/imageset-config.yaml" \ + --from file://"$IMPORT_ROOT/archive" docker://"$DESTINATION" +``` + +Do not extract the archive or use its `imageset-config.yaml` unless this +publisher-provided verification succeeds. Retain the verified archive digest +with the disconnected installation evidence. + +Some registries limit repository nesting. Use `--max-nested-paths` if required +and supported by your selected oc-mirror version. Grant pull access to +the service accounts used by CatalogSource, OLM bundle unpack, manager and +operands, including cross-project access when images live in another project. +The import user's successful push does not grant those workloads pull access. + +Apply the generated image-mirror resources and CatalogSource. The generated +file names may vary, so apply every matching IDMS, ITMS, and `cs-*.yaml` file: + +```bash +export CLUSTER_RESOURCES="$IMPORT_ROOT/archive/working-dir/cluster-resources" +for manifest in "$CLUSTER_RESOURCES"/idms-*.yaml \ + "$CLUSTER_RESOURCES"/itms-*.yaml \ + "$CLUSTER_RESOURCES"/cs-*.yaml; do + [ -e "$manifest" ] || continue + oc apply -f "$manifest" +done +oc get catalogsource -n openshift-marketplace +``` + +Select the OLM v0 CatalogSource resources for this operator, not an OLM v1 +ClusterCatalog. + +Use the **generated** CatalogSource image reference: filtering can rebuild the +catalog, so its destination digest need not equal the source catalog digest. +Wait for registry configuration rollout and the CatalogSource connection state +to become `READY` before subscribing. Do not proceed on incomplete import or +missing image errors. + +## 4. Install through OLM + +Public registry access must be unavailable during an isolated acceptance test. +Verify the restriction at the node/container-runtime pull path, not only with +a namespace NetworkPolicy. Use fresh nodes or establish that the tested images +are not already cached; a new namespace alone does not do this. Preserve proof +of the restriction and the resulting mirror pulls. + +Create a dedicated +namespace, an all-namespaces OperatorGroup, and a Subscription. Replace the +source name below with `metadata.name` from the generated CatalogSource. + +```yaml +apiVersion: v1 +kind: Namespace +metadata: + name: hyperfleet-system +--- +apiVersion: operators.coreos.com/v1 +kind: OperatorGroup +metadata: + name: hyperfleet + namespace: hyperfleet-system +spec: {} +--- +apiVersion: operators.coreos.com/v1alpha1 +kind: Subscription +metadata: + name: hyperfleet-operator + namespace: hyperfleet-system +spec: + channel: stable + name: hyperfleet-operator + source: REPLACE_WITH_GENERATED_CATALOGSOURCE_NAME + sourceNamespace: openshift-marketplace + installPlanApproval: Automatic +``` + +Save the manifest as `hyperfleet-subscription.yaml`, apply it, and wait for OLM +to install the operator: + +```bash +oc apply -f hyperfleet-subscription.yaml +oc get installplan,csv -n hyperfleet-system +``` + +With `installPlanApproval: Automatic`, OLM creates and approves the InstallPlan. +Wait for the installed CSV to reach `Succeeded`; do not hardcode a CSV version +from the development repository. If your cluster requires change control, use a +separate, manual-approval procedure: set `installPlanApproval: Manual`, inspect +the generated InstallPlan and its selected CSV, then approve that specific plan. +The bundle supports `AllNamespaces`; do not add `targetNamespaces` to this +OperatorGroup. + +Create a valid HyperFleetConfig and its referenced Secrets, with reachable +database and authentication services as required by that configuration. Confirm +operator and API readiness and inspect pod events for failed pulls. Compare +runtime images with the selected release bundle; multi-architecture image IDs +may identify platform manifests beneath the declared image index. + +For the release installed by this guide, provide the database Secret referenced +by `spec.api.database.secretRef.name`; database provisioning is outside this +installation procedure. Its keys are `db.host`, `db.port`, `db.name`, `db.user` +and `db.password`. Consult the API contract and installation documentation +shipped with the selected release for its database requirements. + +Do not substitute `operator-sdk run bundle` for this installation path: its +development catalog helpers are not the mirrored release catalog. + +Keep a sanitized installation record containing catalog and bundle digests, +package/channel, tool and cluster versions, export/import results, generated +mirror resources, isolation checks, and OLM/operand states. Never include +credentials. Successful archive transfer alone does not establish successful +installation or application readiness. + +References: [oc-mirror filtering](https://github.com/openshift/oc-mirror/blob/main/docs/features/filtering.md), +[generated cluster resources](https://github.com/openshift/oc-mirror/blob/main/docs/features/cluster-resources.md). diff --git a/docs/examples/imageset-config-catalog.yaml b/docs/examples/imageset-config-catalog.yaml new file mode 100644 index 0000000..6e78f6f --- /dev/null +++ b/docs/examples/imageset-config-catalog.yaml @@ -0,0 +1,11 @@ +# Replace the catalog placeholder with the published release catalog digest. +# Confirm the package and channel against that catalog before exporting. +apiVersion: mirror.openshift.io/v2alpha1 +kind: ImageSetConfiguration +mirror: + operators: + - catalog: registry.example.com/hyperfleet/hyperfleet-operator-catalog@sha256:0000000000000000000000000000000000000000000000000000000000000000 + packages: + - name: hyperfleet-operator + channels: + - name: stable diff --git a/go.mod b/go.mod index 7739d1a..f4ff8f4 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/openshift-hyperfleet/hyperfleet-operator go 1.26.0 require ( + github.com/distribution/reference v0.6.0 github.com/onsi/ginkgo/v2 v2.27.4 github.com/onsi/gomega v1.39.0 k8s.io/api v0.37.0 @@ -56,6 +57,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.24.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect diff --git a/go.sum b/go.sum index a49eda7..811536f 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= @@ -127,6 +129,8 @@ github.com/onsi/ginkgo/v2 v2.27.4 h1:fcEcQW/A++6aZAZQNUmNjvA9PSOzefMJBerHJ4t8v8Y github.com/onsi/ginkgo/v2 v2.27.4/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q= github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/hack/bundle/update_bundle.sh b/hack/bundle/update_bundle.sh new file mode 100755 index 0000000..7f81749 --- /dev/null +++ b/hack/bundle/update_bundle.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +set -euo pipefail + +CSV_FILE="${CSV_FILE:-/manifests/hyperfleet-operator.clusterserviceversion.yaml}" +YQ="${YQ:-yq}" + +require_digest_pullspec() { + local variable_name="$1" + local pullspec="${!variable_name:-}" + if [[ ! "${pullspec}" =~ ^[^[:space:]]+@sha256:[0-9a-f]{64}$ ]]; then + echo "error: ${variable_name} must be a non-empty sha256 digest pullspec, got '${pullspec}'" >&2 + exit 1 + fi +} + +require_digest_pullspec HYPERFLEET_OPERATOR_IMAGE_PULLSPEC +require_digest_pullspec HYPERFLEET_API_IMAGE_PULLSPEC +[[ -f "${CSV_FILE}" ]] || { echo "error: CSV not found: ${CSV_FILE}" >&2; exit 1; } + +# Fail before patching if the runtime override disappeared from the template. +# yq assignments to an empty selection can otherwise silently do nothing. +"${YQ}" eval -p yaml -o yaml -e ' + [.spec.install.spec.deployments[].spec.template.spec.containers[] + | select(.name == "manager")] | length == 1 +' "${CSV_FILE}" >/dev/null +if ! "${YQ}" eval -p yaml -o yaml -e ' + [.spec.install.spec.deployments[].spec.template.spec.containers[] + | select(.name == "manager") | .env[] + | select(.name == "RELATED_IMAGE_HYPERFLEET_API")] | length == 1 +' "${CSV_FILE}" >/dev/null; then + echo "error: CSV is missing exactly one RELATED_IMAGE_HYPERFLEET_API runtime override" >&2 + exit 1 +fi + +# Update image references in the CSV file using yq +"${YQ}" eval -p yaml -o yaml ' + # Update operator deployment image + (.spec.install.spec.deployments[].spec.template.spec.containers[] | select(.name == "manager") | .image) = strenv(HYPERFLEET_OPERATOR_IMAGE_PULLSPEC) | + + # Update RELATED_IMAGE_HYPERFLEET_API env var + (.spec.install.spec.deployments[].spec.template.spec.containers[] | select(.name == "manager") | .env[] | select(.name == "RELATED_IMAGE_HYPERFLEET_API") | .value) = strenv(HYPERFLEET_API_IMAGE_PULLSPEC) | + + # Update containerImage annotation + .metadata.annotations.containerImage = strenv(HYPERFLEET_OPERATOR_IMAGE_PULLSPEC) | + + # Update relatedImages + .spec.relatedImages = [ + {"name": "hyperfleet-operator", "image": strenv(HYPERFLEET_OPERATOR_IMAGE_PULLSPEC)}, + {"name": "hyperfleet-api", "image": strenv(HYPERFLEET_API_IMAGE_PULLSPEC)} + ] +' -i "${CSV_FILE}" + +cat "${CSV_FILE}" diff --git a/hack/oc-mirror.Dockerfile b/hack/oc-mirror.Dockerfile new file mode 100644 index 0000000..ac5f021 --- /dev/null +++ b/hack/oc-mirror.Dockerfile @@ -0,0 +1,35 @@ +# Container image providing oc-mirror v2 and skopeo for disconnected mirroring tests. +# By default, downloads the official OpenShift release binary from mirror.openshift.com. + +FROM registry.access.redhat.com/ubi9/ubi-minimal@sha256:7fbeae18dc9476399f565e68255f602a3374ea8614ba3d14843565131a13ff93 + +ARG TARGETARCH +ARG OCP_VERSION=4.18.18 +ARG OC_MIRROR_X86_64_SHA256=b41059474ecfd1ba4ebae3aa7d052ea33f337097d9bea85a4363646c43d1822c +ARG OC_MIRROR_AARCH64_SHA256=7bdb10ea539d9d5e16338eb6df32f9998aca66d39f6156be9e0127a4d9779f64 + +RUN microdnf install -y \ + tar \ + gzip \ + ca-certificates \ + shadow-utils \ + skopeo \ + && microdnf clean all + +RUN set -eux; \ + ARCH="${TARGETARCH:-$(uname -m)}"; \ + case "$ARCH" in \ + x86_64|amd64) ARCH_DIR="x86_64"; ARCHIVE_SHA256="$OC_MIRROR_X86_64_SHA256" ;; \ + aarch64|arm64) ARCH_DIR="aarch64"; ARCHIVE_SHA256="$OC_MIRROR_AARCH64_SHA256" ;; \ + *) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; \ + esac; \ + URL="https://mirror.openshift.com/pub/openshift-v4/${ARCH_DIR}/clients/ocp/${OCP_VERSION}/oc-mirror.tar.gz"; \ + echo "Downloading oc-mirror from ${URL}..."; \ + curl -fsSLo /tmp/oc-mirror.tar.gz "$URL"; \ + echo "${ARCHIVE_SHA256} /tmp/oc-mirror.tar.gz" | sha256sum -c -; \ + tar -xzf /tmp/oc-mirror.tar.gz -C /usr/local/bin oc-mirror; \ + rm /tmp/oc-mirror.tar.gz; \ + chmod +x /usr/local/bin/oc-mirror; \ + /usr/local/bin/oc-mirror version + +ENTRYPOINT ["/usr/local/bin/oc-mirror"] diff --git a/hack/test-disconnected-mirror.sh b/hack/test-disconnected-mirror.sh new file mode 100755 index 0000000..9d0d2c7 --- /dev/null +++ b/hack/test-disconnected-mirror.sh @@ -0,0 +1,284 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Catalog archive transfer test. This does not install the operator on a cluster. +CONTAINER_TOOL="${CONTAINER_TOOL:-docker}" +OC_MIRROR_IMAGE="${OC_MIRROR_IMAGE:-hyperfleet-oc-mirror:local}" +# Disposable destination registry for the isolated disk-to-mirror phase. +REGISTRY_IMAGE="${REGISTRY_IMAGE:-docker.io/library/registry@sha256:a3d8aaa63ed8681a604f1dea0aa03f100d5895b6a58ace528858a7b332415373}" +YQ="${YQ:-yq}" +KEEP_WORKSPACE="${KEEP_WORKSPACE:-false}" +CONTAINER_DNS="${CONTAINER_DNS:-}" +MIRROR_PLATFORM="${MIRROR_PLATFORM:-}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +fail() { echo "error: $*" >&2; exit 1; } + +if [[ -n "$MIRROR_PLATFORM" ]]; then + [[ "$MIRROR_PLATFORM" =~ ^linux/(amd64|arm64)$ ]] || + fail "MIRROR_PLATFORM must be linux/amd64 or linux/arm64, got $MIRROR_PLATFORM" +fi + +container_with_platform() { + local subcommand="$1" + shift + + if [[ -n "$MIRROR_PLATFORM" ]]; then + "$CONTAINER_TOOL" "$subcommand" --platform "$MIRROR_PLATFORM" "$@" + else + "$CONTAINER_TOOL" "$subcommand" "$@" + fi +} + +destination_for() { + local image="$1" + local repository="${image%@sha256:*}" + local digest="@sha256:${image##*@sha256:}" + local source mirror matched_source="" matched_mirror="" + + while IFS=$'\t' read -r source mirror; do + if [[ "$repository" == "$source" || "$repository" == "$source/"* ]] && + (( ${#source} > ${#matched_source} )); then + matched_source="$source" + matched_mirror="$mirror" + fi + done < <("$YQ" eval -r ' + .spec.imageDigestMirrors[] | [.source, .mirrors[0]] | @tsv + ' "$resources/idms-oc-mirror.yaml") + + [[ -n "$matched_source" ]] || return 1 + printf '%s%s%s\n' "$matched_mirror" "${repository#"$matched_source"}" "$digest" +} + +# connected_docker_run permits an explicit resolver only for the container that +# reads the connected source registry. The disconnected import must retain +# Docker's embedded DNS so it can resolve the disposable registry alias. +connected_docker_run() { + if [[ -n "$CONTAINER_DNS" ]]; then + container_with_platform run --rm --dns "$CONTAINER_DNS" "$@" + else + container_with_platform run --rm "$@" + fi +} + +verify_destination_image() { + local image="$1" + local platform_description="the runner platform" + local inspect_args=(inspect) + + if [[ -n "$MIRROR_PLATFORM" ]]; then + platform_description="$MIRROR_PLATFORM" + inspect_args+=( + --override-os "${MIRROR_PLATFORM%%/*}" + --override-arch "${MIRROR_PLATFORM#*/}" + ) + fi + + # The raw manifest establishes that the archive transfer retained the image. + container_with_platform run --rm --network "$network" \ + --entrypoint /usr/bin/skopeo "$OC_MIRROR_IMAGE" \ + inspect --raw --tls-verify=false "docker://$image" >/dev/null + + # Raw presence is insufficient for a CatalogSource or bundle image: the + # target platform must be able to select a runnable manifest from its index. + container_with_platform run --rm --network "$network" \ + --entrypoint /usr/bin/skopeo "$OC_MIRROR_IMAGE" \ + "${inspect_args[@]}" --tls-verify=false "docker://$image" >/dev/null || + fail "destination image cannot be selected for $platform_description: $image" +} + +for tool in "$CONTAINER_TOOL" "$YQ"; do + command -v "$tool" >/dev/null 2>&1 || fail "required command: $tool" +done +[[ "${CATALOG_IMG:-}" =~ ^[^[:space:]]+@sha256:[0-9a-f]{64}$ ]] || + fail "set CATALOG_IMG to the digest-pinned catalog to test" +for image in "$OC_MIRROR_IMAGE" "$REGISTRY_IMAGE"; do + "$CONTAINER_TOOL" image inspect "$image" >/dev/null 2>&1 || + fail "prepare test helper image on the connected host first: $image" +done + +workspace="$(mktemp -d "${TMPDIR:-/tmp}/hyperfleet-catalog-mirror.XXXXXX")" +connected="$workspace/connected" +disconnected="$workspace/disconnected" +mkdir -p "$connected/home" "$disconnected/home" "$disconnected/archive" +network="hyperfleet-catalog-$$" +registry_container="" +catalog_container="" +cleanup() { + # Never retain source registry credentials in an evidence workspace, even + # when the workspace is kept after a failure. + rm -f "$connected/auth.json" + [[ -z "$catalog_container" ]] || "$CONTAINER_TOOL" rm -f "$catalog_container" >/dev/null 2>&1 || true + [[ -z "$registry_container" ]] || "$CONTAINER_TOOL" rm -f "$registry_container" >/dev/null 2>&1 || true + "$CONTAINER_TOOL" network rm "$network" >/dev/null 2>&1 || true + if [[ "$KEEP_WORKSPACE" == true ]]; then + echo "evidence workspace: $workspace" + else + rm -rf "$workspace" + fi +} +trap cleanup EXIT + +# The catalog is the sole release input. Read its selected bundle and images to +# verify the imported registry without reading repository manifests or accepting +# a separately supplied bundle image. +"$YQ" eval '.mirror.operators[0].catalog = strenv(CATALOG_IMG)' \ + "$SCRIPT_DIR/../docs/examples/imageset-config-catalog.yaml" >"$connected/imageset-config.yaml" +package_name="$("$YQ" eval -r '.mirror.operators[0].packages[0].name' "$connected/imageset-config.yaml")" +channel_name="$("$YQ" eval -r '.mirror.operators[0].packages[0].channels[0].name' "$connected/imageset-config.yaml")" +[[ -n "$package_name" && "$package_name" != null && -n "$channel_name" && "$channel_name" != null ]] || + fail "ImageSetConfiguration must select one package and channel" + +# A file-based catalog exposes the FBC directory through this standard label. +# Pulling the immutable catalog is a connected-host operation and uses the +# container engine's registry login; oc-mirror has its own optional auth file. +container_with_platform pull "$CATALOG_IMG" >/dev/null +catalog_configs_path="$("$CONTAINER_TOOL" image inspect "$CATALOG_IMG" \ + --format '{{ index .Config.Labels "operators.operatorframework.io.index.configs.v1" }}')" +[[ "$catalog_configs_path" == /* ]] || + fail "catalog is not a file-based catalog with an index configs label" +mkdir -p "$workspace/catalog-configs" +catalog_container="$(container_with_platform create --pull=never --entrypoint /bin/true "$CATALOG_IMG")" +"$CONTAINER_TOOL" cp "$catalog_container:$catalog_configs_path/." "$workspace/catalog-configs" +"$CONTAINER_TOOL" rm "$catalog_container" >/dev/null +catalog_container="" + +catalog_files=() +while IFS= read -r -d '' file; do + catalog_files+=("$file") +done < <(find "$workspace/catalog-configs" -type f \( -name '*.yaml' -o -name '*.yml' -o -name '*.json' \) -print0) +(( ${#catalog_files[@]} > 0 )) || fail "catalog has no FBC metadata files" + +channel_entry_names=() +replaced_bundle_names=() +while IFS=$'\t' read -r bundle_name replaces; do + [[ -n "$bundle_name" ]] || continue + channel_entry_names+=("$bundle_name") + [[ -z "$replaces" || "$replaces" == null ]] || replaced_bundle_names+=("$replaces") +done < <(PACKAGE_NAME="$package_name" CHANNEL_NAME="$channel_name" "$YQ" eval-all -r ' + select(.schema == "olm.channel" and .package == strenv(PACKAGE_NAME) and .name == strenv(CHANNEL_NAME)) | + .entries[] | [.name, (.replaces // "")] | @tsv +' "${catalog_files[@]}") +channel_heads=() +for bundle_name in "${channel_entry_names[@]}"; do + is_replaced=false + for replaced_name in "${replaced_bundle_names[@]-}"; do + if [[ "$bundle_name" == "$replaced_name" ]]; then + is_replaced=true + break + fi + done + [[ "$is_replaced" == true ]] || channel_heads+=("$bundle_name") +done +(( ${#channel_heads[@]} == 1 )) || + fail "could not identify one head bundle for $package_name/$channel_name" + +expected_images=() +while IFS= read -r image; do + [[ -n "$image" ]] && expected_images+=("$image") +done < <(OLM_BUNDLE_NAME="${channel_heads[0]}" "$YQ" eval-all -r ' + select(.schema == "olm.bundle" and .name == strenv(OLM_BUNDLE_NAME)) | + (.image, .relatedImages[]?.image) +' "${catalog_files[@]}") +(( ${#expected_images[@]} > 0 )) || + fail "catalog head bundle ${channel_heads[0]} has no images" +for image in "${expected_images[@]}"; do + [[ "$image" =~ ^[^[:space:]]+@sha256:[0-9a-f]{64}$ ]] || + fail "catalog bundle contains a non-digest image: $image" +done + +mirror_args=(--v2 --config imageset-config.yaml file://archive) +if [[ -f "${REGISTRY_AUTH_FILE:-}" ]]; then + if "$YQ" -e '.credsStore? or .credHelpers?' "$REGISTRY_AUTH_FILE" >/dev/null 2>&1; then + fail "REGISTRY_AUTH_FILE uses a Docker credential helper; provide a containers auth file with inline auth entries" + fi + cp "$REGISTRY_AUTH_FILE" "$connected/auth.json" + mirror_args=(--authfile /work/auth.json "${mirror_args[@]}") +fi +# oc-mirror discovers bundles and related images from the selected catalog. +connected_docker_run \ + -e HOME=/work/home -v "$connected:/work:z" -w /work \ + "$OC_MIRROR_IMAGE" "${mirror_args[@]}" +# The auth file is needed only by the connected mirror operation. Remove it +# before any evidence workspace can be retained; cleanup also covers failures. +rm -f "$connected/auth.json" + +archives=("$connected"/archive/mirror_*.tar) +[[ -e "${archives[0]}" ]] || fail "mirror-to-disk produced no archives" +for archive in "${archives[@]}"; do + cp "$archive" "$disconnected/archive/" +done +cp "$connected/imageset-config.yaml" "$disconnected/" + +# The import gets only archives and configuration, without connected caches or +# source credentials. Its registry and runner share a network without egress. +if [[ "$CONTAINER_TOOL" == podman ]]; then + podman_network_backend="$("$CONTAINER_TOOL" info --format '{{.Host.NetworkBackend}}')" || + fail "could not determine the Podman network backend" + [[ "$podman_network_backend" != cni ]] || + fail "Podman CNI networking is unsupported: use netavark so the registry network alias resolves" +fi +"$CONTAINER_TOOL" network create --internal "$network" >/dev/null +registry_container="$(container_with_platform run -d --rm --pull=never --network "$network" \ + --network-alias registry "$REGISTRY_IMAGE")" + +registry_ready=false +registry_probe_output="" +for ((attempt = 1; attempt <= 30; attempt++)); do + registry_probe_output="$(container_with_platform run --rm --pull=never --network "$network" \ + --entrypoint /usr/bin/skopeo "$OC_MIRROR_IMAGE" \ + list-tags --tls-verify=false docker://registry:5000/readiness 2>&1)" || true + # A fresh registry returns NAME_UNKNOWN for this deliberately absent + # repository. That response proves registry:5000 accepted the request. + if [[ "$registry_probe_output" == *"repository name not known to registry"* ]]; then + registry_ready=true + break + fi + sleep 1 +done +[[ "$registry_ready" == true ]] || + fail "registry:5000 was not ready after 30 seconds: $registry_probe_output" + +# Check Docker Hub connectivity from the isolated runner. The internal network +# provides isolation; this probe alone does not test every public registry. +# Verify the pinned runner image and its documented skopeo entrypoint first so +# missing images, missing skopeo, and container startup errors fail the test. +skopeo_version="$(container_with_platform run --rm --pull=never --network "$network" \ + --entrypoint /usr/bin/skopeo "$OC_MIRROR_IMAGE" --version 2>&1)" || + fail "isolated runner could not execute /usr/bin/skopeo: $skopeo_version" +[[ "$skopeo_version" == skopeo\ version\ * ]] || + fail "unexpected skopeo version output: $skopeo_version" + +if isolation_error="$(container_with_platform run --rm --pull=never --network "$network" \ + --entrypoint /usr/bin/skopeo "$OC_MIRROR_IMAGE" \ + inspect docker://docker.io/library/registry:2 2>&1)"; then + fail "disconnected network unexpectedly reached Docker Hub" +fi +# Accept only a Docker Hub DNS or connection failure. Do not accept unrelated +# errors, such as an absent image or a failed container startup. +[[ "$isolation_error" == *"registry-1.docker.io"* ]] || + fail "unexpected Docker Hub isolation error: $isolation_error" +[[ "$isolation_error" =~ (dial\ tcp|no\ such\ host|server\ misbehaving|network\ is\ unreachable|i/o\ timeout) ]] || + fail "unexpected Docker Hub isolation error: $isolation_error" + +container_with_platform run --rm --network "$network" \ + -e HOME=/work/home -v "$disconnected:/work:z" -w /work \ + "$OC_MIRROR_IMAGE" --v2 --config imageset-config.yaml \ + --from file://archive --dest-tls-verify=false docker://registry:5000 + +resources="$disconnected/archive/working-dir/cluster-resources" +[[ -f "$resources/idms-oc-mirror.yaml" ]] || fail "import did not generate digest mirror mappings" +catalogs=("$resources"/cs-*.yaml) +[[ -e "${catalogs[0]}" ]] || fail "import did not generate a CatalogSource" +for manifest in "${catalogs[@]}"; do + target="$("$YQ" eval '.spec.image' "$manifest")" + [[ "$target" == registry:5000/* ]] || fail "CatalogSource does not use destination registry: $target" + verify_destination_image "$target" +done +for image in "${expected_images[@]}"; do + destination="$(destination_for "$image")" || + fail "generated IDMS does not cover catalog-selected image: $image" + verify_destination_image "$destination" +done +echo "catalog head bundle and all related images are present after archive import" +echo "OpenShift installation and operand readiness have NOT been tested" diff --git a/hack/test-verify-bundle-related-images.sh b/hack/test-verify-bundle-related-images.sh new file mode 100644 index 0000000..8f9729a --- /dev/null +++ b/hack/test-verify-bundle-related-images.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Negative checks run through the real bundle-image gate with temporary packaging +# inputs, rather than only testing hand-written verifier CSV fixtures. +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +YQ="${YQ:-yq}" +export YQ +workspace="$(mktemp -d "${TMPDIR:-/tmp}/hyperfleet-bundle-test.XXXXXX")" +trap 'rm -rf "$workspace"' EXIT +mkdir -p "$workspace/bundle/manifests" "$workspace/hack/bundle" +csv="$workspace/bundle/manifests/hyperfleet-operator.clusterserviceversion.yaml" + +reset_fixture() { + cp "$root/bundle/manifests/hyperfleet-operator.clusterserviceversion.yaml" "$csv" + cp "$root/bundle.konflux.Dockerfile" "$workspace/bundle.konflux.Dockerfile" + cp "$root/hack/bundle/update_bundle.sh" "$workspace/hack/bundle/update_bundle.sh" +} + +bundle_image_args() { + sed -n 's/^ARG \(HYPERFLEET_[A-Z0-9_]*_IMAGE_PULLSPEC\)="[^"]*"$/\1/p' "$1" +} + +load_bundle_image_pullspecs() { + local image_arg value image_arg_count=0 + + while IFS= read -r image_arg; do + value="$(sed -n "s/^ARG ${image_arg}=\"\\([^\"]*\\)\"$/\\1/p" "$workspace/bundle.konflux.Dockerfile")" + [[ -n "$value" ]] || { + echo "missing default for bundle image argument: $image_arg" >&2 + exit 1 + } + export "$image_arg=$value" + ((image_arg_count += 1)) + done < <(bundle_image_args "$workspace/bundle.konflux.Dockerfile") + ((image_arg_count > 0)) || { + echo "bundle Dockerfile has no image pullspec arguments" >&2 + exit 1 + } +} + +expect_failure() { + local description="$1" + local expected_message="$2" + + if bash "$root/hack/verify-bundle-related-images.sh" "$workspace" >"$workspace/result.log" 2>&1; then + echo "bundle-image gate unexpectedly accepted: $description" >&2 + exit 1 + fi + if ! grep -Fq -- "$expected_message" "$workspace/result.log"; then + echo "bundle-image gate returned the wrong diagnostic for: $description" >&2 + cat "$workspace/result.log" >&2 + exit 1 + fi + echo "bundle-image negative check passed: $description" +} + +reset_fixture +"$YQ" eval -i ' + del(.spec.install.spec.deployments[].spec.template.spec.containers[].env[] | + select(.name == "RELATED_IMAGE_HYPERFLEET_API")) | + del(.spec.relatedImages[] | select(.name == "hyperfleet-api")) +' "$csv" +expect_failure \ + "API missing from both override and relatedImages" \ + "CSV is missing exactly one RELATED_IMAGE_HYPERFLEET_API runtime override" + +reset_fixture +load_bundle_image_pullspecs +CSV_FILE="$csv" bash "$workspace/hack/bundle/update_bundle.sh" >/dev/null +"$YQ" eval -r '.spec.relatedImages[]?.name | select(. != null)' "$csv" \ + >"$workspace/related-image-names" + +related_image_count=0 +while IFS= read -r related_image; do + ((related_image_count += 1)) + reset_fixture + # Simulate a regression in bundle packaging that drops each declared image + # after update_bundle.sh has populated the final CSV. + export RELATED_IMAGE_NAME="$related_image" + printf '\n"$YQ" eval -i '\''del(.spec.relatedImages[] | select(.name == strenv(RELATED_IMAGE_NAME)))'\'' "$CSV_FILE"\n' \ + >>"$workspace/hack/bundle/update_bundle.sh" + expect_failure \ + "$related_image missing from the transformed bundle CSV" \ + "CSV spec.relatedImages is missing CSV image sources entry \"$related_image\"" + unset RELATED_IMAGE_NAME +done <"$workspace/related-image-names" +((related_image_count > 0)) || { + echo "bundle CSV has no relatedImages to test" >&2 + exit 1 +} + +image_arg_count=0 +while IFS= read -r image_arg; do + [[ "$image_arg" =~ ^HYPERFLEET_[A-Z0-9_]+_IMAGE_PULLSPEC$ ]] || { + echo "unexpected bundle image argument: $image_arg" >&2 + exit 1 + } + ((image_arg_count += 1)) + reset_fixture + sed -E "s|^ARG ${image_arg}=\"[^\"]*\"$|ARG ${image_arg}=\"example.com/invalid:latest\"|" \ + "$root/bundle.konflux.Dockerfile" >"$workspace/bundle.konflux.Dockerfile" + expect_failure \ + "mutable $image_arg" \ + "$image_arg must be a non-empty sha256 digest pullspec" +done < <(bundle_image_args "$root/bundle.konflux.Dockerfile") +((image_arg_count > 0)) || { + echo "bundle Dockerfile has no image pullspec arguments to test" >&2 + exit 1 +} diff --git a/hack/verify-bundle-related-images.sh b/hack/verify-bundle-related-images.sh new file mode 100644 index 0000000..b233f4a --- /dev/null +++ b/hack/verify-bundle-related-images.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Check the same CSV transformation used by the bundle build without +# modifying generated manifests or requiring a container runtime in PR CI. +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source_root="${1:-$root}" +YQ="${YQ:-yq}" +export YQ +command -v "$YQ" >/dev/null 2>&1 || { echo "required command: $YQ" >&2; exit 1; } + +# Read defaults as data, never source/eval a Dockerfile. Duplicate or missing +# declarations fail the digest checks in update_bundle.sh. +for variable in HYPERFLEET_OPERATOR_IMAGE_PULLSPEC HYPERFLEET_API_IMAGE_PULLSPEC; do + value="$(sed -n "s/^ARG ${variable}=\"\([^\"]*\)\"$/\1/p" "$source_root/bundle.konflux.Dockerfile")" + export "$variable=$value" +done + +workspace="$(mktemp -d "${TMPDIR:-/tmp}/hyperfleet-bundle-check.XXXXXX")" +trap 'rm -rf "$workspace"' EXIT +cp "$source_root/bundle/manifests/hyperfleet-operator.clusterserviceversion.yaml" "$workspace/bundle.yaml" +export CSV_FILE="$workspace/bundle.yaml" +bash "$source_root/hack/bundle/update_bundle.sh" >/dev/null +cd "$root" +go run ./hack/verify-related-images -csv "$CSV_FILE" diff --git a/hack/verify-related-images/integration_test.go b/hack/verify-related-images/integration_test.go new file mode 100644 index 0000000..60e1436 --- /dev/null +++ b/hack/verify-related-images/integration_test.go @@ -0,0 +1,154 @@ +// Copyright 2026. +// +// 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. + +//go:build integration + +package main + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "sigs.k8s.io/yaml" +) + +// Exercise the bundle patching entrypoint with images that differ from the +// source CSV. This models the Konflux build boundary without registry access. +func TestBundleBuildImageUpdate(t *testing.T) { + yq, err := exec.LookPath("yq") + if err != nil { + t.Skip("yq is required for the bundle build integration test") + } + + t.Run("updates image references", func(t *testing.T) { + csvPath := writeCSV(t, validCSV()) + output, err := runBundleUpdate(t, yq, csvPath, extraImage, operatorImage) + if err != nil { + t.Fatalf("%v: %s", err, output) + } + data, err := os.ReadFile(csvPath) + if err != nil { + t.Fatal(err) + } + if err := verifyCSV(data); err != nil { + t.Fatal(err) + } + + assertBundleImageReferences(t, data, extraImage, operatorImage) + if strings.Contains(string(data), apiImage) { + t.Fatal("old source digest survived bundle build") + } + }) + + t.Run("rejects missing runtime override", func(t *testing.T) { + missing := strings.Replace(validCSV(), "RELATED_IMAGE_HYPERFLEET_API", "UNRELATED_SETTING", 1) + csvPath := writeCSV(t, missing) + output, err := runBundleUpdate(t, yq, csvPath, extraImage, operatorImage) + if err == nil { + t.Fatalf("bundle build accepted missing API override: %s", output) + } + if got := string(output); !strings.Contains(got, "CSV is missing exactly one RELATED_IMAGE_HYPERFLEET_API runtime override") { + t.Fatalf("missing API override error = %q", got) + } + }) + + t.Run("rejects tagged manager image", func(t *testing.T) { + csvPath := writeCSV(t, validCSV()) + rejectedPullspec := "registry.example.com/operator:latest" + output, err := runBundleUpdate(t, yq, csvPath, rejectedPullspec, apiImage) + if err == nil { + t.Fatalf("bundle build accepted tagged manager image: %s", output) + } + if got := string(output); !strings.Contains(got, rejectedPullspec) { + t.Fatalf("tagged image error = %q, want rejected pullspec %q", got, rejectedPullspec) + } + }) +} + +func assertBundleImageReferences(t *testing.T, data []byte, operatorPullspec, apiPullspec string) { + t.Helper() + var csv csvDocument + if err := yaml.Unmarshal(data, &csv); err != nil { + t.Fatalf("parse updated CSV: %v", err) + } + var metadata struct { + Metadata struct { + Annotations map[string]string `json:"annotations"` + } `json:"metadata"` + } + if err := yaml.Unmarshal(data, &metadata); err != nil { + t.Fatalf("parse updated CSV metadata: %v", err) + } + + var managerImage, apiOverride string + for _, deployment := range csv.Spec.Install.Spec.Deployments { + for _, container := range deployment.Spec.Template.Spec.Containers { + if container.Name != "manager" { + continue + } + managerImage = container.Image + for _, variable := range container.Env { + if variable.Name == "RELATED_IMAGE_HYPERFLEET_API" { + apiOverride = variable.Value + } + } + } + } + var operatorRelatedImage, apiRelatedImage string + for _, image := range csv.Spec.RelatedImages { + switch image.Name { + case "hyperfleet-operator": + operatorRelatedImage = image.Image + case "hyperfleet-api": + apiRelatedImage = image.Image + } + } + + for _, check := range []struct { + location string + got string + want string + }{ + {"manager image", managerImage, operatorPullspec}, + {"metadata.annotations.containerImage", metadata.Metadata.Annotations["containerImage"], operatorPullspec}, + {"related image hyperfleet-operator", operatorRelatedImage, operatorPullspec}, + {"RELATED_IMAGE_HYPERFLEET_API", apiOverride, apiPullspec}, + {"related image hyperfleet-api", apiRelatedImage, apiPullspec}, + } { + if check.got != check.want { + t.Errorf("%s = %q, want requested pullspec %q", check.location, check.got, check.want) + } + } +} + +func writeCSV(t *testing.T, contents string) string { + t.Helper() + path := filepath.Join(t.TempDir(), "bundle.csv") + if err := os.WriteFile(path, []byte(contents), 0600); err != nil { + t.Fatal(err) + } + return path +} + +func runBundleUpdate(t *testing.T, yq, csvPath, operatorPullspec, apiPullspec string) ([]byte, error) { + t.Helper() + cmd := exec.CommandContext(t.Context(), "bash", "../bundle/update_bundle.sh") + cmd.Env = append(os.Environ(), "YQ="+yq, "CSV_FILE="+csvPath, + "HYPERFLEET_OPERATOR_IMAGE_PULLSPEC="+operatorPullspec, + "HYPERFLEET_API_IMAGE_PULLSPEC="+apiPullspec) + return cmd.CombinedOutput() +} diff --git a/hack/verify-related-images/main.go b/hack/verify-related-images/main.go new file mode 100644 index 0000000..3f90166 --- /dev/null +++ b/hack/verify-related-images/main.go @@ -0,0 +1,210 @@ +// Copyright 2026. +// +// 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. + +// verify-related-images validates image metadata in a final, built bundle CSV. +// It deliberately does not compare bundle image references to development manifests. +package main + +import ( + _ "crypto/sha256" // Register SHA-256 for go-digest validation. + "errors" + "flag" + "fmt" + "os" + "slices" + "strings" + + "github.com/distribution/reference" + "github.com/openshift-hyperfleet/hyperfleet-operator/internal/component/api" + "sigs.k8s.io/yaml" +) + +const relatedImagePrefix = "RELATED_IMAGE_" + +type imageEntry struct { + Name string `json:"name"` + Image string `json:"image"` +} + +type csvDocument struct { + Spec struct { + Install struct { + Spec struct { + Deployments []struct { + Spec struct { + Template struct { + Spec struct { + Containers []container `json:"containers"` + } `json:"spec"` + } `json:"template"` + } `json:"spec"` + } `json:"deployments"` + } `json:"spec"` + } `json:"install"` + RelatedImages []imageEntry `json:"relatedImages"` + } `json:"spec"` +} + +type container struct { + Name string `json:"name"` + Image string `json:"image"` + Env []env `json:"env"` +} + +type env struct { + Name string `json:"name"` + Value string `json:"value"` +} + +func main() { + csvPath := flag.String("csv", "", "required: CSV extracted from the built bundle") + flag.Parse() + if *csvPath == "" { + fmt.Fprintln(os.Stderr, + "provide -csv pointing to the final built bundle CSV; development manifests are not bundle inputs") + os.Exit(1) + } + data, err := os.ReadFile(*csvPath) + if err == nil { + err = verifyCSV(data) + } + if err != nil { + fmt.Fprintf(os.Stderr, "related image verification failed:\n%v\n", err) + os.Exit(1) + } + fmt.Println("bundle related image verification passed") +} + +func verifyCSV(data []byte) error { + var csv csvDocument + if err := yaml.Unmarshal(data, &csv); err != nil { + return fmt.Errorf("parse CSV YAML: %w", err) + } + images, problems := deployableImages(csv) + // Require the same override consumed by the runtime. Absence is an error + // even if relatedImages also omits the API: otherwise runtime uses a fallback + // which the bundle mirroring metadata does not describe. + apiName := strings.ToLower(strings.ReplaceAll(strings.TrimPrefix(api.RelatedImageEnv, relatedImagePrefix), "_", "-")) + if !slices.ContainsFunc(images, func(entry imageEntry) bool { return entry.Name == apiName }) { + problems = append(problems, "CSV manager is missing required runtime override "+api.RelatedImageEnv) + } + problems = append(problems, + compareImages("CSV image sources", images, "CSV spec.relatedImages", csv.Spec.RelatedImages)...) + return problemError(problems) +} + +func deployableImages(csv csvDocument) ([]imageEntry, []string) { + var managerImages []string + var images []imageEntry + problems := []string{} + for _, deployment := range csv.Spec.Install.Spec.Deployments { + for _, c := range deployment.Spec.Template.Spec.Containers { + if c.Name == "manager" { + managerImages = append(managerImages, c.Image) + } + if c.Name != "manager" { + continue + } + for _, variable := range c.Env { + if !strings.HasPrefix(variable.Name, relatedImagePrefix) { + continue + } + suffix := strings.TrimPrefix(variable.Name, relatedImagePrefix) + if suffix == "" { + problems = append(problems, "CSV contains an empty RELATED_IMAGE_ variable name") + continue + } + images = append(images, imageEntry{ + Name: strings.ToLower(strings.ReplaceAll(suffix, "_", "-")), + Image: variable.Value, + }) + } + } + } + + if len(managerImages) != 1 { + problems = append(problems, fmt.Sprintf("expected exactly one manager container, found %d", len(managerImages))) + } else { + images = append([]imageEntry{{Name: "hyperfleet-operator", Image: managerImages[0]}}, images...) + } + return images, problems +} + +func compareImages(leftLabel string, left []imageEntry, rightLabel string, right []imageEntry) []string { + problems := []string{} + leftByName, leftProblems := imageMap(leftLabel, left) + rightByName, rightProblems := imageMap(rightLabel, right) + problems = append(problems, leftProblems...) + problems = append(problems, rightProblems...) + + for name, image := range leftByName { + if actual, ok := rightByName[name]; !ok { + problems = append(problems, fmt.Sprintf("%s is missing %s entry %q", rightLabel, leftLabel, name)) + } else if actual != image { + problems = append(problems, fmt.Sprintf("%s entry %q is %q, expected %q", rightLabel, name, actual, image)) + } + } + for name := range rightByName { + if _, ok := leftByName[name]; !ok { + problems = append(problems, fmt.Sprintf("%s has undeclared entry %q", rightLabel, name)) + } + } + return problems +} + +func imageMap(label string, images []imageEntry) (map[string]string, []string) { + result := make(map[string]string, len(images)) + seenImages := make(map[string]string, len(images)) + problems := []string{} + for _, image := range images { + if previous, ok := result[image.Name]; ok { + problems = append(problems, + fmt.Sprintf("%s has duplicate name %q (%q and %q)", label, image.Name, previous, image.Image), + ) + continue + } + if previous, ok := seenImages[image.Image]; ok { + problems = append(problems, + fmt.Sprintf("%s has duplicate image %q (%s and %s)", label, image.Image, previous, image.Name), + ) + } + result[image.Name] = image.Image + seenImages[image.Image] = image.Name + if !isSHA256DigestPullspec(image.Image) { + problems = append(problems, fmt.Sprintf("mutable or malformed %s image %q: %q", label, image.Name, image.Image)) + } + } + return result, problems +} + +func isSHA256DigestPullspec(image string) bool { + named, err := reference.ParseNormalizedNamed(image) + if err != nil { + return false + } + digested, ok := named.(reference.Digested) + if !ok { + return false + } + digest := digested.Digest() + return digest.Algorithm().String() == "sha256" && digest.Validate() == nil +} + +func problemError(problems []string) error { + if len(problems) == 0 { + return nil + } + slices.Sort(problems) + return errors.New("- " + strings.Join(problems, "\n- ")) +} diff --git a/hack/verify-related-images/main_test.go b/hack/verify-related-images/main_test.go new file mode 100644 index 0000000..2c66b35 --- /dev/null +++ b/hack/verify-related-images/main_test.go @@ -0,0 +1,95 @@ +// Copyright 2026. +// +// 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. + +package main + +import ( + "strings" + "testing" +) + +var ( + operatorImage = "registry.example.com/hyperfleet-operator@sha256:" + strings.Repeat("1", 64) + apiImage = "registry.example.com/hyperfleet-api@sha256:" + strings.Repeat("2", 64) + extraImage = "registry.example.com/extra@sha256:" + strings.Repeat("3", 64) +) + +func validCSV() string { + return `apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +spec: + install: + spec: + deployments: + - name: hyperfleet-operator-controller-manager + spec: + template: + spec: + containers: + - name: manager + image: ` + operatorImage + ` + env: + - name: RELATED_IMAGE_HYPERFLEET_API + value: ` + apiImage + ` + relatedImages: + - name: hyperfleet-operator + image: ` + operatorImage + ` + - name: hyperfleet-api + image: ` + apiImage + ` +` +} + +func TestVerifyBuiltBundleCSV(t *testing.T) { + tests := []struct { + name string + mutate func(string) string + want string + }{ + {"valid bundle", func(s string) string { return s }, ""}, + {"different bundle", func(s string) string { return strings.ReplaceAll(s, apiImage, extraImage) }, ""}, + {"stale related image", func(s string) string { + return strings.Replace(s, " image: "+apiImage, " image: "+extraImage, 1) + }, "expected"}, + {"tagged bundle image", func(s string) string { + return strings.ReplaceAll(s, apiImage, "registry.example.com/api:latest") + }, "mutable or malformed"}, + {"URL-style bundle image", func(s string) string { + return strings.ReplaceAll(s, apiImage, "https://registry.example.com/api@sha256:"+strings.Repeat("4", 64)) + }, "mutable or malformed"}, + {"duplicate", func(s string) string { + return s + " - name: hyperfleet-api\n image: " + apiImage + "\n" + }, "duplicate name"}, + {"missing API everywhere", func(s string) string { + env := " env:\n - name: RELATED_IMAGE_HYPERFLEET_API\n" + + " value: " + apiImage + "\n" + s = strings.Replace(s, env, "", 1) + return strings.Replace(s, " - name: hyperfleet-api\n image: "+apiImage+"\n", "", 1) + }, "missing required runtime override"}, + {"missing manager", func(s string) string { + return strings.Replace(s, "name: manager", "name: other", 1) + }, "expected exactly one manager"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := verifyCSV([]byte(tt.mutate(validCSV()))) + if tt.want == "" { + if err != nil { + t.Fatal(err) + } + } else if err == nil || !strings.Contains(err.Error(), tt.want) { + t.Fatalf("error = %v, want %q", err, tt.want) + } + }) + } +} diff --git a/internal/component/api/api.go b/internal/component/api/api.go index 52981af..73c6ae8 100644 --- a/internal/component/api/api.go +++ b/internal/component/api/api.go @@ -26,30 +26,13 @@ import ( hyperfleetv1alpha1 "github.com/openshift-hyperfleet/hyperfleet-operator/api/v1alpha1" ) -// DefaultImage is the compiled-in fallback image used when the operator is not -// given RELATED_IMAGE_HYPERFLEET_API. Production deployments override it with a -// digest-pinned image via that env var (OLM relatedImages convention). -// -// Must be v0.3.0 or later: config.yaml renders entities (pkg/registry) and the -// multi-issuer server.jwt.configs list, neither of which exist in v0.2.x's -// config schema — the API's loader uses viper's UnmarshalExact, so a v0.2.x -// binary rejects this config and crash-loops at startup (verified field-for-field -// against hyperfleet-api's pkg/config/server.go and pkg/registry at each tag; -// see PR #6 review discussion). -// -// Pinned to 0.4.0 specifically because it also carries -// HYPERFLEET-1603 (hyperfleet-api#364, merged to main 2026-09-01): database -// credentials via HYPERFLEET_DATABASE_*_FILE (ResolveFileOverrides in -// pkg/config/db.go) — verified field-for-field present at the v0.4.0 git tag. -// -// Registry is redhat-services-prod, not openshift-hyperfleet: hyperfleet-api's -// Konflux pipelines (.tekton/hyperfleet-api-{push,tag}.yaml) publish only to -// the redhat-user-workloads staging tenant; a separate Konflux Release step -// promotes to redhat-services-prod, which is what actually carries a signed, -// pullable 0.4.0 tag (verified against quay.io's tag API — the tag has no "v" -// prefix there, unlike the hyperfleet-api git tag). openshift-hyperfleet/hyperfleet-api -// is a legacy pre-Konflux registry that stopped receiving pushes after v0.2.1 -// and never got a v0.4.0 image at all (PR #6 review comment r3905519856). +// RelatedImageEnv is the release image override supplied by the bundle CSV. +const RelatedImageEnv = "RELATED_IMAGE_HYPERFLEET_API" + +// DefaultImage is the compatibility fallback for development without an image +// override. Konflux sets RelatedImageEnv in release bundles independently of this +// fallback. The API must support the rendered entity/JWT config and database +// HYPERFLEET_DATABASE_*_FILE variables (v0.4.0+). const DefaultImage = "quay.io/redhat-services-prod/hyperfleet-tenant/hyperfleet/hyperfleet-api:0.4.0" // Component renders the HyperFleet API operand. It satisfies the bundle.Component diff --git a/internal/component/api/api_test.go b/internal/component/api/api_test.go index 3c10a3d..f51501f 100644 --- a/internal/component/api/api_test.go +++ b/internal/component/api/api_test.go @@ -152,6 +152,10 @@ func TestRenderDeployment(t *testing.T) { g.Expect(c.SecurityContext.ReadOnlyRootFilesystem).To(HaveValue(BeTrue())) g.Expect(c.SecurityContext.AllowPrivilegeEscalation).To(HaveValue(BeFalse())) g.Expect(dep.Spec.Template.Spec.SecurityContext.RunAsNonRoot).To(HaveValue(BeTrue())) + // A fixed UID or fsGroup is rejected by OpenShift's restricted SCC. Leave + // both unset so the platform can allocate values from the namespace range. + g.Expect(dep.Spec.Template.Spec.SecurityContext.RunAsUser).To(BeNil()) + g.Expect(dep.Spec.Template.Spec.SecurityContext.FSGroup).To(BeNil()) // The config ConfigMap is mounted read-only at the expected path. var mountedConfig bool diff --git a/internal/component/api/render.go b/internal/component/api/render.go index 5880bce..1997aad 100644 --- a/internal/component/api/render.go +++ b/internal/component/api/render.go @@ -243,9 +243,10 @@ func deployment(cr *hyperfleetv1alpha1.HyperFleetConfig, image, namespace string ServiceAccountName: ResourceName, TerminationGracePeriodSeconds: ptr.To[int64](70), SecurityContext: &corev1.PodSecurityContext{ + // Do not set a fixed UID or fsGroup. OpenShift's restricted SCC + // assigns values from the project's allocated range, while the + // image remains required to run as non-root on every platform. RunAsNonRoot: ptr.To(true), - RunAsUser: ptr.To[int64](65532), - FSGroup: ptr.To[int64](65532), }, Containers: []corev1.Container{{ Name: ResourceName, diff --git a/tools/go.mod b/tools/go.mod index 3929857..c76d82b 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -4,6 +4,7 @@ go 1.26.0 tool ( github.com/golangci/golangci-lint/v2/cmd/golangci-lint + github.com/mikefarah/yq/v4 sigs.k8s.io/controller-runtime/tools/setup-envtest sigs.k8s.io/controller-tools/cmd/controller-gen sigs.k8s.io/kustomize/kustomize/v5 @@ -30,8 +31,10 @@ require ( github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/MirrexOne/unqueryvet v1.5.4 // indirect github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect + github.com/a8m/envsubst v1.4.2 // indirect github.com/alecthomas/chroma/v2 v2.24.1 // indirect github.com/alecthomas/go-check-sumtype v0.3.1 // indirect + github.com/alecthomas/participle/v2 v2.1.1 // indirect github.com/alexkohler/nakedret/v2 v2.0.6 // indirect github.com/alexkohler/prealloc v1.1.0 // indirect github.com/alfatraining/structtag v1.0.0 // indirect @@ -67,7 +70,9 @@ require ( github.com/dave/dst v0.27.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/denis-tingaikin/go-header v0.5.0 // indirect + github.com/dimchansky/utfbom v1.1.1 // indirect github.com/dlclark/regexp2 v1.12.0 // indirect + github.com/elliotchance/orderedmap v1.5.1 // indirect github.com/ettle/strcase v0.2.0 // indirect github.com/fatih/color v1.19.0 // indirect github.com/fatih/structtag v1.2.0 // indirect @@ -105,6 +110,8 @@ require ( github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect github.com/gobuffalo/flect v1.0.3 // indirect github.com/gobwas/glob v0.2.3 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/goccy/go-yaml v1.11.3 // indirect github.com/godoc-lint/godoc-lint v0.11.2 // indirect github.com/gofrs/flock v0.13.0 // indirect github.com/golangci/asciicheck v0.5.0 // indirect @@ -133,6 +140,7 @@ require ( github.com/hexops/gotextdiff v1.0.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jgautheron/goconst v1.10.0 // indirect + github.com/jinzhu/copier v0.4.0 // indirect github.com/jjti/go-spancheck v0.6.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/julz/importas v0.2.0 // indirect @@ -151,7 +159,7 @@ require ( github.com/leonklingele/grouper v1.1.2 // indirect github.com/lucasb-eyer/go-colorful v1.4.0 // indirect github.com/macabu/inamedparam v0.2.0 // indirect - github.com/magiconair/properties v1.8.6 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect github.com/manuelarte/funcorder v0.6.0 // indirect github.com/maratori/testableexamples v1.0.1 // indirect @@ -161,6 +169,7 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.23 // indirect github.com/mgechev/revive v1.15.0 // indirect + github.com/mikefarah/yq/v4 v4.44.1 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -228,6 +237,7 @@ require ( github.com/yagipy/maintidx v1.0.0 // indirect github.com/yeya24/promlinter v0.3.0 // indirect github.com/ykadowak/zerologlint v0.1.5 // indirect + github.com/yuin/gopher-lua v1.1.1 // indirect gitlab.com/bosi/decorder v0.4.2 // indirect go-simpler.org/musttag v0.14.0 // indirect go-simpler.org/sloglint v0.12.0 // indirect @@ -244,12 +254,14 @@ require ( golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect golang.org/x/tools v0.44.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect honnef.co/go/tools v0.7.0 // indirect diff --git a/tools/go.sum b/tools/go.sum index ac57a6b..c361072 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -40,12 +40,16 @@ github.com/MirrexOne/unqueryvet v1.5.4 h1:38QOxShO7JmMWT+eCdDMbcUgGCOeJphVkzzRgy github.com/MirrexOne/unqueryvet v1.5.4/go.mod h1:fs9Zq6eh1LRIhsDIsxf9PONVUjYdFHdtkHIgZdJnyPU= github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4= github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo= +github.com/a8m/envsubst v1.4.2 h1:4yWIHXOLEJHQEFd4UjrWDrYeYlV7ncFWJOCBRLOZHQg= +github.com/a8m/envsubst v1.4.2/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM= github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI= github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU= github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E= +github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= +github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQiFSsuzQ= @@ -127,8 +131,12 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8= github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= +github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/elliotchance/orderedmap v1.5.1 h1:G1X4PYlljzimbdQ3RXmtIZiQ9d6aRQ3sH1nzjq5mECE= +github.com/elliotchance/orderedmap v1.5.1/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys= github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= @@ -224,6 +232,10 @@ github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4 github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-yaml v1.11.3 h1:B3W9IdWbvrUu2OYQGwvU1nZtvMQJPBKgBUuweJjLj6I= +github.com/goccy/go-yaml v1.11.3/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= github.com/godoc-lint/godoc-lint v0.11.2 h1:Bp0FkJWoSdNsBikdNgIcgtaoo+xz6I/Y9s5WSBQUeeM= github.com/godoc-lint/godoc-lint v0.11.2/go.mod h1:iVpGdL1JCikNH2gGeAn3Hh+AgN5Gx/I/cxV+91L41jo= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= @@ -299,6 +311,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jgautheron/goconst v1.10.0 h1:Ptt+OoE4NaEWKhLrWrrN3IpZdGLiqaf7WLnEX/iv4Jw= github.com/jgautheron/goconst v1.10.0/go.mod h1:0p+wv1lFOiUr0IlNNT1nrm6+8DB8u2sU6KHGzFRXHDc= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8= github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -344,6 +358,8 @@ github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddB github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww= github.com/manuelarte/embeddedstructfieldcheck v0.4.0/go.mod h1:z8dFSyXqp+fC6NLDSljRJeNQJJDWnY7RoWFzV3PC6UM= github.com/manuelarte/funcorder v0.6.0 h1:0hBngc4fa1IgNiI65A7sFGkMvoMCc878RjqB5V7rWP0= @@ -364,6 +380,8 @@ github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3Ry github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mgechev/revive v1.15.0 h1:vJ0HzSBzfNyPbHKolgiFjHxLek9KUijhqh42yGoqZ8Q= github.com/mgechev/revive v1.15.0/go.mod h1:LlAKO3QQe9OJ0pVZzI2GPa8CbXGZ/9lNpCGvK4T/a8A= +github.com/mikefarah/yq/v4 v4.44.1 h1:FqnryFyO2MLfxKHYlGbrqpqPA8so7KAyuc7ra9vY+vQ= +github.com/mikefarah/yq/v4 v4.44.1/go.mod h1:mpBUHgdchicbt1rSHO0A3GKDHy6eVON14WXMLfpU3kg= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -540,6 +558,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo= gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8= go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ= @@ -670,6 +690,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= @@ -688,6 +710,8 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 h1:6D+BvnJ/j6e222UW8s2qTSe3wGBtvo0MbVQG/c5k8RE= +gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473/go.mod h1:N1eN2tsCx0Ydtgjl4cqmbRCsY4/+z4cYDeqwZTk6zog= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=