diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..c59753b --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,61 @@ +name: CD + +on: + push: + branches: + - master + +jobs: + cd: + name: Build and Publish + runs-on: ubuntu-latest + steps: + - name: Clone the code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Login to Quay.io + run: | + mkdir -p $HOME/.docker + echo "${{ secrets.QUAY_TOKEN }}" | base64 -d > $HOME/.docker/config.json + + - name: Build and push operator image + run: | + SHA=$(git rev-parse --short HEAD) + make container-build IMG=quay.io/codeready-toolchain/claw-operator:${SHA} + make container-push IMG=quay.io/codeready-toolchain/claw-operator:${SHA} + + - name: Build and push proxy image + run: | + SHA=$(git rev-parse --short HEAD) + make container-build-proxy PROXY_IMG=quay.io/codeready-toolchain/claw-proxy:${SHA} + make container-push-proxy PROXY_IMG=quay.io/codeready-toolchain/claw-proxy:${SHA} + + - name: Publish staging bundle and catalog + run: make push-to-quay-staging + + - name: Create Kind cluster for scorecard + continue-on-error: true + id: kind + run: | + make kind + bin/kind create cluster --name scorecard + + - name: Run scorecard tests + if: steps.kind.outcome == 'success' + continue-on-error: true + run: | + bin/operator-sdk scorecard ./bundle \ + --kubeconfig=$HOME/.kube/config \ + --wait-time=120s \ + --output=text || echo "::warning::Scorecard tests failed (non-blocking)" + + - name: Cleanup Kind cluster + if: always() && steps.kind.outcome == 'success' + run: bin/kind delete cluster --name scorecard diff --git a/.github/workflows/lint-bundle.yml b/.github/workflows/lint-bundle.yml new file mode 100644 index 0000000..2fb5d13 --- /dev/null +++ b/.github/workflows/lint-bundle.yml @@ -0,0 +1,28 @@ +name: Lint Bundle + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + lint-bundle: + name: Bundle Validation + runs-on: ubuntu-latest + steps: + - name: Clone the code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Generate bundle + run: make bundle + + - name: Validate bundle + run: bin/operator-sdk bundle validate ./bundle diff --git a/.gitignore b/.gitignore index 2c2956a..39fdbd1 100644 --- a/.gitignore +++ b/.gitignore @@ -28,7 +28,14 @@ go.work *~ tmp -# Generated deploy overlay (created by make deploy/build-installer) +# Generated overlays (created by make deploy/bundle) config/.deploy/ +config/.bundle/ + +# Generated OLM bundle (created by make bundle) +bundle/ + +# Ephemeral catalog directory (created by CD pipeline) +catalog/ .claude/settings.local.json \ No newline at end of file diff --git a/Makefile b/Makefile index 3cf4231..67fec10 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,21 @@ # Image URL to use all building/pushing image targets IMG ?= claw-operator:latest PROXY_IMG ?= claw-proxy:latest +KUBECTL_IMG ?= quay.io/openshift/origin-cli:4.21 +BUNDLE_IMG ?= claw-operator-bundle:v$(VERSION) +CATALOG_IMG ?= claw-operator-catalog:latest PLATFORM ?= linux/amd64 +# OLM bundle configuration +VERSION ?= 0.0.0 +CHANNELS ?= staging +DEFAULT_CHANNEL ?= staging +BUNDLE_METADATA_OPTS ?= --channels=$(CHANNELS) --default-channel=$(DEFAULT_CHANNEL) + +# OS/Arch for downloading binary tools +OS = $(shell go env GOOS) +ARCH = $(shell go env GOARCH) + # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) GOBIN=$(shell go env GOPATH)/bin @@ -67,11 +80,7 @@ test: manifests generate fmt vet setup-envtest ## Run tests. KIND_CLUSTER ?= claw-operator-test-e2e .PHONY: setup-test-e2e -setup-test-e2e: ## Set up a Kind cluster for e2e tests if it does not exist - @command -v $(KIND) >/dev/null 2>&1 || { \ - echo "Kind is not installed. Please install Kind manually."; \ - exit 1; \ - } +setup-test-e2e: kind ## Set up a Kind cluster for e2e tests if it does not exist @case "$$($(KIND) get clusters)" in \ *"$(KIND_CLUSTER)"*) \ echo "Kind cluster '$(KIND_CLUSTER)' already exists. Skipping creation." ;; \ @@ -170,10 +179,20 @@ container-push-proxy: ## Push container image for the credential proxy. # pull-policy defaults to IfNotPresent; dev-deploy passes Always to force re-pulls. define generate-deploy-overlay @rm -rf config/.deploy && mkdir -p config/.deploy - @printf 'apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nresources:\n- ../default\nimages:\n- name: controller\n newName: $(shell echo $(1) | cut -d: -f1)\n newTag: $(shell echo $(1) | cut -d: -f2)\npatches:\n- path: proxy_image_patch.yaml\n target:\n kind: Deployment\n' > config/.deploy/kustomization.yaml + @img=$(1); printf 'apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nresources:\n- ../default\nimages:\n- name: controller\n newName: %s\n newTag: %s\npatches:\n- path: proxy_image_patch.yaml\n target:\n kind: Deployment\n' "$${img%:*}" "$${img##*:}" > config/.deploy/kustomization.yaml @printf 'apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: controller-manager\nspec:\n template:\n spec:\n containers:\n - name: manager\n imagePullPolicy: $(or $(3),IfNotPresent)\n env:\n - name: PROXY_IMAGE\n value: "$(2)"\n - name: IMAGE_PULL_POLICY\n value: "$(or $(3),)"\n' > config/.deploy/proxy_image_patch.yaml endef +# generate-bundle-overlay creates a temporary kustomize overlay at config/.bundle/ +# that wraps config/manifests with an image override for the controller. +# This avoids mutating config/manager/kustomization.yaml (which would break deploy targets). +# Usage: $(call generate-bundle-overlay,) +define generate-bundle-overlay + @rm -rf config/.bundle && mkdir -p config/.bundle + @img=$(1); printf 'apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nresources:\n- ../manifests\nimages:\n- name: controller\n newName: %s\n newTag: %s\n' \ + "$${img%:*}" "$${img##*:}" > config/.bundle/kustomization.yaml +endef + ##@ Deployment ifndef ignore-not-found @@ -288,6 +307,94 @@ dev-cleanup: ## Remove deployed controller and CRDs. $(MAKE) undeploy ignore-not-found=true $(MAKE) uninstall ignore-not-found=true +##@ OLM Bundle + +BUNDLE_CSV = bundle/manifests/claw-operator.clusterserviceversion.yaml + +.PHONY: bundle +bundle: manifests kustomize operator-sdk ## Generate OLM bundle manifests and validate. + $(OPERATOR_SDK) generate kustomize manifests -q + $(call generate-bundle-overlay,$(IMG)) + trap 'rm -rf config/.bundle' EXIT; \ + $(KUSTOMIZE) build config/.bundle | $(OPERATOR_SDK) generate bundle -q --overwrite \ + --version $(VERSION) $(BUNDLE_METADATA_OPTS) + sed -i 's|image: $(IMG)|image: REPLACE_IMAGE|' $(BUNDLE_CSV) + sed -i 's|value: $(PROXY_IMG)|value: REPLACE_PROXY_IMAGE|' $(BUNDLE_CSV) + sed -i 's|value: $(KUBECTL_IMG)|value: REPLACE_KUBECTL_IMAGE|' $(BUNDLE_CSV) + sed -i 's|^ createdAt: .*| createdAt: "REPLACE_CREATED_AT"|' $(BUNDLE_CSV) + sed -i 's|^ version: \(.*\)| relatedImages:\n - image: REPLACE_IMAGE\n name: manager\n - image: REPLACE_PROXY_IMAGE\n name: proxy\n - image: REPLACE_KUBECTL_IMAGE\n name: kubectl\n version: \1|' $(BUNDLE_CSV) + $(OPERATOR_SDK) bundle validate ./bundle + +.PHONY: bundle-build +bundle-build: ## Build the OLM bundle image. + $(CONTAINER_TOOL) build -f bundle.Dockerfile -t $(BUNDLE_IMG) . + +.PHONY: bundle-push +bundle-push: ## Push the OLM bundle image. + $(CONTAINER_TOOL) push $(BUNDLE_IMG) + +.PHONY: clean-bundle +clean-bundle: ## Remove the generated bundle directory. + rm -rf bundle/ + +##@ CD Pipeline + +QUAY_NAMESPACE ?= codeready-toolchain +OPERATOR_REPO = quay.io/$(QUAY_NAMESPACE)/claw-operator +PROXY_REPO = quay.io/$(QUAY_NAMESPACE)/claw-proxy +BUNDLE_REPO = quay.io/$(QUAY_NAMESPACE)/claw-operator-bundle +CATALOG_REPO = quay.io/$(QUAY_NAMESPACE)/claw-operator-catalog +OPM_CATALOG_BASE_IMG ?= quay.io/operator-framework/opm:$(OPM_VERSION) + +# Commit-count based versioning (lazily evaluated, only computed when CD targets run) +GIT_COMMIT_COUNT = $(shell git rev-list --count HEAD) +GIT_SHORT_SHA = $(shell git rev-parse --short HEAD) +CD_VERSION = 0.0.$(GIT_COMMIT_COUNT)-commit-$(GIT_SHORT_SHA) + +.PHONY: push-to-quay-staging +push-to-quay-staging: generate-cd-release-manifests ## Build and push bundle + catalog images for staging channel. + $(MAKE) bundle-build BUNDLE_IMG=$(BUNDLE_REPO):v$(CD_VERSION) + $(MAKE) bundle-push BUNDLE_IMG=$(BUNDLE_REPO):v$(CD_VERSION) + rm -rf catalog/ && mkdir -p catalog/claw-operator + $(OPM) render $(BUNDLE_REPO):v$(CD_VERSION) -o yaml > catalog/claw-operator/bundle.yaml + @printf -- '---\nschema: olm.package\nname: claw-operator\ndefaultChannel: staging\n---\nschema: olm.channel\npackage: claw-operator\nname: staging\nentries:\n- name: claw-operator.v$(CD_VERSION)\n skipRange: ">=0.0.0 <$(CD_VERSION)"\n' \ + > catalog/claw-operator/index.yaml + $(MAKE) build-and-push-catalog CATALOG_IMG=$(CATALOG_REPO):latest + +.PHONY: generate-cd-release-manifests +generate-cd-release-manifests: opm ## Generate bundle with CD version, images, and upgrade metadata. + $(MAKE) bundle IMG=$(OPERATOR_REPO):$(GIT_SHORT_SHA) VERSION=$(CD_VERSION) + @echo "Patching CSV for staging release $(CD_VERSION)..." + sed -i 's|REPLACE_IMAGE|$(OPERATOR_REPO):$(GIT_SHORT_SHA)|g' $(BUNDLE_CSV) + sed -i 's|REPLACE_PROXY_IMAGE|$(PROXY_REPO):$(GIT_SHORT_SHA)|g' $(BUNDLE_CSV) + sed -i 's|REPLACE_KUBECTL_IMAGE|$(KUBECTL_IMG)|g' $(BUNDLE_CSV) + sed -i 's|REPLACE_CREATED_AT|$(shell date -u +"%Y-%m-%dT%H:%M:%SZ")|' $(BUNDLE_CSV) + sed -i '/^ createdAt:/a\ olm.skipRange: ">=0.0.0 <$(CD_VERSION)"' $(BUNDLE_CSV) + +.PHONY: build-and-push-catalog +build-and-push-catalog: opm ## Validate, build, and push FBC catalog image from catalog/ directory. + $(OPM) validate catalog/ + printf 'FROM $(OPM_CATALOG_BASE_IMG)\nCOPY catalog /configs\nLABEL operators.operatorframework.io.index.configs.v1=/configs\nENTRYPOINT ["/bin/opm"]\nCMD ["serve", "/configs", "--cache-dir=/tmp/cache"]\n' | \ + $(CONTAINER_TOOL) build -f - -t $(CATALOG_IMG) . + $(CONTAINER_TOOL) push $(CATALOG_IMG) + rm -rf catalog/ + +.PHONY: publish-current-bundle +publish-current-bundle: opm ## One-shot publish for testing OLM install (alpha channel, no replaces). Requires IMG and PROXY_IMG. + $(MAKE) bundle VERSION=$(CD_VERSION) CHANNELS=alpha DEFAULT_CHANNEL=alpha + @echo "Patching CSV for alpha release $(CD_VERSION)..." + sed -i 's|REPLACE_IMAGE|$(IMG)|g' $(BUNDLE_CSV) + sed -i 's|REPLACE_PROXY_IMAGE|$(PROXY_IMG)|g' $(BUNDLE_CSV) + sed -i 's|REPLACE_KUBECTL_IMAGE|$(KUBECTL_IMG)|g' $(BUNDLE_CSV) + sed -i 's|REPLACE_CREATED_AT|$(shell date -u +"%Y-%m-%dT%H:%M:%SZ")|' $(BUNDLE_CSV) + $(MAKE) bundle-build BUNDLE_IMG=$(BUNDLE_REPO):v$(CD_VERSION) + $(MAKE) bundle-push BUNDLE_IMG=$(BUNDLE_REPO):v$(CD_VERSION) + rm -rf catalog/ && mkdir -p catalog/claw-operator + $(OPM) render $(BUNDLE_REPO):v$(CD_VERSION) -o yaml > catalog/claw-operator/bundle.yaml + @printf -- '---\nschema: olm.package\nname: claw-operator\ndefaultChannel: alpha\n---\nschema: olm.channel\npackage: claw-operator\nname: alpha\nentries:\n- name: claw-operator.v$(CD_VERSION)\n' \ + > catalog/claw-operator/index.yaml + $(MAKE) build-and-push-catalog + ##@ Dependencies ## Location to install dependencies to @@ -297,11 +404,13 @@ $(LOCALBIN): ## Tool Binaries KUBECTL ?= kubectl -KIND ?= kind +KIND ?= $(LOCALBIN)/kind KUSTOMIZE ?= $(LOCALBIN)/kustomize CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen ENVTEST ?= $(LOCALBIN)/setup-envtest GOLANGCI_LINT = $(LOCALBIN)/golangci-lint +OPERATOR_SDK ?= $(LOCALBIN)/operator-sdk +OPM ?= $(LOCALBIN)/opm ## Tool Versions KUSTOMIZE_VERSION ?= v5.6.0 @@ -311,6 +420,9 @@ ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller #ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31) ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}') GOLANGCI_LINT_VERSION ?= v2.11.4 +OPERATOR_SDK_VERSION ?= v1.42.0 +OPM_VERSION ?= v1.59.0 +KIND_VERSION ?= v0.31.0 .PHONY: kustomize kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. @@ -340,6 +452,38 @@ golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. $(GOLANGCI_LINT): $(LOCALBIN) $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) +.PHONY: operator-sdk +operator-sdk: $(OPERATOR_SDK) ## Download operator-sdk locally if necessary. +$(OPERATOR_SDK): $(LOCALBIN) + $(call download-tool,$(OPERATOR_SDK),$(OPERATOR_SDK_VERSION),\ + https://github.com/operator-framework/operator-sdk/releases/download/$(OPERATOR_SDK_VERSION)/operator-sdk_$(OS)_$(ARCH)) + +.PHONY: opm +opm: $(OPM) ## Download opm locally if necessary. +$(OPM): $(LOCALBIN) + $(call download-tool,$(OPM),$(OPM_VERSION),\ + https://github.com/operator-framework/operator-registry/releases/download/$(OPM_VERSION)/$(OS)-$(ARCH)-opm) + +.PHONY: kind +kind: $(KIND) ## Download kind locally if necessary. +$(KIND): $(LOCALBIN) + $(call download-tool,$(KIND),$(KIND_VERSION),\ + https://github.com/kubernetes-sigs/kind/releases/download/$(KIND_VERSION)/kind-$(OS)-$(ARCH)) + +# download-tool downloads a pre-built binary if it doesn't exist +# $1 - target path with name of binary +# $2 - version tag +# $3 - download URL +define download-tool +@[ -f "$(1)-$(2)" ] || { \ +set -e; \ +echo "Downloading $(1) $(2)"; \ +curl --silent --show-error --location --fail --retry 3 --output $(1)-$(2) $(3); \ +chmod +x $(1)-$(2); \ +}; \ +ln -sf $(1)-$(2) $(1) +endef + # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist # $1 - target path with name of binary # $2 - package url which can be installed diff --git a/README.md b/README.md index e26440a..c896a8f 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,79 @@ The operator applies multiple layers of defense: - **Gateway authentication** -- a cryptographically random 256-bit token is auto-generated per instance and required for all gateway access. - **Device pairing** -- remote browser connections require a one-time approval via CLI before they can interact with the instance. -## Quick Start +## Installation (OLM) + +The recommended way to install the operator on an OpenShift cluster with OLM. + +### 1. Create the Operator Namespace + +```sh +oc create namespace claw-operator +``` + +### 2. Create a CatalogSource + +```sh +oc apply -f - <|"make container-build"| img["Operator Image"] + dev -->|"make container-build-proxy"| proxy["Proxy Image"] + dev -->|"make deploy"| cluster["Cluster"] + img --> cluster + proxy --> cluster + cluster -->|"kustomize build config/.deploy"| deploy["Deployment + RBAC + CRDs"] +``` + +### Target OLM Deployment Flow + +```mermaid +flowchart LR + dev["Developer / CI"] + dev -->|"make container-build"| img["Operator Image\nquay.io/.../claw-operator:tag"] + dev -->|"make container-build-proxy"| proxy["Proxy Image\nquay.io/.../claw-proxy:tag"] + dev -->|"make bundle"| bundleDir["bundle/\nCSV + CRDs + metadata"] + bundleDir -->|"podman build -f bundle.Dockerfile"| bundleImg["Bundle Image\nquay.io/.../claw-operator-bundle:version"] + bundleImg -->|"opm render + podman build"| catalogImg["Catalog Image\nquay.io/.../claw-operator-catalog:latest"] + catalogImg -->|"CatalogSource"| olm["OLM on Cluster"] + olm -->|"Subscription"| deploy["Operator Deployment"] +``` + +### Bundle Directory Structure + +After running `make bundle`, the generated `bundle/` directory contains: + +``` +bundle/ +├── manifests/ +│ ├── claw-operator.clusterserviceversion.yaml +│ ├── claw-operator-controller-manager-metrics-service_v1_service.yaml +│ ├── claw.sandbox.redhat.com_claws.yaml +│ └── claw.sandbox.redhat.com_clawdevicepairingrequests.yaml +├── metadata/ +│ └── annotations.yaml +├── tests/ +│ └── scorecard/ +│ └── config.yaml +``` + +Note: RBAC resources (roles, bindings, service account) and the Deployment are embedded in the CSV by `operator-sdk generate bundle`, not emitted as separate files. Additional manifests may appear depending on Kustomize configuration. + +The existing `config/default/kustomization.yaml` excludes CRDs (`../crd` is commented out) because `make deploy` installs them separately via `make install`. For bundle generation, CRDs must be in the kustomize output. Rather than uncommenting `../crd` in `config/default` (which would change existing deployment behavior), `../crd` is added directly to `config/manifests/kustomization.yaml` so only the bundle generation path picks them up. + +The `bundle/` directory is **committed to the repo**. CI enforces consistency by running `make bundle && git diff --exit-code bundle/` to catch drift from the source in `config/`. + +### Catalog Image Build Strategy + +The catalog is **not committed to the repo** — it is built ephemerally during CD, matching the host-operator's approach of building the index image on-the-fly without committing state back to the repository. Each CD run: + +1. Pulls the existing catalog image (if one exists) +2. Renders the previous catalog content via `opm render ` +3. Renders the new bundle via `opm render ` +4. Assembles the updated FBC (package + channel + bundle entries) into a temporary `catalog/` directory +5. Validates with `opm validate catalog/` +6. Builds and pushes the new catalog image + +This avoids the need for git commits back to master during CD (no git identity, protected branch, or race condition concerns). The `olm.skipRange` on the staging channel means only the latest bundle entry is needed for upgrades, keeping the catalog minimal. + +## Core Concepts + +### ClusterServiceVersion (CSV) + +The CSV base lives at `config/manifests/bases/claw-operator.clusterserviceversion.yaml` (the path already referenced by the existing `config/manifests/kustomization.yaml`). It declares: + +- **Owned CRDs**: `Claw` and `ClawDevicePairingRequest` +- **Deployment spec**: single controller-manager container with `PROXY_IMAGE` and `KUBECTL_IMAGE` env vars +- **`relatedImages`**: all three operator-managed images (manager, proxy, kubectl), enabling disconnected/airgapped installs +- **Install modes**: `OwnNamespace: true`, `SingleNamespace: true`, `MultiNamespace: false`, `AllNamespaces: false` (matches host-operator; RBAC controls actual cluster-wide watch scope) +- **Metadata**: display name, description, icon, maintainers, links, keywords, maturity level (`alpha`) + +### `relatedImages` + +The CSV declares all three operator-managed images in `spec.relatedImages`: + +```yaml +spec: + relatedImages: + - name: manager + image: REPLACE_IMAGE + - name: proxy + image: REPLACE_PROXY_IMAGE + - name: kubectl + image: REPLACE_KUBECTL_IMAGE +``` + +The Deployment spec references the proxy and kubectl images via the `PROXY_IMAGE` and `KUBECTL_IMAGE` env vars on the manager container. During CD bundle generation, the Makefile replaces `REPLACE_IMAGE`, `REPLACE_PROXY_IMAGE`, and `REPLACE_KUBECTL_IMAGE` with the actual `quay.io/codeready-toolchain/claw-operator:`, `quay.io/codeready-toolchain/claw-proxy:`, and `quay.io/openshift/origin-cli:` references. `REPLACE_CREATED_AT` is substituted with the current UTC timestamp (e.g., `2026-05-14T12:00:00Z`) to populate `spec.annotations.createdAt`. + +### Bundle Image + +An OCI image containing the `bundle/` directory contents. Built from `bundle.Dockerfile` at the project root (operator-sdk default naming). + +### Catalog Image (File-Based Catalog) + +A file-based catalog (FBC) image at `quay.io/codeready-toolchain/claw-operator-catalog:latest`. The catalog contains `olm.package`, `olm.channel`, and `olm.bundle` entries in declarative YAML format. The catalog image is built from a temporary directory during CD and serves the catalog via `opm serve`. A `CatalogSource` on the cluster references this image; OLM polls it for available operator versions and handles upgrades via the `Subscription` resource. + +FBC is the current OPM standard, replacing the deprecated SQLite-based `opm index add` approach used by the host-operator. While the host-operator uses `opm index add --from-index` to accumulate bundles in a SQLite index image, the claw-operator uses FBC for forward-compatibility. The ephemeral build strategy (pull previous catalog, render, update, push) mirrors the host-operator's fire-and-forget CD model without requiring git commits back to the repo. + +### Versioning + +Commit-count-based, matching the toolchain pattern: + +- **Format:** `0.0.-commit-` (e.g., `0.0.342-commit-a1b2c3d`) +- **`replaces`:** computed from `HEAD^` as `0.0.-commit-` +- **`olm.skipRange`:** `>=0.0.0 =0.0.0 <0.0.342-commit-a1b2c3d`) on the staging channel for fast-forward upgrades + +### Channels + +- **`staging`** (default): auto-published on every master push, `olm.skipRange` for fast-forward +- **`alpha`**: manual publish via `make publish-current-bundle` (first-release mode, no `replaces`) + +## Implementation Plan + +### Phase 1: CSV Base and Bundle Generation + +**Goal:** `make bundle` produces a valid bundle that passes `operator-sdk bundle validate`. + +1. Create `config/manifests/bases/claw-operator.clusterserviceversion.yaml` with: + - Owned CRDs (`Claw`, `ClawDevicePairingRequest`) with display names and descriptions + - Deployment spec placeholder (populated by `operator-sdk generate bundle`) + - `relatedImages` with `REPLACE_IMAGE`, `REPLACE_PROXY_IMAGE`, and `REPLACE_KUBECTL_IMAGE` placeholders + - Install modes (`OwnNamespace: true`, `SingleNamespace: true`, `MultiNamespace: false`, `AllNamespaces: false`) + - Metadata: icon, description, maintainers (`devsandbox@redhat.com`), links, keywords + - `REPLACE_CREATED_AT` placeholder for build timestamps + +2. Add `../crd` to `config/manifests/kustomization.yaml` so CRDs are included in the bundle generation kustomize output (without changing `config/default/kustomization.yaml` where `../crd` is intentionally excluded) + +3. Add Makefile targets: + - `bundle`: runs `operator-sdk generate kustomize manifests`, pipes through kustomize, generates bundle with `operator-sdk generate bundle`, then validates + - `bundle-build`: builds the bundle image from `bundle.Dockerfile` + - `bundle-push`: pushes the bundle image + - `clean-bundle`: removes the `bundle/` directory + +4. Create `bundle.Dockerfile` at the project root (generated by `operator-sdk generate bundle`, committed to the repo) + +5. Add `OPERATOR_SDK` (v1.42.0) to the tool dependencies section of the Makefile (similar to existing `KUSTOMIZE`, `CONTROLLER_GEN`, etc.), matching the scorecard test image version in `config/scorecard/` + +6. Commit the generated `bundle/` directory + +### Phase 2: CD Pipeline and Catalog Image + +**Goal:** CI automatically builds operator images, generates the bundle, pushes bundle and catalog images on master push. + +1. Add self-contained Makefile targets for CD: + - `push-to-quay-staging`: computes version, generates release manifests, pushes bundle + catalog + - `generate-cd-release-manifests`: computes commit-count version, runs `make bundle` with version and image overrides, patches CSV with `replaces` clause and `olm.skipRange` + - `build-and-push-catalog`: pulls the existing catalog image (if any), renders its FBC via `opm render`, renders the new bundle via `opm render `, assembles the updated FBC entries (package + channel + bundle) into a temporary `catalog/` directory, validates with `opm validate`, builds the catalog image, and pushes it. On first release (no existing catalog image), generates the FBC from scratch with just the `olm.package` and initial bundle entry. + - `publish-current-bundle`: one-shot publish for the current commit (alpha channel, first release, no `replaces`) + +2. Add GitHub Actions CD workflow (`.github/workflows/cd.yml`): + - Trigger: push to `master` + - Steps: build operator + proxy images, push to `quay.io/codeready-toolchain/`, run `make push-to-quay-staging` + - Secrets: `QUAY_TOKEN` (base64-encoded quay.io auth, matching host-operator convention) + +3. Add `opm` (v1.59.0) tool dependency to the Makefile (used for `opm render` and `opm validate`). This version comes from the `operator-registry` project and matches the version that `operator-sdk` v1.42.0 depends on (see `operator-sdk/go.mod`), following the same version selection used by the host-operator's `prepare-tools-action`. + +### Phase 3: Bundle Validation in PR CI + +> **Note:** This phase depends only on Phase 1 (bundle generation) and can be implemented independently of Phase 2 (CD pipeline). + +**Goal:** Every PR validates the bundle; master is always publishable. + +1. Add a new `.github/workflows/lint-bundle.yml` workflow (triggered on PRs and master push, matching the existing workflow triggers): + - Run `make bundle` + - Run `operator-sdk bundle validate ./bundle` (static validation, no cluster needed) + - Run `git diff --exit-code bundle/` to enforce committed bundle consistency + +2. Full scorecard suite runs in the CD pipeline (post-merge) where a cluster is available + +### Phase 4: Scorecard in CD + +**Goal:** Published bundles are validated by the Operator Framework scorecard. + +1. Add scorecard execution to the CD workflow after bundle publish +2. Use the existing `config/scorecard/` configuration (basic + OLM suites) +3. Scorecard failures should warn but not block the publish (initially) + +## Dev Workflow + +The OLM deployment model is the production path. Daily development is unchanged: + +1. **`make dev-deploy`** (existing) — raw Kustomize, no OLM. Fast iteration, no bundle generation needed. No OLM required on the dev cluster. +2. **`make publish-current-bundle`** — available for testing the full OLM install path on a dev cluster with OLM installed (one-shot, alpha channel). + +## Decisions Summary + +All decisions resolved in [olm-deployment-questions.md](olm-deployment-questions.md): + +| # | Question | Decision | +|---|----------|----------| +| Q1 | Bundle directory | Committed with CI enforcement (`git diff --exit-code`) | +| Q2 | CD scripts | Self-contained Makefile targets (no toolchain-cicd dependency) | +| Q3 | CI/CD design | Static bundle validation on PRs; full CD on master push | +| Q4 | Container file naming | `bundle.Dockerfile` (operator-sdk default) | +| Q5 | Install modes | `OwnNamespace + SingleNamespace` (matches host-operator) | +| Q6 | Catalog image registry | `quay.io/codeready-toolchain/claw-operator-catalog:latest` (ephemeral FBC, not committed) | +| Q7 | Versioning | Commit-count-based (`0.0.-commit-`) | +| Q8 | Channels | `staging` (auto, default) + `alpha` (manual) | +| Q9 | Scorecard in CI | Static validation in PRs; full scorecard in CD | +| Q10 | Dev workflow | `make dev-deploy` unchanged; OLM is production-only | diff --git a/docs/proposals/olm-deployment-questions.md b/docs/proposals/olm-deployment-questions.md new file mode 100644 index 0000000..55463c7 --- /dev/null +++ b/docs/proposals/olm-deployment-questions.md @@ -0,0 +1,170 @@ +**Status:** Resolved — all decisions made +**Related:** [Design document](olm-deployment-design.md) + +All questions resolved. Each entry preserves the chosen option with full trade-offs and the decision rationale. + +## Q1: Bundle directory — committed or `.gitignore`d? + +The `bundle/` directory is generated by `operator-sdk generate bundle`. It contains the final CSV, CRDs, and metadata. The question is whether to commit it to the repo or generate it fresh each time. + +### Option C: Committed but with CI enforcement + +- **Pro:** Reviewable in PRs and CI verifies `make bundle && git diff --exit-code bundle/` to catch drift. +- **Pro:** Best of both worlds — visibility plus correctness. +- **Con:** Slightly more CI complexity to enforce consistency. + +**Decision:** Option C — matches host-operator pattern; reviewable in PRs with CI enforcing consistency. + +_Considered and rejected: Option A — committed without CI check (risk of drift), Option B — `.gitignore`d (not visible in PR diffs)._ + +--- + +## Q2: CD scripts — use toolchain-cicd or self-contained? + +The codeready-toolchain operators use shared CD scripts from the `toolchain-cicd` repo (`olm-setup.sh`, `generate-cd-release-manifests.sh`, `push-bundle-and-index-image.sh`). These scripts handle version computation, CSV patching, bundle image building, and index image updates. The claw-operator could either integrate with these scripts or implement equivalent functionality in its own Makefile. + +### Option B: Self-contained Makefile targets + +- **Pro:** All build logic is in one repo — easier to understand, debug, and modify. +- **Pro:** Can be tailored to the claw-operator's specific needs (three images, no embedded repos). +- **Pro:** No network dependency on external scripts at build time. +- **Con:** Diverges from the organizational pattern — separate maintenance burden. +- **Con:** Must reimplement the version computation and CSV patching logic. + +**Decision:** Option B — self-contained Makefile targets tailored to the single-repo, three-image claw-operator. + +_Considered and rejected: Option A — toolchain-cicd integration (multi-repo complexity not needed), Option C — fork of toolchain-cicd scripts (maintenance burden of backporting)._ + +--- + +## Q3: CI/CD pipeline design + +The claw-operator currently has three GitHub Actions workflows: test, lint, and e2e. None build or push images. The OLM adoption requires a CD pipeline. + +### Option C: Add bundle validation to existing PR workflows, CD on master only + +- **Pro:** PRs validate `make bundle` and `operator-sdk bundle validate` without pushing anything. +- **Pro:** CD workflow on master push handles the actual image build + bundle + index publish. +- **Pro:** Keeps PR feedback fast (no image push) while ensuring master is always publishable. +- **Con:** Slightly more workflow code, but well-separated concerns. + +**Decision:** Option C — static bundle validation in PRs, full CD pipeline on master push only. + +_Considered and rejected: Option A — single CD workflow (harder to debug), Option B — separate image build and publish workflows (more coordination overhead)._ + +--- + +## Q4: Container file naming — `bundle.Dockerfile` vs `Containerfile.bundle` + +The project uses `Containerfile` and `Containerfile.proxy` (podman convention). The OLM tooling (`operator-sdk generate bundle`) generates `bundle.Dockerfile` by default. + +### Option A: `bundle.Dockerfile` (operator-sdk default) + +- **Pro:** Zero-effort — `operator-sdk generate bundle` creates it automatically. +- **Pro:** Matches the name the toolchain-cicd scripts expect. +- **Con:** Inconsistent with the project's `Containerfile.*` naming convention. + +**Decision:** Option A — use the operator-sdk default naming to avoid fighting the tooling. + +_Considered and rejected: Option B — `Containerfile.bundle` (requires renaming workarounds and adjusting CD commands for cosmetic consistency)._ + +--- + +## Q5: Install modes + +OLM install modes control which namespace configurations the operator supports. + +### Option A: OwnNamespace + SingleNamespace only + +- **Pro:** Matches the host-operator's configuration. +- **Pro:** The operator runs in its own namespace but watches all namespaces for Claw CRs. +- **Con:** Technically the operator watches all namespaces, so `AllNamespaces` is more accurate for the watch scope. + +**Decision:** Option A — `OwnNamespace: true, SingleNamespace: true, MultiNamespace: false, AllNamespaces: false`. Matches host-operator; RBAC controls actual watch scope. + +_Considered and rejected: Option B — AllNamespaces (diverges from host-operator pattern), Option C — all three modes (MultiNamespace untested)._ + +--- + +## Q6: Catalog image naming and registry + +The catalog image is the OLM file-based catalog (FBC) that references all bundle versions. It needs a name, a registry location, and a tag strategy. + +### Option A: `claw-operator-catalog` on quay.io/codeready-toolchain + +- **Pro:** Same registry org as host-operator (`quay.io/codeready-toolchain/host-operator-index`). +- **Pro:** Consistent naming: `-catalog`. +- **Con:** Requires push access to the `codeready-toolchain` quay.io org. + +**Decision:** Option A — `quay.io/codeready-toolchain/claw-operator-catalog:latest`, using file-based catalog (FBC) format, keeping all toolchain catalogs in the same registry namespace. The catalog is built ephemerally during CD (not committed to the repo), matching the host-operator's fire-and-forget CD model. + +_Considered and rejected: Option B — project-specific quay.io namespace (diverges from organizational pattern)._ + +--- + +## Q7: Versioning strategy + +The CSV version determines the OLM upgrade graph. Each new version must be unique and the `replaces` clause must point to the previous version. + +### Option A: Commit-count-based (toolchain pattern) + +Format: `0.0.-commit-` (e.g., `0.0.342-commit-a1b2c3d`). + +- **Pro:** Automatic — no manual version bumping, every commit gets a unique version. +- **Pro:** Matches the host-operator/member-operator versioning exactly. +- **Pro:** `replaces` is computed deterministically from `HEAD^`. +- **Con:** Versions aren't human-meaningful — you can't tell which version is "newer" without comparing commit counts. +- **Con:** Rebases or squash merges can break the commit-count chain. + +**Decision:** Option A — commit-count-based versioning matching the toolchain pattern. Pre-v1, no external consumers, automated CD. + +_Considered and rejected: Option B — semver (requires manual bumping, doesn't match toolchain), Option C — hybrid commit-count + semver (premature complexity)._ + +--- + +## Q8: Channel strategy + +OLM channels define upgrade streams. Users subscribe to a channel and receive updates from it. + +### Option B: `staging` + `alpha` + +- **Pro:** `staging` auto-publishes on every master push; `alpha` is for manual one-shot publishes (`make publish-current-bundle`). +- **Pro:** Matches the host-operator's dual-channel setup. +- **Con:** Extra complexity for a project that may not need two channels yet. + +**Decision:** Option B — `staging` (default, auto-published) + `alpha` (manual one-shot). Matches toolchain pattern; `stable` can be added later. + +_Considered and rejected: Option A — staging only (no separate "known good" channel), Option C — staging + stable (premature for pre-v1)._ + +--- + +## Q9: Scorecard integration in CI + +The project already has `config/scorecard/` configured with OLM suite tests. The question is when and how to run them. + +### Option C: Static validation in PRs, full scorecard in CD + +- **Pro:** `operator-sdk bundle validate` (static, no cluster needed) runs on PRs for fast feedback. +- **Pro:** Full scorecard (with cluster) runs in CD for comprehensive validation. +- **Pro:** Best balance of speed and thoroughness. +- **Con:** Two different validation levels could create confusion about what "passes CI" means. + +**Decision:** Option C — static `operator-sdk bundle validate` in PRs, full scorecard suite in CD pipeline. + +_Considered and rejected: Option A — scorecard in PRs (too slow, requires Kind cluster), Option B — scorecard in CD only (no pre-merge bundle validation)._ + +--- + +## Q10: Dev workflow impact + +The current dev workflow (`make dev-build dev-push dev-deploy`) bypasses OLM entirely. The question is how OLM adoption changes daily development. + +### Option A: Keep `make dev-deploy` as-is, OLM is production-only + +- **Pro:** Zero friction for developers — no change to the fast iteration loop. +- **Pro:** No need for OLM installed on dev clusters. +- **Con:** The OLM install path is only tested in CI/CD, not during development. + +**Decision:** Option A — `make dev-deploy` unchanged. OLM is the production/CI path; daily dev stays fast and OLM-free. + +_Considered and rejected: Option B — OLM-based dev install (too slow, requires OLM everywhere), Option C — dual dev workflows (maintenance burden)._ diff --git a/internal/controller/claw_mcp.go b/internal/controller/claw_mcp.go index 4dd35c6..07cc671 100644 --- a/internal/controller/claw_mcp.go +++ b/internal/controller/claw_mcp.go @@ -135,7 +135,7 @@ func buildMcpServerConfig(spec clawv1alpha1.McpServerSpec) map[string]any { } else { entry["url"] = spec.URL if spec.Transport != "" { - entry["transport"] = spec.Transport + entry["transport"] = string(spec.Transport) } } diff --git a/internal/controller/claw_mcp_test.go b/internal/controller/claw_mcp_test.go index 69a8626..58f6877 100644 --- a/internal/controller/claw_mcp_test.go +++ b/internal/controller/claw_mcp_test.go @@ -66,7 +66,7 @@ func TestInjectMcpServersIntoConfigMap(t *testing.T) { instance := testClawWithMcpServers(map[string]clawv1alpha1.McpServerSpec{ "context7": { URL: "https://mcp.context7.com/mcp", - Transport: "streamable-http", + Transport: clawv1alpha1.McpTransportStreamableHTTP, }, }) @@ -115,7 +115,7 @@ func TestInjectMcpServersIntoConfigMap(t *testing.T) { instance := testClawWithMcpServers(map[string]clawv1alpha1.McpServerSpec{ "context7": { URL: "https://mcp.context7.com/mcp", - Transport: "streamable-http", + Transport: clawv1alpha1.McpTransportStreamableHTTP, }, "github": { Command: "npx", @@ -294,7 +294,7 @@ func TestBuildMcpServerConfig(t *testing.T) { t.Run("should not include envFrom for HTTP servers", func(t *testing.T) { spec := clawv1alpha1.McpServerSpec{ URL: "https://example.com/mcp", - Transport: "streamable-http", + Transport: clawv1alpha1.McpTransportStreamableHTTP, } config := buildMcpServerConfig(spec) @@ -374,7 +374,7 @@ func TestMcpServersIntegration(t *testing.T) { McpServers: map[string]clawv1alpha1.McpServerSpec{ "context7": { URL: "https://mcp.context7.com/mcp", - Transport: "streamable-http", + Transport: clawv1alpha1.McpTransportStreamableHTTP, }, }, }, @@ -962,7 +962,7 @@ func TestMcpServerCELValidation(t *testing.T) { McpServers: map[string]clawv1alpha1.McpServerSpec{ "context7": { URL: "https://mcp.context7.com/mcp", - Transport: "streamable-http", + Transport: clawv1alpha1.McpTransportStreamableHTTP, }, }, }, @@ -970,4 +970,23 @@ func TestMcpServerCELValidation(t *testing.T) { err := k8sClient.Create(ctx, instance) require.NoError(t, err, "valid HTTP MCP server should be accepted") }) + + t.Run("should reject stdio MCP server with transport set", func(t *testing.T) { + t.Cleanup(func() { deleteAndWaitAllResources(t, namespace) }) + + instance := &clawv1alpha1.Claw{ + ObjectMeta: metav1.ObjectMeta{Name: testInstanceName, Namespace: namespace}, + Spec: clawv1alpha1.ClawSpec{ + McpServers: map[string]clawv1alpha1.McpServerSpec{ + "bad": { + Command: "npx", + Transport: clawv1alpha1.McpTransportSSE, + }, + }, + }, + } + err := k8sClient.Create(ctx, instance) + require.Error(t, err, "CEL should reject stdio MCP server with transport set") + assert.Contains(t, err.Error(), "transport is only allowed for HTTP MCP servers (url)") + }) } diff --git a/internal/controller/claw_proxy_test.go b/internal/controller/claw_proxy_test.go index 7112253..65d4494 100644 --- a/internal/controller/claw_proxy_test.go +++ b/internal/controller/claw_proxy_test.go @@ -1288,7 +1288,7 @@ func TestGenerateProxyConfigKubernetes(t *testing.T) { func TestMcpServerDomainExtraction(t *testing.T) { t.Run("should auto-extract HTTP MCP URL domain as passthrough route", func(t *testing.T) { mcpServers := map[string]clawv1alpha1.McpServerSpec{ - "context7": {URL: "https://mcp.context7.com/mcp", Transport: "streamable-http"}, + "context7": {URL: "https://mcp.context7.com/mcp", Transport: clawv1alpha1.McpTransportStreamableHTTP}, } data, err := generateProxyConfig(nil, mcpServers, nil) diff --git a/internal/controller/claw_resource_controller.go b/internal/controller/claw_resource_controller.go index 949ffef..545b7b7 100644 --- a/internal/controller/claw_resource_controller.go +++ b/internal/controller/claw_resource_controller.go @@ -373,6 +373,7 @@ type ClawResourceReconciler struct { ImagePullPolicy string } +// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles,verbs=get;list;watch;create;update;patch // +kubebuilder:rbac:groups=claw.sandbox.redhat.com,resources=claws,verbs=get;list;watch // +kubebuilder:rbac:groups=claw.sandbox.redhat.com,resources=claws/status,verbs=get;update;patch // +kubebuilder:rbac:groups=claw.sandbox.redhat.com,resources=claws/finalizers,verbs=update @@ -385,7 +386,6 @@ type ClawResourceReconciler struct { // +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=route.openshift.io,resources=routes,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=route.openshift.io,resources=routes/custom-host,verbs=create;update -// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles,verbs=get;list;watch;create;update;patch // +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=get;list;watch;create;update;patch // Reconcile manages the complete lifecycle of resources for Claw instances