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..a611cdc 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: @@ -38,6 +39,8 @@ spec: value: . - name: prefetch-input value: '{"type": "gomod"}' + - name: hermetic + value: "true" pipelineSpec: description: | This pipeline is ideal for building container images from a Containerfile while maintaining trust after pipeline customization. diff --git a/Makefile b/Makefile index 87fc497..1d66e8c 100644 --- a/Makefile +++ b/Makefile @@ -120,10 +120,24 @@ cleanup-test-e2e: ## Tear down the Kind cluster used for e2e tests ##@ Lint +LOCALBIN ?= $(shell pwd)/bin + +.PHONY: verify-related-images +verify-related-images: ## Verify a final release CSV (CSV_FILE is required). + @test -n "$(CSV_FILE)" || { echo "Set CSV_FILE to the CSV extracted from the release bundle"; exit 1; } + go run ./hack/verify-related-images -csv "$(CSV_FILE)" + .PHONY: lint -lint: ## Run golangci-lint linter +lint: verify-release-images ## Check release image metadata and run golangci-lint. $(GOLANGCI_LINT) run +.PHONY: verify-release-images +verify-release-images: ## Transform the repository bundle CSV and verify its release images. + yq_path=$$($(call gotool,-n yq)); \ + YQ="$$yq_path" bash ./hack/verify-release-images.sh; \ + PATH="$$(dirname "$$yq_path"):$$PATH" go test -tags integration ./hack/verify-related-images; \ + YQ="$$yq_path" bash ./hack/test-release-images.sh + .PHONY: lint-fix lint-fix: ## Run golangci-lint linter and perform fixes $(GOLANGCI_LINT) run --fix @@ -134,6 +148,19 @@ lint-config: ## Verify golangci-lint linter configuration ##@ Build +OC_MIRROR_IMAGE ?= hyperfleet-oc-mirror:local + +.PHONY: build-oc-mirror-image +build-oc-mirror-image: check-container-tool ## Build the containerized oc-mirror runner. + "$(CONTAINER_TOOL)" build -f hack/oc-mirror.Dockerfile -t "$(OC_MIRROR_IMAGE)" . + +.PHONY: test-disconnected-mirror +test-disconnected-mirror: check-container-tool ## Test catalog export and isolated archive import (no cluster install). + CONTAINER_TOOL="$(CONTAINER_TOOL)" OC_MIRROR_IMAGE="$(OC_MIRROR_IMAGE)" \ + IMAGE_SET_CONFIG="$(IMAGE_SET_CONFIG)" RELEASE_CSV="$(RELEASE_CSV)" \ + BUNDLE_IMAGE="$(BUNDLE_IMAGE)" CONTAINER_DNS="$(CONTAINER_DNS)" \ + ./hack/test-disconnected-mirror.sh + .PHONY: build build: manifests generate fmt vet ## Build manager binary. go build -o bin/manager cmd/main.go @@ -178,10 +205,12 @@ GOFLAGS ?= -trimpath .PHONY: check-container-tool check-container-tool: -ifndef CONTAINER_TOOL - @echo "Error: No container tool found (docker or podman)" - @exit 1 -endif + @case "$(CONTAINER_TOOL)" in \ + docker|podman) command -v "$(CONTAINER_TOOL)" >/dev/null 2>&1 || { \ + echo "Error: container tool is not installed: $(CONTAINER_TOOL)"; exit 1; \ + } ;; \ + *) echo "Error: CONTAINER_TOOL must be docker or podman"; exit 1 ;; \ + esac .PHONY: image image: check-container-tool manifests generate fmt vet ## Build container image with configurable registry/tag @@ -388,7 +417,6 @@ catalog-push: ## Push a catalog image. ##@ Dependencies ## Location to install dependencies to -LOCALBIN ?= $(shell pwd)/bin $(LOCALBIN): mkdir -p $(LOCALBIN) diff --git a/README.md b/README.md index da5a05e..2bde586 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,12 @@ 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 + +- [Bundle development](docs/bundle.md) +- [Disconnected OpenShift installation with oc-mirror v2](docs/disconnected-install.md) +- [Release artifact validation](docs/release-validation.md) + ## Getting Started ### Prerequisites 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..422fdc8 100644 --- a/bundle.konflux.Dockerfile +++ b/bundle.konflux.Dockerfile @@ -1,5 +1,5 @@ # 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 +# for local dev with operator-sdk), 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 && \ @@ -15,7 +15,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/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 93a768e..72b04dc 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -4,8 +4,7 @@ kind: Kustomization resources: - manager.yaml -# Base image configuration for local development -# For production/Konflux builds, digest-pinned images are set via bundle.konflux.Dockerfile ARG overrides +# Development defaults. Konflux sets release digests in the built bundle CSV. # # Local development override: # make bundle-override-img IMG=quay.io//hyperfleet-operator:dev- @@ -21,13 +20,12 @@ images: # RELATED_IMAGE_HYPERFLEET_API environment variable # Sets the image used for the API operand. # -# IMPORTANT: Must stay in sync with api.DefaultImage in the operator code -# (v0.4.0+: config schema compatibility plus HYPERFLEET-1603 database *_FILE support) +# This is a static plain-manifest default. The API image must support the +# rendered config schema and HYPERFLEET-1603 database *_FILE variables (v0.4.0+). +# Production Konflux bundle builds override it through bundle.konflux.Dockerfile. # # For local development with a custom API image, manually edit the value below # or use yq to update programmatically, then restore before committing. -# -# Production/Konflux: Digest-pinned via bundle.konflux.Dockerfile ARG overrides patches: - patch: |- - op: add diff --git a/config/manifests/bases/hyperfleet-operator.clusterserviceversion.yaml b/config/manifests/bases/hyperfleet-operator.clusterserviceversion.yaml index 8e3a240..57370d3 100644 --- a/config/manifests/bases/hyperfleet-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/hyperfleet-operator.clusterserviceversion.yaml @@ -33,9 +33,6 @@ metadata: } ] capabilities: Basic Install - # Conforma olm.feature_annotations_format requires all seven as string booleans. - # disconnected is true because relatedImages are digest-pinned. fips-compliant - # flips to "true" only after a verified FIPS build. HYPERFLEET-1614. features.operators.openshift.io/disconnected: "true" features.operators.openshift.io/fips-compliant: "false" features.operators.openshift.io/proxy-aware: "false" diff --git a/docs/bundle.md b/docs/bundle.md index 99587ba..3c68bbe 100644 --- a/docs/bundle.md +++ b/docs/bundle.md @@ -1,24 +1,19 @@ -## Pre-merge checks -1. Updates to bundle.Dockerfile are also reflected in bundle.konflux.Dockerfile -2. bundle/ is correctly updated before merging -3. config/manager/kustomization.yaml is not wrongly updated +# Bundle development -## CI Installation +This guide covers local bundle-development workflows. It is not an installation +guide. -Once Konflux is in place, the CI pipeline will automatically handle bundling building with operator image updates: +- For an OpenShift air-gapped installation, follow the + [disconnected installation guide](disconnected-install.md). +- For release artifact and catalog-mirror validation, see + [release validation](release-validation.md). -1. Konflux builds the operator image and publishes it to quay.io/redhat-services-prod/hyperfleet-tenant/hyperfleet/hyperfleet-operator -2. The operator-bundle .tekton pipeline will be triggered by any update to the bundle.konflux.Dockerfile -2. `bundle.konflux.Dockerfile` runs `update_bundle.sh` with the new operator image reference -3. `update_bundle.sh` uses yq to update the CSV to ensure the operator deployment has proper values - image, relatedImages, annotations, etc. -4. Publishes the operator-bundle to quay.io/redhat-services-prod/hyperfleet-tenant/hyperfleet/hyperfleet-operator-bundle - -(HYPERFLEET-1411: TODO add more information once the konflux pipelines are in place) - -## Development Installation +## Development installation ### Prerequisite steps -For local development and installation, set your Quay username to automatically configure image paths: + +For local development and installation, set your Quay username to automatically +configure image paths: ```bash # Set your Quay username (required for image-dev) @@ -37,13 +32,13 @@ export IMG=quay.io/$QUAY_USER/hyperfleet-operator:dev- - IMG (hyperfleet-operator image): `quay.io/$QUAY_USER/hyperfleet-operator:dev-` (defaults `make image-dev`) - BUNDLE_IMG (hyperfleet-operator-bundle): `quay.io/$QUAY_USER/hyperfleet-operator-bundle:v$(VERSION)` (default VERSION=0.0.1) +### OLM installation -### OLM Installation -Testing hyperfleet-operator installation with OLM +Testing hyperfleet-operator installation with OLM. -**Note:** Ensure `IMG` is properly exported before running these commands +**Note:** Ensure `IMG` is properly exported before running these commands. -1. **Update bundle with operator image:** - WARNING restore changes once done testing! +1. **Update bundle with operator image:** WARNING: restore changes once done testing. ```bash make bundle-override-img # Updates bundle/ manifests with the operator image from step 2 @@ -59,48 +54,52 @@ Testing hyperfleet-operator installation with OLM # To override: make bundle-build VERSION=0.0.2 BUNDLE_IMG= ``` -3. **Quick testing on a k8s cluster:** - ```bash - export BUNDLE_IMG=quay.io/$QUAY_USER/hyperfleet-operator-bundle:v$(VERSION) - # Install Operator Lifecycle Manager in your cluster - operator-sdk olm install - - # Install operator from bundle (note: bundle image uses v prefix) - operator-sdk run bundle $(BUNDLE_IMG) -n - - # Cleanup when done - IMPORTANT: Delete CRs before uninstalling operator - # 1. Export and delete the cluster-scoped HyperFleetConfig CR - kubectl get hyperfleetconfig -o yaml > hyperfleetconfig-backup.yaml - kubectl delete hyperfleetconfig --all - - # 2. Clean up operator (removes CRDs and controller) - operator-sdk cleanup hyperfleet-operator -n - - # 3. Uninstall Operator Lifecycle Manager from your cluster - operator-sdk olm uninstall - ``` - - -### Non-OLM Installation -Testing hyperfleet-operator installation without OLM (kubectl apply) - -**Note:** Ensure `IMG` is properly exported before running these commands -1. **Quick testing on a k8s cluster:** - ```bash - export IMG="quay.io/$QUAY_USER/hyperfleet-operator:dev-" - make deploy - # Generates: dist/install.yaml - # Again, make sure to restore config/manager/kustomization.yaml after testing - # Check status to see that everything installed properly - - # Cleanup - IMPORTANT: Delete CRs before uninstalling operator - # 1. Export and delete the cluster-scoped HyperFleetConfig CR - kubectl get hyperfleetconfig -o yaml > hyperfleetconfig-backup.yaml - kubectl delete hyperfleetconfig --all - - # 2. Undeploy operator (removes CRDs and controller) - make undeploy - # Or manually: kubectl delete -f dist/install.yaml - ``` - -**Note:** `bundle-override-img` and `build-deployer-override-img` modify config/manager/kustomization.yaml in place. So before committing any changes make sure to revert these changes. Additionally when running `bundle-override-img` the bundle/ and bundle.Dockerfile get regenerated in place, so make sure to check these changes before committing them. +3. **Quick testing on a Kubernetes cluster:** + ```bash + export BUNDLE_IMG=quay.io/$QUAY_USER/hyperfleet-operator-bundle:v$(VERSION) + # Install Operator Lifecycle Manager in your cluster + operator-sdk olm install + + # Install operator from bundle (note: bundle image uses v prefix) + operator-sdk run bundle $(BUNDLE_IMG) -n + + # Cleanup when done - IMPORTANT: Delete CRs before uninstalling operator + # 1. Export and delete the cluster-scoped HyperFleetConfig CR + kubectl get hyperfleetconfig -o yaml > hyperfleetconfig-backup.yaml + kubectl delete hyperfleetconfig --all + + # 2. Clean up operator (removes CRDs and controller) + operator-sdk cleanup hyperfleet-operator -n + + # 3. Uninstall Operator Lifecycle Manager from your cluster + operator-sdk olm uninstall + ``` + +### Non-OLM installation + +Testing hyperfleet-operator installation without OLM (`kubectl apply`). + +**Note:** Ensure `IMG` is properly exported before running these commands. + +1. **Quick testing on a Kubernetes cluster:** + ```bash + export IMG="quay.io/$QUAY_USER/hyperfleet-operator:dev-" + make deploy + # Generates: dist/install.yaml + # Again, make sure to restore config/manager/kustomization.yaml after testing + # Check status to see that everything installed properly + + # Cleanup - IMPORTANT: Delete CRs before uninstalling operator + # 1. Export and delete the cluster-scoped HyperFleetConfig CR + kubectl get hyperfleetconfig -o yaml > hyperfleetconfig-backup.yaml + kubectl delete hyperfleetconfig --all + + # 2. Undeploy operator (removes CRDs and controller) + make undeploy + # Or manually: kubectl delete -f dist/install.yaml + ``` + +**Note:** `bundle-override-img` and `build-deployer-override-img` modify +`config/manager/kustomization.yaml` in place. Before committing, revert those +changes. `bundle-override-img` also regenerates `bundle/` and +`bundle.Dockerfile`; inspect and restore those generated changes as needed. diff --git a/docs/disconnected-install.md b/docs/disconnected-install.md new file mode 100644 index 0000000..119460c --- /dev/null +++ b/docs/disconnected-install.md @@ -0,0 +1,186 @@ +# Disconnected installation from a release catalog + +This workflow starts with a **published HyperFleet operator catalog**. The +catalog selects the bundles; their release image metadata determines the +operator and operand images to mirror. The CSV in this repository is a +development/build template, not the published release manifest. + +Catalog publication is a prerequisite owned by the release 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. + +Release maintainers must publish and validate the catalog before installers use +this procedure. Release-artifact checks and archive-transfer simulation are +covered separately in [release validation](release-validation.md). + +## 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. Start with 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 dependency discovery. Do not use +`skipDependencies` or blocked-image filters to suppress required release content. + +## 2. Export and transfer + +```bash +oc-mirror --v2 --config "$MIRROR_ROOT/imageset-config.yaml" \ + file://"$MIRROR_ROOT/archive" +tar -C "$MIRROR_ROOT" -czf /media/transfer/hyperfleet-mirror.tgz \ + archive imageset-config.yaml +``` + +Transfer the complete archive and configuration. Source credentials are not +part of the transfer artifact. Keep the 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)" +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 --config "$IMPORT_ROOT/imageset-config.yaml" \ + --from file://"$IMPORT_ROOT/archive" docker://"$DESTINATION" +``` + +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/docs/release-validation.md b/docs/release-validation.md new file mode 100644 index 0000000..761f364 --- /dev/null +++ b/docs/release-validation.md @@ -0,0 +1,85 @@ +# Release artifact validation + +This guide is for release maintainers validating a published HyperFleet operator +bundle and catalog. It is not part of the disconnected installation procedure. +Installers start with the published catalog and follow the +[disconnected installation guide](disconnected-install.md). + +## Release prerequisites + +Before validating artifacts, obtain from the catalog publisher: + +- the digest-pinned catalog pullspec, package, and channel; +- the digest-pinned bundle selected by that catalog; and +- the publisher-supported OpenShift and `oc-mirror` versions. + +Catalog publication is owned by the release workflow. This repository does not +create a production catalog, channel history, or upgrade graph. + +## Validate the published bundle CSV + +Extract the CSV from the exact bundle selected by the published catalog and +validate its image metadata: + +```bash +export BUNDLE_IMAGE='registry.example.com/hyperfleet/operator-bundle@sha256:' +RELEASE_CHECK="$(mktemp -d)" +BUNDLE_CONTAINER="$(docker create --entrypoint /bin/true "$BUNDLE_IMAGE")" +docker cp "$BUNDLE_CONTAINER:/manifests/hyperfleet-operator.clusterserviceversion.yaml" "$RELEASE_CHECK/release.csv" +docker rm "$BUNDLE_CONTAINER" +make verify-related-images CSV_FILE="$RELEASE_CHECK/release.csv" +``` + +The verifier checks digest references and agreement between the release image +sources and `spec.relatedImages`. It does not test catalog membership, registry +access, OLM installation, or operand readiness. + +For pull-request validation, `make lint` also runs `make verify-release-images`. +That target transforms a temporary copy of the repository bundle CSV using the +Konflux Dockerfile defaults, validates the result, and runs verifier regression +tests. It does not modify generated source files. Validate the CSV extracted +from the actual published bundle separately, especially when release build +arguments differ from their defaults. + +## Simulate archive transfer + +This optional test exercises `oc-mirror` v2 mirror-to-disk and disk-to-mirror +against an internal, disposable registry. It validates that catalog-selected +content can be imported without the test import phase reaching the public +network. It does not install OLM, unpack a bundle on a cluster, or start the +HyperFleet API. + +First complete [Validate the published bundle CSV](#validate-the-published-bundle-csv) +to create `"$RELEASE_CHECK/release.csv"`. Then prepare all helper images and +release artifacts while connected: + +```bash +export RELEASE_CSV="$RELEASE_CHECK/release.csv" +export VALIDATION_ROOT="$(mktemp -d)" +cp docs/examples/imageset-config-catalog.yaml "$VALIDATION_ROOT/imageset-config.yaml" +# Replace the catalog placeholder with the published catalog digest. +export IMAGE_SET_CONFIG="$VALIDATION_ROOT/imageset-config.yaml" +export REGISTRY_IMAGE='docker.io/library/registry@sha256:a3d8aaa63ed8681a604f1dea0aa03f100d5895b6a58ace528858a7b332415373' +export REGISTRY_AUTH_FILE='/path/to/source-auth.json' # required for a private source registry + +docker pull "$BUNDLE_IMAGE" +docker pull "$REGISTRY_IMAGE" +make build-oc-mirror-image CONTAINER_TOOL=docker +IMAGE_SET_CONFIG="$IMAGE_SET_CONFIG" RELEASE_CSV="$RELEASE_CSV" \ + BUNDLE_IMAGE="$BUNDLE_IMAGE" make test-disconnected-mirror CONTAINER_TOOL=docker +``` + +The ImageSetConfiguration must select the published catalog through +`mirror.operators`; do not supply a hand-maintained `additionalImages` list. +The test requires a containers-style auth file with inline credentials when the +source registry is private. Do not commit or transfer that auth file. + +If Docker's embedded resolver cannot resolve the connected source registry, set +`CONTAINER_DNS` for the connected export container, for example +`CONTAINER_DNS=1.1.1.1`. The isolated import container retains Docker DNS so it +can resolve the disposable registry. + +The test checks the catalog, selected bundle, and digest-pinned runtime images +from the supplied release CSV against generated IDMS mappings and the imported +registry. The preserved workspace may contain logs and source credentials; if +using `KEEP_WORKSPACE=true`, handle it as sensitive release evidence. diff --git a/hack/bundle/update_bundle.sh b/hack/bundle/update_bundle.sh new file mode 100755 index 0000000..f075db5 --- /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 -e ' + [.spec.install.spec.deployments[].spec.template.spec.containers[] + | select(.name == "manager")] | length == 1 +' "${CSV_FILE}" >/dev/null +if ! "${YQ}" eval -p 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..5e303ed --- /dev/null +++ b/hack/test-disconnected-mirror.sh @@ -0,0 +1,210 @@ +#!/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}" +REGISTRY_IMAGE="${REGISTRY_IMAGE:-docker.io/library/registry@sha256:a3d8aaa63ed8681a604f1dea0aa03f100d5895b6a58ace528858a7b332415373}" +YQ="${YQ:-yq}" +KEEP_WORKSPACE="${KEEP_WORKSPACE:-false}" +CONTAINER_DNS="${CONTAINER_DNS:-}" + +fail() { echo "error: $*" >&2; exit 1; } + +# destination_for resolves a digest image through the generated IDMS. oc-mirror +# v2 does not always write mapping.txt, so IDMS is the authoritative mapping +# artifact produced by disk-to-mirror. +destination_for() { + local image="$1" + local repository="${image%@sha256:*}" + local digest="@sha256:${image##*@sha256:}" + local source mirror + local 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_TOOL" run --rm --dns "$CONTAINER_DNS" "$@" + else + "$CONTAINER_TOOL" run --rm "$@" + fi +} + +for tool in "$CONTAINER_TOOL" "$YQ"; do + command -v "$tool" >/dev/null 2>&1 || fail "required command: $tool" +done +[[ -f "${IMAGE_SET_CONFIG:-}" ]] || fail "set IMAGE_SET_CONFIG to the catalog ImageSetConfiguration" +[[ -f "${RELEASE_CSV:-}" ]] || fail "set RELEASE_CSV to the final release CSV extracted from the selected bundle" +[[ "${BUNDLE_IMAGE:-}" =~ ^[^[:space:]]+@sha256:[0-9a-f]{64}$ ]] || + fail "set BUNDLE_IMAGE to the selected digest-pinned bundle image" +# Old image overrides must not silently give users a false expectation. +[[ -z "${OPERATOR_IMAGE:-}${API_IMAGE:-}${CATALOG_IMAGE:-}" ]] || + fail "derive operand images from RELEASE_CSV; individual image overrides are not supported" + +"$YQ" eval -e ' + (.apiVersion == "mirror.openshift.io/v2alpha1") and + (.kind == "ImageSetConfiguration") and + ((.mirror.operators | length) == 1) and + ((.mirror.additionalImages // [] | length) == 0) +' "$IMAGE_SET_CONFIG" >/dev/null || fail "expected one operator catalog and no additionalImages" +catalog="$("$YQ" eval '.mirror.operators[0].catalog' "$IMAGE_SET_CONFIG")" +[[ "$catalog" =~ ^[^[:space:]]+@sha256:[0-9a-f]{64}$ ]] || + fail "pin the source catalog by digest" + +# These references come from the selected immutable release artifacts: the +# catalog, the bundle it selects, and the final CSV that OLM deploys. This is +# deliberately not a separately maintained image inventory. +release_images=() +while IFS= read -r image; do + [[ -n "$image" ]] && release_images+=("$image") +done < <("$YQ" eval -p yaml -o yaml -r ' + .spec.install.spec.deployments[].spec.template.spec.containers[] + | select(.name == "manager") + | (.image, (.env[] | select(.name | test("^RELATED_IMAGE_")) | .value)) +' "$RELEASE_CSV") +(( ${#release_images[@]} > 0 )) || fail "RELEASE_CSV has no manager deployment images" +for image in "${release_images[@]}"; do + [[ "$image" =~ ^[^[:space:]]+@sha256:[0-9a-f]{64}$ ]] || + fail "RELEASE_CSV contains a non-digest runtime image: $image" +done +expected_images=("$BUNDLE_IMAGE" "${release_images[@]}") +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="" +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 "$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 +cp "$IMAGE_SET_CONFIG" "$connected/imageset-config.yaml" +mirror_args=(--v2 --config imageset-config.yaml file://archive) +if [[ -n "${REGISTRY_AUTH_FILE:-}" ]]; then + [[ -f "$REGISTRY_AUTH_FILE" ]] || fail "REGISTRY_AUTH_FILE does not exist" + 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.json 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_TOOL" 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_TOOL" 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_TOOL" 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_TOOL" 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 +# Skopeo 1.22.2 in the pinned runner reports an isolated Docker network as a +# registry ping followed by a DNS/network dial failure. Do not accept unrelated +# errors, such as an absent image or a failed container startup. +[[ "$isolation_error" =~ pinging\ container\ registry\ registry-1\.docker\.io:.*https://registry-1\.docker\.io/v2/.*dial\ tcp ]] || + fail "unexpected Docker Hub isolation error: $isolation_error" + +"$CONTAINER_TOOL" 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" + "$CONTAINER_TOOL" run --rm --network "$network" \ + --entrypoint /usr/bin/skopeo "$OC_MIRROR_IMAGE" \ + inspect --tls-verify=false "docker://$target" >/dev/null +done +for image in "${expected_images[@]}"; do + destination="$(destination_for "$image")" || + fail "generated IDMS does not cover required image: $image" + "$CONTAINER_TOOL" run --rm --network "$network" \ + --entrypoint /usr/bin/skopeo "$OC_MIRROR_IMAGE" \ + inspect --tls-verify=false "docker://$destination" >/dev/null +done +echo "catalog, bundle, and release runtime images are present after archive import" +echo "OpenShift installation and operand readiness have NOT been tested" diff --git a/hack/test-release-images.sh b/hack/test-release-images.sh new file mode 100644 index 0000000..6f7f496 --- /dev/null +++ b/hack/test-release-images.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Negative checks run through the real release 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-release-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" +} + +expect_failure() { + if bash "$root/hack/verify-release-images.sh" "$workspace" >"$workspace/result.log" 2>&1; then + echo "release gate unexpectedly accepted: $1" >&2 + exit 1 + fi + echo "release gate rejected: $1" +} + +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" + +reset_fixture +# Simulate a regression in release packaging that drops API from its output. +printf '\n"$YQ" eval -i '\''del(.spec.relatedImages[] | select(.name == "hyperfleet-api"))'\'' "$CSV_FILE"\n' \ + >>"$workspace/hack/bundle/update_bundle.sh" +expect_failure "API missing from the transformed release CSV" + +reset_fixture +sed 's|^ARG HYPERFLEET_API_IMAGE_PULLSPEC=.*|ARG HYPERFLEET_API_IMAGE_PULLSPEC="example.com/api:latest"|' \ + "$root/bundle.konflux.Dockerfile" >"$workspace/bundle.konflux.Dockerfile" +expect_failure "mutable release image argument" diff --git a/hack/verify-related-images/integration_test.go b/hack/verify-related-images/integration_test.go new file mode 100644 index 0000000..b2624d3 --- /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 release patching entrypoint with versions 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("release 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("release 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(), "release.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..125cb41 --- /dev/null +++ b/hack/verify-related-images/main.go @@ -0,0 +1,198 @@ +// 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 release CSV. +// It deliberately does not compare release versions to development manifests. +package main + +import ( + "errors" + "flag" + "fmt" + "os" + "regexp" + "slices" + "strings" + + "github.com/openshift-hyperfleet/hyperfleet-operator/internal/component/api" + "sigs.k8s.io/yaml" +) + +const relatedImagePrefix = "RELATED_IMAGE_" + +var digestPullspec = regexp.MustCompile(`^\S+@sha256:[0-9a-f]{64}$`) + +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 release bundle") + flag.Parse() + if *csvPath == "" { + fmt.Fprintln(os.Stderr, + "provide -csv pointing to the final release CSV; development manifests are not release 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("release 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 release 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 !digestPullspec.MatchString(image.Image) { + problems = append(problems, fmt.Sprintf("mutable or malformed %s image %q: %q", label, image.Name, image.Image)) + } + } + return result, problems +} + +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..2106dc1 --- /dev/null +++ b/hack/verify-related-images/main_test.go @@ -0,0 +1,92 @@ +// 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 TestVerifyReleaseCSV(t *testing.T) { + tests := []struct { + name string + mutate func(string) string + want string + }{ + {"valid release", func(s string) string { return s }, ""}, + {"different release", 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 release", func(s string) string { + return strings.ReplaceAll(s, apiImage, "registry.example.com/api:latest") + }, "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/hack/verify-release-images.sh b/hack/verify-release-images.sh new file mode 100644 index 0000000..df6ead2 --- /dev/null +++ b/hack/verify-release-images.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Check the same CSV transformation used by the release 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-release-check.XXXXXX")" +trap 'rm -rf "$workspace"' EXIT +cp "$source_root/bundle/manifests/hyperfleet-operator.clusterserviceversion.yaml" "$workspace/release.yaml" +export CSV_FILE="$workspace/release.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/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=