From e8576a49279470c2060b83b5d84c83a485067f98 Mon Sep 17 00:00:00 2001 From: Miguel Angel Ajo Pelayo Date: Thu, 26 Mar 2026 15:51:34 +0100 Subject: [PATCH 1/4] fix(operator): fix kustomize image substitution and CSV containerImage Use IMAGE_TAG_BASE instead of 'controller' in kustomize edit set image so the image name actually matches what manager.yaml declares. Fix the kustomization.yaml image entry name to match the real image reference. Add a post-generation step in the bundle target to align the CSV containerImage annotation with the actual IMG value. Made-with: Cursor (cherry picked from commit 93230f9d1e8bb9f971c88547ad8b75e9ff8cb296) --- controller/deploy/operator/Makefile | 10 ++++++---- .../deploy/operator/config/manager/kustomization.yaml | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/controller/deploy/operator/Makefile b/controller/deploy/operator/Makefile index 15f337e72..b92d4614d 100644 --- a/controller/deploy/operator/Makefile +++ b/controller/deploy/operator/Makefile @@ -62,7 +62,7 @@ endif # This is useful for CI or a project to utilize a specific version of the operator-sdk toolkit. OPERATOR_SDK_VERSION ?= v1.41.1 # Image URL to use all building/pushing image targets -IMG ?= quay.io/jumpstarter-dev/jumpstarter-operator:latest +IMG ?= $(IMAGE_TAG_BASE):$(VERSION) # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) @@ -201,7 +201,7 @@ docker-buildx: ## Build and push docker image for the manager for cross-platform .PHONY: build-installer build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. mkdir -p dist - cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + cd config/manager && $(KUSTOMIZE) edit set image $(IMAGE_TAG_BASE)=${IMG} $(KUSTOMIZE) build config/default > dist/install.yaml ##@ Deployment @@ -220,7 +220,7 @@ uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified .PHONY: deploy deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. - cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + cd config/manager && $(KUSTOMIZE) edit set image $(IMAGE_TAG_BASE)=${IMG} $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - .PHONY: undeploy @@ -315,8 +315,10 @@ endif .PHONY: bundle bundle: manifests kustomize operator-sdk ## Generate bundle manifests and metadata, then validate generated files. $(OPERATOR_SDK) generate kustomize manifests -q - cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) + cd config/manager && $(KUSTOMIZE) edit set image $(IMAGE_TAG_BASE)=$(IMG) $(KUSTOMIZE) build config/manifests | $(OPERATOR_SDK) generate bundle $(BUNDLE_GEN_FLAGS) + @# Align CSV containerImage annotation with the manager image (base CSV still references :latest) + @perl -pi -e 's|quay.io/jumpstarter-dev/jumpstarter-operator:latest|$(IMG)|g' bundle/manifests/jumpstarter-operator.clusterserviceversion.yaml @# Inject Red Hat OpenShift version range into bundle metadata (not handled by operator-sdk) @if ! grep -q 'com.redhat.openshift.versions' bundle/metadata/annotations.yaml; then \ echo ' com.redhat.openshift.versions: $(OPENSHIFT_VERSIONS)' >> bundle/metadata/annotations.yaml; \ diff --git a/controller/deploy/operator/config/manager/kustomization.yaml b/controller/deploy/operator/config/manager/kustomization.yaml index 9c94df038..81cfe22fa 100644 --- a/controller/deploy/operator/config/manager/kustomization.yaml +++ b/controller/deploy/operator/config/manager/kustomization.yaml @@ -3,6 +3,6 @@ resources: apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: -- name: controller +- name: quay.io/jumpstarter-dev/jumpstarter-operator newName: quay.io/jumpstarter-dev/jumpstarter-operator newTag: latest From e6b85225975afb7db94bbbf90f444669e9c8a8a0 Mon Sep 17 00:00:00 2001 From: Miguel Angel Ajo Pelayo Date: Thu, 26 Mar 2026 16:51:15 +0100 Subject: [PATCH 2/4] operator-release: agent instructions (cherry picked from commit fcb1ac7b4fd937be6c42df5692dc8f311ec3ef2e) --- .claude/rules/releasing-operator.md | 1 + .cursor/rules/releasing-operator.mdc | 133 +++++++++++++++++++++++++++ CLAUDE.md | 3 + 3 files changed, 137 insertions(+) create mode 120000 .claude/rules/releasing-operator.md create mode 100644 .cursor/rules/releasing-operator.mdc diff --git a/.claude/rules/releasing-operator.md b/.claude/rules/releasing-operator.md new file mode 120000 index 000000000..470a8f9c6 --- /dev/null +++ b/.claude/rules/releasing-operator.md @@ -0,0 +1 @@ +../../.cursor/rules/releasing-operator.mdc \ No newline at end of file diff --git a/.cursor/rules/releasing-operator.mdc b/.cursor/rules/releasing-operator.mdc new file mode 100644 index 000000000..6b224bed2 --- /dev/null +++ b/.cursor/rules/releasing-operator.mdc @@ -0,0 +1,133 @@ +--- +description: when the user is releasing a new version of the Kubernetes operator +alwaysApply: false +--- +# Releasing the Jumpstarter Operator + +This guide covers preparing and publishing a new operator release to OLM and community-operators repos. + +## Version Source of Truth + +The operator bundle version is driven by variables in `controller/deploy/operator/Makefile`: + +- **`VERSION`**: The semver for the bundle (e.g. `0.8.1`). This is NOT prefixed with `v`. +- **`REPLACES`**: The previous OLM bundle this release upgrades from (e.g. `jumpstarter-operator.v0.8.0`). Note the OLM name uses a `v` prefix per OLM convention, but image tags do NOT. +- **`IMG`**: Defaults to `$(IMAGE_TAG_BASE):$(VERSION)`, so all image references are automatically derived from `VERSION`. No need to pass `IMG` explicitly in most cases. +- **`FBC_CHANNELS`**: Catalog channel (default `alpha`). +- **`OPENSHIFT_VERSIONS`**: Red Hat OpenShift version range (e.g. `v4.18-v4.21`). +- **`IMAGE_TAG_BASE`**: Registry prefix (`quay.io/jumpstarter-dev/jumpstarter-operator`). +- **`BUNDLE_IMG`** / **`CATALOG_IMG`**: Also derived from `IMAGE_TAG_BASE` and `VERSION` automatically. + +The `config/manifests/bases/jumpstarter-operator.clusterserviceversion.yaml` uses placeholder version `0.0.0` -- the real version is injected by `make bundle`. + +## Image Tag Convention + +This project tags container images **without** a `v` prefix: `:0.8.1`, NOT `:v0.8.1`. Since `IMG` defaults to `$(IMAGE_TAG_BASE):$(VERSION)`, running `make bundle` or `make contribute` produces correct image references automatically. You can still override for custom builds: + +``` +make bundle IMG=quay.io/jumpstarter-dev/jumpstarter-operator:custom-tag +``` + +## Files to Update When Bumping a Version + +When preparing a new release version X.Y.Z, update these files together: + +### 1. `controller/deploy/operator/Makefile` +- `VERSION ?= X.Y.Z` +- `REPLACES ?= jumpstarter-operator.vPREVIOUS` (previous released version) +- Optionally update `OPENSHIFT_VERSIONS` if the supported range changes + +### 2. `controller/deploy/operator/api/v1alpha1/jumpstarter_types.go` +Update the kubebuilder default image tags for both controller and router: +```go +// In RoutersConfig: +// +kubebuilder:default="quay.io/jumpstarter-dev/jumpstarter-controller:X.Y.Z" +Image string `json:"image,omitempty"` + +// In ControllerConfig: +// +kubebuilder:default="quay.io/jumpstarter-dev/jumpstarter-controller:X.Y.Z" +Image string `json:"image,omitempty"` +``` +These defaults flow into the CRD YAML via `make manifests` and into the OLM bundle via `make bundle`. + +### 3. `controller/deploy/operator/test/e2e/e2e_test.go` +Update the default controller image constant: +```go +const defaultControllerImage = "quay.io/jumpstarter-dev/jumpstarter-controller:X.Y.Z" +``` + +### 4. `controller/deploy/operator/config/manager/kustomization.yaml` +The `newTag` field is updated automatically by `make bundle` (via `kustomize edit set image`), but verify it reflects the correct tag after running the bundle target. + +## Release Steps + +### Step 1: Update version references +Edit the files listed above with the new version. Run: +```bash +cd controller/deploy/operator +make manifests generate +``` +This regenerates `config/crd/bases/operator.jumpstarter.dev_jumpstarters.yaml` with updated defaults. + +### Step 2: Regenerate the OLM bundle +```bash +cd controller/deploy/operator +make bundle +``` +`IMG` automatically resolves to `$(IMAGE_TAG_BASE):$(VERSION)`. This: +1. Runs `make manifests` and `kustomize edit set image` with the correct image +2. Generates `bundle/manifests/`, `bundle/metadata/`, `bundle.Dockerfile` +3. Replaces `:latest` with the actual `IMG` in the CSV `containerImage` annotation +4. Injects `com.redhat.openshift.versions` into bundle metadata +5. Validates the bundle with `operator-sdk bundle validate` +6. Generates `bundle/release-config.yaml` with `replaces`, `skipRange`, and channel info + +### Step 3: Verify the bundle +Check that all image references are correct (no `:latest`, no `:vX.Y.Z`): +```bash +grep -E "containerImage|image: quay" bundle/manifests/jumpstarter-operator.clusterserviceversion.yaml +grep "default: quay" bundle/manifests/operator.jumpstarter.dev_jumpstarters.yaml +cat bundle/release-config.yaml +``` + +### Step 4: Commit +Create a commit with the Makefile, types, CRD, kustomization, and e2e changes. The `bundle/` directory is NOT committed to the repo (it was removed in commit `12b680e3`). + +### Step 5: Contribute to community-operators (when ready) +```bash +cd controller/deploy/operator +# if it's a different github user: export GITHUB_USER=yourusername # defaults to mangelajo +make contribute +``` +This runs `make bundle` then `contribute/update-contributions.sh`, which: +1. Shows a confirmation prompt with version, channels, replaces, and skipRange +2. Clones/updates `community-operators` and `community-operators-prod` repos +3. Creates branch `jumpstarter-operator-release-X.Y.Z` +4. Copies `bundle/` into `operators/jumpstarter-operator/X.Y.Z/` and commits + +After the script completes, push to your fork and open PRs: +```bash +cd contribute/community-operators && git push -f user jumpstarter-operator-release-X.Y.Z +cd ../community-operators-prod && git push -f user jumpstarter-operator-release-X.Y.Z +``` + +## Cherry-Picking Fixes to Main + +When a release includes Makefile infrastructure fixes (not version-specific), split them into a separate commit so they can be cherry-picked to `main` independently from version bumps. + +## How `make bundle` Works Internally + +``` +Makefile (VERSION, REPLACES, IMG=$(IMAGE_TAG_BASE):$(VERSION)) + -> operator-sdk generate kustomize manifests + -> kustomize edit set image IMAGE_TAG_BASE=IMG + -> kustomize build config/manifests | operator-sdk generate bundle + -> perl replaces :latest with IMG in CSV containerImage annotation + -> inject openshift.versions into annotations.yaml + -> operator-sdk bundle validate + -> generate release-config.yaml (replaces, skipRange, channels) +``` + +The `config/manager/kustomization.yaml` image entry must use `name: quay.io/jumpstarter-dev/jumpstarter-operator` (the real image name from `manager.yaml`), NOT `name: controller` (which was the original scaffolding default and does not match). + +The CSV base template in `config/manifests/bases/` still contains `containerImage: ...latest`. The `bundle` target's perl post-processing step replaces this with the actual `$(IMG)` value after `operator-sdk generate bundle` runs. diff --git a/CLAUDE.md b/CLAUDE.md index 891995470..ade44965e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,10 +10,13 @@ Important project-specific rules and guidelines are located in the `.claude/rule - **`.claude/rules/creating-new-drivers.md`**: Guidelines for creating new driver packages, including naming conventions, required information, and the driver creation process. Read this when tasked with creating or modifying drivers. +- **`.claude/rules/releasing-operator.md`**: Step-by-step guide for releasing a new version of the Jumpstarter operator, including which files to update, image tag conventions, bundle generation, and community-operators contribution workflow. + ## When to Read These Rules - **Always**: Read `project-structure.md` when working with files, packages, or understanding the codebase layout - **When creating drivers**: Read `creating-new-drivers.md` before creating, improving, or documenting driver packages +- **When releasing the operator**: Read `releasing-operator.md` before preparing a new operator version for OLM - **When modifying structure**: Consult both files when making changes that affect project organization ## Key Information From 055aa9bb102e91ceda3936af08e9dc6e11dd7028 Mon Sep 17 00:00:00 2001 From: Miguel Angel Ajo Pelayo Date: Thu, 26 Mar 2026 17:20:27 +0100 Subject: [PATCH 3/4] fix: operator-img reference in e2e (cherry picked from commit 7f5cc6dbb204ac0348400879d664e0b603c0c235) --- controller/deploy/operator/Makefile | 4 ++++ controller/hack/deploy_vars | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/controller/deploy/operator/Makefile b/controller/deploy/operator/Makefile index b92d4614d..2247f1cbc 100644 --- a/controller/deploy/operator/Makefile +++ b/controller/deploy/operator/Makefile @@ -95,6 +95,10 @@ SHELL = /usr/bin/env bash -o pipefail .PHONY: all all: build +.PHONY: print-img +print-img: + @echo $(IMG) + ##@ General # The help target prints out all targets with their descriptions organized diff --git a/controller/hack/deploy_vars b/controller/hack/deploy_vars index b61c9f159..4cc3c15ae 100755 --- a/controller/hack/deploy_vars +++ b/controller/hack/deploy_vars @@ -6,7 +6,7 @@ IP=${IP:-$(get_external_ip)} BASEDOMAIN=${BASEDOMAIN:-"jumpstarter.${IP}.nip.io"} IMG=${IMG:-quay.io/jumpstarter-dev/jumpstarter-controller:latest} -OPERATOR_IMG=${OPERATOR_IMG:-quay.io/jumpstarter-dev/jumpstarter-operator:latest} +OPERATOR_IMG=${OPERATOR_IMG:-$(make -C deploy/operator --no-print-directory -s print-img 2>/dev/null || echo "quay.io/jumpstarter-dev/jumpstarter-operator:latest")} # Determine endpoints based on NETWORKING_MODE and CLUSTER_TYPE if [ "${NETWORKING_MODE}" == "ingress" ]; then From 9f698ef8b2d6d2b58caf3fffcb2b12453ae08b6c Mon Sep 17 00:00:00 2001 From: Miguel Angel Ajo Pelayo Date: Thu, 26 Mar 2026 19:41:49 +0100 Subject: [PATCH 4/4] docs: document OLM upgrade graph and REPLACES rules for RC releases Add guidance on ensuring REPLACES points to the most recently published version in the channel (including release candidates) to avoid multiple channel heads in the OLM graph. Made-with: Cursor (cherry picked from commit 59d6d3b8f87ccd35787ba76bd859e11d2a0a57f6) --- .cursor/rules/releasing-operator.mdc | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.cursor/rules/releasing-operator.mdc b/.cursor/rules/releasing-operator.mdc index 6b224bed2..7ce00c072 100644 --- a/.cursor/rules/releasing-operator.mdc +++ b/.cursor/rules/releasing-operator.mdc @@ -11,7 +11,7 @@ This guide covers preparing and publishing a new operator release to OLM and com The operator bundle version is driven by variables in `controller/deploy/operator/Makefile`: - **`VERSION`**: The semver for the bundle (e.g. `0.8.1`). This is NOT prefixed with `v`. -- **`REPLACES`**: The previous OLM bundle this release upgrades from (e.g. `jumpstarter-operator.v0.8.0`). Note the OLM name uses a `v` prefix per OLM convention, but image tags do NOT. +- **`REPLACES`**: The previous OLM bundle this release upgrades from (e.g. `jumpstarter-operator.v0.8.0`). Note the OLM name uses a `v` prefix per OLM convention, but image tags do NOT. **Important**: This must point to the _most recent_ published version in the channel, including release candidates. For example, if `v0.8.1-rc.1` was published, then `v0.8.1` must replace `v0.8.1-rc.1` (not `v0.8.0`), otherwise OLM will report "multiple channel heads". - **`IMG`**: Defaults to `$(IMAGE_TAG_BASE):$(VERSION)`, so all image references are automatically derived from `VERSION`. No need to pass `IMG` explicitly in most cases. - **`FBC_CHANNELS`**: Catalog channel (default `alpha`). - **`OPENSHIFT_VERSIONS`**: Red Hat OpenShift version range (e.g. `v4.18-v4.21`). @@ -34,7 +34,7 @@ When preparing a new release version X.Y.Z, update these files together: ### 1. `controller/deploy/operator/Makefile` - `VERSION ?= X.Y.Z` -- `REPLACES ?= jumpstarter-operator.vPREVIOUS` (previous released version) +- `REPLACES ?= jumpstarter-operator.vPREVIOUS` (the most recent version published to the OLM channel -- including RCs. E.g. if `v0.8.1-rc.1` was published, REPLACES must be `jumpstarter-operator.v0.8.1-rc.1`, not `v0.8.0`) - Optionally update `OPENSHIFT_VERSIONS` if the supported range changes ### 2. `controller/deploy/operator/api/v1alpha1/jumpstarter_types.go` @@ -111,6 +111,22 @@ cd contribute/community-operators && git push -f user jumpstarter-operator-relea cd ../community-operators-prod && git push -f user jumpstarter-operator-release-X.Y.Z ``` +## OLM Upgrade Graph and `REPLACES` + +OLM requires a **single channel head** (latest version) per channel. The `replaces` field creates a directed upgrade edge in the graph. If two bundles both replace the same version, OLM reports: + +``` +multiple channel heads found in graph: jumpstarter-operator.vA, jumpstarter-operator.vB +``` + +**Rule**: `REPLACES` must always point to the _most recently published_ version in the channel, forming a linear chain. When release candidates are published to the channel before the final release, the chain must include them: + +``` +v0.8.0 → v0.8.1-rc.1 → v0.8.1 +``` + +The `skipRange` (e.g. `>=0.8.0 <0.8.1`) allows users on any intermediate version to upgrade directly, but `replaces` must still form a valid graph with a single head. + ## Cherry-Picking Fixes to Main When a release includes Makefile infrastructure fixes (not version-specific), split them into a separate commit so they can be cherry-picked to `main` independently from version bumps.