From e0d4f8ce3f52011ab3d4907113d728303de63f1a Mon Sep 17 00:00:00 2001 From: MikelAlejoBR Date: Thu, 14 May 2026 10:28:17 -0400 Subject: [PATCH 01/14] feature: bundle operator with OLM and OperatorSDK In order to be able to install and deploy the operator in OpenShift, we need to bundle it with OLM and OperatorSDK. Assisted-by: Cursor with Opus 4.6 Jira ticket: SANDBOX-1843 --- .github/workflows/cd.yml | 63 +++ .github/workflows/lint-bundle.yml | 31 ++ .gitignore | 6 +- Makefile | 141 ++++++ bundle.Dockerfile | 21 + ...er-manager-metrics-service_v1_service.yaml | 20 + ...c.authorization.k8s.io_v1_clusterrole.yaml | 10 + .../claw-operator.clusterserviceversion.yaml | 351 +++++++++++++++ ....redhat.com_clawdevicepairingrequests.yaml | 189 ++++++++ .../claw.sandbox.redhat.com_claws.yaml | 413 ++++++++++++++++++ bundle/metadata/annotations.yaml | 15 + bundle/tests/scorecard/config.yaml | 70 +++ .../claw-operator.clusterserviceversion.yaml | 71 +++ config/manifests/kustomization.yaml | 1 + ...law_v1alpha1_clawdevicepairingrequest.yaml | 9 + config/samples/kustomization.yaml | 1 + docs/proposals/olm-deployment-design.md | 233 ++++++++++ docs/proposals/olm-deployment-questions.md | 170 +++++++ 18 files changed, 1814 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/cd.yml create mode 100644 .github/workflows/lint-bundle.yml create mode 100644 bundle.Dockerfile create mode 100644 bundle/manifests/claw-operator-controller-manager-metrics-service_v1_service.yaml create mode 100644 bundle/manifests/claw-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml create mode 100644 bundle/manifests/claw-operator.clusterserviceversion.yaml create mode 100644 bundle/manifests/claw.sandbox.redhat.com_clawdevicepairingrequests.yaml create mode 100644 bundle/manifests/claw.sandbox.redhat.com_claws.yaml create mode 100644 bundle/metadata/annotations.yaml create mode 100644 bundle/tests/scorecard/config.yaml create mode 100644 config/manifests/bases/claw-operator.clusterserviceversion.yaml create mode 100644 config/samples/claw_v1alpha1_clawdevicepairingrequest.yaml create mode 100644 docs/proposals/olm-deployment-design.md create mode 100644 docs/proposals/olm-deployment-questions.md diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..5b49e21 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,63 @@ +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: | + curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64 + chmod +x ./kind + sudo mv ./kind /usr/local/bin/kind + 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: 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..53b701f --- /dev/null +++ b/.github/workflows/lint-bundle.yml @@ -0,0 +1,31 @@ +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 + + - name: Check bundle consistency + run: git diff --exit-code bundle/ diff --git a/.gitignore b/.gitignore index 2c2956a..e983c3b 100644 --- a/.gitignore +++ b/.gitignore @@ -28,7 +28,11 @@ go.work *~ tmp -# Generated deploy overlay (created by make deploy/build-installer) +# Generated overlays (created by make deploy/bundle) config/.deploy/ +config/.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..586b149 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 @@ -174,6 +187,16 @@ define generate-deploy-overlay @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 + @printf 'apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nresources:\n- ../manifests\nimages:\n- name: controller\n newName: %s\n newTag: %s\n' \ + "$$(echo $(1) | cut -d: -f1)" "$$(echo $(1) | cut -d: -f2)" > config/.bundle/kustomization.yaml +endef + ##@ Deployment ifndef ignore-not-found @@ -288,6 +311,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|^ 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) +GIT_PREVIOUS_SHORT_SHA = $(shell git rev-parse --short HEAD~1) +CD_VERSION = 0.0.$(GIT_COMMIT_COUNT)-commit-$(GIT_SHORT_SHA) +CD_PREVIOUS_VERSION = 0.0.$(shell expr $(GIT_COMMIT_COUNT) - 1)-commit-$(GIT_PREVIOUS_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 's|^ version: \(.*\)| replaces: claw-operator.v$(CD_PREVIOUS_VERSION)\n version: \1|' $(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). + $(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 + $(MAKE) bundle-push + rm -rf catalog/ && mkdir -p catalog/claw-operator + $(OPM) render $(BUNDLE_IMG) -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 @@ -302,6 +413,8 @@ 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 +424,8 @@ 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 .PHONY: kustomize kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. @@ -340,6 +455,32 @@ 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) + +# 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 -sSLo $(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/bundle.Dockerfile b/bundle.Dockerfile new file mode 100644 index 0000000..7f8b237 --- /dev/null +++ b/bundle.Dockerfile @@ -0,0 +1,21 @@ +FROM scratch + +# Core bundle labels. +LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 +LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ +LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ +LABEL operators.operatorframework.io.bundle.package.v1=claw-operator +LABEL operators.operatorframework.io.bundle.channels.v1=staging +LABEL operators.operatorframework.io.bundle.channel.default.v1=staging +LABEL operators.operatorframework.io.metrics.builder=operator-sdk-v1.42.0 +LABEL operators.operatorframework.io.metrics.mediatype.v1=metrics+v1 +LABEL operators.operatorframework.io.metrics.project_layout=go.kubebuilder.io/v4 + +# Labels for testing. +LABEL operators.operatorframework.io.test.mediatype.v1=scorecard+v1 +LABEL operators.operatorframework.io.test.config.v1=tests/scorecard/ + +# Copy files to locations specified by labels. +COPY bundle/manifests /manifests/ +COPY bundle/metadata /metadata/ +COPY bundle/tests/scorecard /tests/scorecard/ diff --git a/bundle/manifests/claw-operator-controller-manager-metrics-service_v1_service.yaml b/bundle/manifests/claw-operator-controller-manager-metrics-service_v1_service.yaml new file mode 100644 index 0000000..f5253cd --- /dev/null +++ b/bundle/manifests/claw-operator-controller-manager-metrics-service_v1_service.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Service +metadata: + creationTimestamp: null + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: claw-operator + control-plane: controller-manager + name: claw-operator-controller-manager-metrics-service +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: 8443 + selector: + app.kubernetes.io/name: claw-operator + control-plane: controller-manager +status: + loadBalancer: {} diff --git a/bundle/manifests/claw-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml b/bundle/manifests/claw-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml new file mode 100644 index 0000000..1ead909 --- /dev/null +++ b/bundle/manifests/claw-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml @@ -0,0 +1,10 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: claw-operator-metrics-reader +rules: +- nonResourceURLs: + - /metrics + verbs: + - get diff --git a/bundle/manifests/claw-operator.clusterserviceversion.yaml b/bundle/manifests/claw-operator.clusterserviceversion.yaml new file mode 100644 index 0000000..080ee68 --- /dev/null +++ b/bundle/manifests/claw-operator.clusterserviceversion.yaml @@ -0,0 +1,351 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: |- + [ + { + "apiVersion": "claw.sandbox.redhat.com/v1alpha1", + "kind": "Claw", + "metadata": { + "name": "instance" + }, + "spec": { + "credentials": [ + { + "name": "gemini", + "provider": "google", + "secretRef": [ + { + "key": "api-key", + "name": "gemini-api-key" + } + ], + "type": "apiKey" + } + ] + } + }, + { + "apiVersion": "claw.sandbox.redhat.com/v1alpha1", + "kind": "ClawDevicePairingRequest", + "metadata": { + "name": "example-pairing" + }, + "spec": { + "requestID": "example-request-id", + "selector": { + "matchLabels": { + "app.kubernetes.io/name": "claw" + } + } + } + } + ] + capabilities: Basic Install + createdAt: "REPLACE_CREATED_AT" + operators.operatorframework.io/builder: operator-sdk-v1.42.0 + operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 + name: claw-operator.v0.0.0 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: ClawDevicePairingRequest is the Schema for the clawdevicepairingrequests + API + displayName: Claw Device Pairing Request + kind: ClawDevicePairingRequest + name: clawdevicepairingrequests.claw.sandbox.redhat.com + version: v1alpha1 + - description: Claw is the Schema for the claws API + displayName: Claw + kind: Claw + name: claws.claw.sandbox.redhat.com + version: v1alpha1 + description: |- + Kubernetes operator that manages OpenClaw instances on OpenShift/Kubernetes. + Handles deployment, credential injection via MITM proxy, LLM provider routing, + and messaging channel integrations. + displayName: Claw Operator + icon: + - base64data: "" + mediatype: image/png + install: + spec: + clusterPermissions: + - rules: + - apiGroups: + - "" + resources: + - configmaps + - persistentvolumeclaims + - secrets + - serviceaccounts + verbs: + - create + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - pods + verbs: + - list + - apiGroups: + - "" + resources: + - pods/exec + verbs: + - create + - apiGroups: + - "" + resources: + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - get + - list + - patch + - update + - watch + - apiGroups: + - claw.sandbox.redhat.com + resources: + - clawdevicepairingrequests + verbs: + - create + - get + - list + - patch + - update + - watch + - apiGroups: + - claw.sandbox.redhat.com + resources: + - clawdevicepairingrequests/finalizers + - claws/finalizers + verbs: + - update + - apiGroups: + - claw.sandbox.redhat.com + resources: + - clawdevicepairingrequests/status + - claws/status + verbs: + - get + - patch + - update + - apiGroups: + - claw.sandbox.redhat.com + resources: + - claws + verbs: + - get + - list + - watch + - apiGroups: + - networking.k8s.io + resources: + - networkpolicies + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterroles + - rolebindings + verbs: + - create + - get + - list + - patch + - update + - watch + - apiGroups: + - route.openshift.io + resources: + - routes + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - route.openshift.io + resources: + - routes/custom-host + verbs: + - create + - update + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create + serviceAccountName: claw-operator-controller-manager + deployments: + - label: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: claw-operator + control-plane: controller-manager + name: claw-operator-controller-manager + spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: claw-operator + control-plane: controller-manager + strategy: {} + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + app.kubernetes.io/name: claw-operator + control-plane: controller-manager + spec: + containers: + - args: + - --metrics-bind-address=:8443 + - --leader-elect + - --health-probe-bind-address=:8081 + command: + - /manager + env: + - name: PROXY_IMAGE + value: claw-proxy:latest + - name: KUBECTL_IMAGE + value: quay.io/openshift/origin-cli:4.21 + image: claw-operator:latest + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + serviceAccountName: claw-operator-controller-manager + terminationGracePeriodSeconds: 10 + permissions: + - rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + serviceAccountName: claw-operator-controller-manager + strategy: deployment + installModes: + - supported: true + type: OwnNamespace + - supported: true + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: false + type: AllNamespaces + keywords: + - ai + - llm + - openclaw + - developer-sandbox + links: + - name: Claw Operator + url: https://github.com/codeready-toolchain/claw-operator + maintainers: + - email: devsandbox@redhat.com + name: Developer Sandbox Team + maturity: alpha + minKubeVersion: 1.28.0 + provider: + name: Red Hat + url: https://developers.redhat.com + relatedImages: + - image: REPLACE_IMAGE + name: manager + - image: REPLACE_PROXY_IMAGE + name: proxy + - image: REPLACE_KUBECTL_IMAGE + name: kubectl + version: 0.0.0 diff --git a/bundle/manifests/claw.sandbox.redhat.com_clawdevicepairingrequests.yaml b/bundle/manifests/claw.sandbox.redhat.com_clawdevicepairingrequests.yaml new file mode 100644 index 0000000..cca8051 --- /dev/null +++ b/bundle/manifests/claw.sandbox.redhat.com_clawdevicepairingrequests.yaml @@ -0,0 +1,189 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + creationTimestamp: null + name: clawdevicepairingrequests.claw.sandbox.redhat.com +spec: + group: claw.sandbox.redhat.com + names: + kind: ClawDevicePairingRequest + listKind: ClawDevicePairingRequestList + plural: clawdevicepairingrequests + singular: clawdevicepairingrequest + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.requestID + name: RequestID + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ClawDevicePairingRequest is the Schema for the clawdevicepairingrequests + API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ClawDevicePairingRequestSpec defines the desired state of + ClawDevicePairingRequest + properties: + requestID: + description: RequestID is the unique identifier for this pairing request + minLength: 1 + type: string + selector: + description: Selector specifies which pod should process this device + pairing request + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + x-kubernetes-validations: + - message: selector must include at least one matchLabels or matchExpressions + entry + rule: (has(self.matchLabels) && size(self.matchLabels) > 0) || (has(self.matchExpressions) + && size(self.matchExpressions) > 0) + required: + - requestID + - selector + type: object + status: + description: ClawDevicePairingRequestStatus defines the observed state + of ClawDevicePairingRequest + properties: + conditions: + description: Conditions represent the latest available observations + of the ClawDevicePairingRequest's state + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/bundle/manifests/claw.sandbox.redhat.com_claws.yaml b/bundle/manifests/claw.sandbox.redhat.com_claws.yaml new file mode 100644 index 0000000..9dcd298 --- /dev/null +++ b/bundle/manifests/claw.sandbox.redhat.com_claws.yaml @@ -0,0 +1,413 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + creationTimestamp: null + name: claws.claw.sandbox.redhat.com +spec: + group: claw.sandbox.redhat.com + names: + kind: Claw + listKind: ClawList + plural: claws + singular: claw + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].reason + name: Reason + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: Claw is the Schema for the claws API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ClawSpec defines the desired state of Claw + properties: + configMode: + default: merge + description: |- + ConfigMode controls how operator config is applied on pod start. + "merge" (default) deep-merges operator settings into the existing + user config, preserving user-owned keys. "overwrite" fully replaces + the config on every pod start. + enum: + - merge + - overwrite + type: string + credentials: + description: Credentials configures proxy credential injection per + domain. + items: + description: CredentialSpec defines a single credential entry for + proxy injection. + properties: + allowedPaths: + description: |- + AllowedPaths restricts which URL paths the proxy permits for this domain. + Each entry is a path prefix (e.g., "/v1/api/"). If empty, all paths are allowed. + items: + type: string + type: array + apiKey: + description: APIKey configures custom header injection. Required + when type is "apiKey". + properties: + header: + description: Header name where the API key is injected (e.g., + "x-goog-api-key", "x-api-key") + minLength: 1 + type: string + valuePrefix: + description: |- + ValuePrefix is prepended to the secret value before injection. + Examples: "Bot " (Discord), "Basic " (pre-encoded basic auth). + type: string + required: + - header + type: object + channel: + description: |- + Channel declares this credential as a messaging channel integration. + When set, the operator enables the channel in OpenClaw's config and + infers proxy defaults (type, domain, injection config, companion routes). + Known values: telegram, discord, slack, whatsapp. + Mutually exclusive with provider. + type: string + channelConfig: + description: |- + ChannelConfig is opaque JSON deep-merged into the channel's config block + in operator.json. Use for channel-specific settings (dmPolicy, allowFrom, etc.). + type: object + x-kubernetes-preserve-unknown-fields: true + defaultHeaders: + additionalProperties: + type: string + description: |- + DefaultHeaders are injected on every proxied request for this credential, + in addition to the credential itself (e.g., "anthropic-version: 2023-06-01"). + type: object + domain: + description: |- + Domain the proxy matches against the request Host header. + Exact match: "api.github.com". Suffix match: ".googleapis.com" (leading dot). + Optional for known providers and channels — the operator infers the default domain. + type: string + gcp: + description: GCP configures GCP service account credential injection. + Required when type is "gcp". + properties: + location: + description: Location is the GCP region (e.g., us-central1) + minLength: 1 + type: string + project: + description: Project is the GCP project ID + minLength: 1 + type: string + required: + - location + - project + type: object + name: + description: Name uniquely identifies this credential entry. + minLength: 1 + type: string + oauth2: + description: OAuth2 configures client credentials token exchange. + Required when type is "oauth2". + properties: + clientID: + description: ClientID for the OAuth2 client credentials + flow + minLength: 1 + type: string + scopes: + description: Scopes requested during token exchange + items: + type: string + type: array + tokenURL: + description: TokenURL is the OAuth2 token endpoint + minLength: 1 + type: string + required: + - clientID + - tokenURL + type: object + pathToken: + description: PathToken configures URL path token injection. + Required when type is "pathToken". + properties: + prefix: + description: Prefix is prepended before the token in the + URL path (e.g., "/bot" for Telegram) + minLength: 1 + type: string + required: + - prefix + type: object + provider: + description: |- + Provider maps this credential to an OpenClaw LLM provider (e.g., "google", "anthropic", "openai", "openrouter"). + When set, the controller configures gateway routing and generates the provider entry in openclaw.json. + Mutually exclusive with channel. + type: string + secretRef: + description: |- + SecretRef references Kubernetes Secrets holding credential values. + For single-secret credentials, use a one-element array. + For multi-secret channels (e.g., Slack), use role to distinguish entries. + Not required for type "none" (proxy allowlist, no auth) or channels that use + non-secret auth (e.g., WhatsApp QR pairing). + items: + description: SecretRefEntry references a specific key in a + Secret. + properties: + key: + description: Key is the key in the Secret's data map + minLength: 1 + type: string + name: + description: Name is the name of the Secret + minLength: 1 + type: string + role: + description: |- + Role distinguishes multiple secrets for the same credential. + Required when multiple secretRef entries are present (e.g., Slack botToken/appToken). + maxLength: 63 + type: string + required: + - key + - name + type: object + type: array + type: + description: |- + Type selects the credential injection mechanism. + Optional when channel is set — the operator infers the type from the channel defaults. + enum: + - apiKey + - bearer + - gcp + - pathToken + - oauth2 + - none + - kubernetes + type: string + required: + - name + type: object + x-kubernetes-validations: + - message: either type or channel must be set + rule: has(self.type) || has(self.channel) + - message: provider and channel are mutually exclusive + rule: '!has(self.provider) || !has(self.channel)' + - message: secretRef is required unless type is none or channel + is set + rule: has(self.channel) || self.type == 'none' || has(self.secretRef) + - message: apiKey config is required when type is apiKey without + inferred defaults + rule: self.type != 'apiKey' || has(self.apiKey) || has(self.provider) + || has(self.channel) + - message: gcp config is required when type is gcp without inferred + defaults + rule: self.type != 'gcp' || has(self.gcp) || has(self.channel) + - message: pathToken config is required when type is pathToken without + inferred defaults + rule: self.type != 'pathToken' || has(self.pathToken) || has(self.channel) + - message: oauth2 config is required when type is oauth2 without + inferred defaults + rule: self.type != 'oauth2' || has(self.oauth2) || has(self.channel) + type: array + mcpServers: + additionalProperties: + description: McpServerSpec defines an MCP server the operator injects + into OpenClaw's config. + properties: + args: + description: Args are command-line arguments for the stdio server. + items: + type: string + type: array + command: + description: Command is the executable for a stdio MCP server. + type: string + env: + additionalProperties: + type: string + description: |- + Env are plain environment variables passed to the stdio server process + and written into the MCP server config in operator.json. + Use for non-secret values and tier-2 placeholder tokens. + type: object + envFrom: + description: |- + EnvFrom are secret-backed environment variables mounted on the gateway + container and inherited by the stdio server subprocess (tier 3). + Use only when the proxy-placeholder pattern (tier 2) is not viable. + items: + description: |- + McpEnvFromSecret maps a Kubernetes Secret key to an environment variable + on the gateway container for tier 3 MCP secret injection. + properties: + name: + description: Name is the environment variable name. + minLength: 1 + type: string + secretRef: + description: SecretRef references a key in a Kubernetes + Secret. + properties: + key: + description: Key is the key in the Secret's data map + minLength: 1 + type: string + name: + description: Name is the name of the Secret + minLength: 1 + type: string + role: + description: |- + Role distinguishes multiple secrets for the same credential. + Required when multiple secretRef entries are present (e.g., Slack botToken/appToken). + maxLength: 63 + type: string + required: + - key + - name + type: object + required: + - name + - secretRef + type: object + type: array + transport: + description: |- + Transport selects the HTTP transport type ("streamable-http" or "sse"). + Only valid when url is set. + type: string + url: + description: URL is the endpoint for an HTTP MCP server. + type: string + type: object + x-kubernetes-validations: + - message: either command (stdio) or url (HTTP) must be set + rule: has(self.command) || has(self.url) + - message: command and url are mutually exclusive + rule: '!has(self.command) || !has(self.url)' + - message: envFrom is only allowed for stdio MCP servers (command), + not HTTP (url) + rule: '!has(self.url) || !has(self.envFrom) || size(self.envFrom) + == 0' + description: |- + McpServers declares MCP servers injected into OpenClaw's config. + Map keys are server names as they appear in the mcp.servers config. + type: object + type: object + status: + description: ClawStatus defines the observed state of Claw + properties: + conditions: + description: Conditions represent the latest available observations + of the Claw's state + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + gatewayTokenSecretRef: + description: GatewayTokenSecretRef is the name of the Secret containing + the gateway authentication token + type: string + url: + description: URL is the HTTPS URL for accessing the Claw instance + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/bundle/metadata/annotations.yaml b/bundle/metadata/annotations.yaml new file mode 100644 index 0000000..4f7770e --- /dev/null +++ b/bundle/metadata/annotations.yaml @@ -0,0 +1,15 @@ +annotations: + # Core bundle annotations. + operators.operatorframework.io.bundle.mediatype.v1: registry+v1 + operators.operatorframework.io.bundle.manifests.v1: manifests/ + operators.operatorframework.io.bundle.metadata.v1: metadata/ + operators.operatorframework.io.bundle.package.v1: claw-operator + operators.operatorframework.io.bundle.channels.v1: staging + operators.operatorframework.io.bundle.channel.default.v1: staging + operators.operatorframework.io.metrics.builder: operator-sdk-v1.42.0 + operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 + operators.operatorframework.io.metrics.project_layout: go.kubebuilder.io/v4 + + # Annotations for testing. + operators.operatorframework.io.test.mediatype.v1: scorecard+v1 + operators.operatorframework.io.test.config.v1: tests/scorecard/ diff --git a/bundle/tests/scorecard/config.yaml b/bundle/tests/scorecard/config.yaml new file mode 100644 index 0000000..6a6e414 --- /dev/null +++ b/bundle/tests/scorecard/config.yaml @@ -0,0 +1,70 @@ +apiVersion: scorecard.operatorframework.io/v1alpha3 +kind: Configuration +metadata: + name: config +stages: +- parallel: true + tests: + - entrypoint: + - scorecard-test + - basic-check-spec + image: quay.io/operator-framework/scorecard-test:v1.42.0 + labels: + suite: basic + test: basic-check-spec-test + storage: + spec: + mountPath: {} + - entrypoint: + - scorecard-test + - olm-bundle-validation + image: quay.io/operator-framework/scorecard-test:v1.42.0 + labels: + suite: olm + test: olm-bundle-validation-test + storage: + spec: + mountPath: {} + - entrypoint: + - scorecard-test + - olm-crds-have-validation + image: quay.io/operator-framework/scorecard-test:v1.42.0 + labels: + suite: olm + test: olm-crds-have-validation-test + storage: + spec: + mountPath: {} + - entrypoint: + - scorecard-test + - olm-crds-have-resources + image: quay.io/operator-framework/scorecard-test:v1.42.0 + labels: + suite: olm + test: olm-crds-have-resources-test + storage: + spec: + mountPath: {} + - entrypoint: + - scorecard-test + - olm-spec-descriptors + image: quay.io/operator-framework/scorecard-test:v1.42.0 + labels: + suite: olm + test: olm-spec-descriptors-test + storage: + spec: + mountPath: {} + - entrypoint: + - scorecard-test + - olm-status-descriptors + image: quay.io/operator-framework/scorecard-test:v1.42.0 + labels: + suite: olm + test: olm-status-descriptors-test + storage: + spec: + mountPath: {} +storage: + spec: + mountPath: {} diff --git a/config/manifests/bases/claw-operator.clusterserviceversion.yaml b/config/manifests/bases/claw-operator.clusterserviceversion.yaml new file mode 100644 index 0000000..2177a55 --- /dev/null +++ b/config/manifests/bases/claw-operator.clusterserviceversion.yaml @@ -0,0 +1,71 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: '[]' + capabilities: Basic Install + createdAt: REPLACE_CREATED_AT + operators.operatorframework.io/builder: operator-sdk-v1.42.0 + operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 + name: claw-operator.v0.0.0 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: Claw is the Schema for the claws API + displayName: Claw + kind: Claw + name: claws.claw.sandbox.redhat.com + version: v1alpha1 + - description: ClawDevicePairingRequest is the Schema for the clawdevicepairingrequests + API + displayName: Claw Device Pairing Request + kind: ClawDevicePairingRequest + name: clawdevicepairingrequests.claw.sandbox.redhat.com + version: v1alpha1 + description: |- + Kubernetes operator that manages OpenClaw instances on OpenShift/Kubernetes. + Handles deployment, credential injection via MITM proxy, LLM provider routing, + and messaging channel integrations. + displayName: Claw Operator + icon: + - base64data: "" + mediatype: image/png + install: + spec: + deployments: [] + strategy: deployment + installModes: + - supported: true + type: OwnNamespace + - supported: true + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: false + type: AllNamespaces + keywords: + - ai + - llm + - openclaw + - developer-sandbox + links: + - name: Claw Operator + url: https://github.com/codeready-toolchain/claw-operator + maintainers: + - email: devsandbox@redhat.com + name: Developer Sandbox Team + maturity: alpha + minKubeVersion: 1.28.0 + provider: + name: Red Hat + url: https://developers.redhat.com + relatedImages: + - image: REPLACE_IMAGE + name: manager + - image: REPLACE_PROXY_IMAGE + name: proxy + - image: REPLACE_KUBECTL_IMAGE + name: kubectl + version: 0.0.0 diff --git a/config/manifests/kustomization.yaml b/config/manifests/kustomization.yaml index 48d39fb..ab76ecd 100644 --- a/config/manifests/kustomization.yaml +++ b/config/manifests/kustomization.yaml @@ -3,6 +3,7 @@ resources: - bases/claw-operator.clusterserviceversion.yaml - ../default +- ../crd - ../samples - ../scorecard diff --git a/config/samples/claw_v1alpha1_clawdevicepairingrequest.yaml b/config/samples/claw_v1alpha1_clawdevicepairingrequest.yaml new file mode 100644 index 0000000..6235dbc --- /dev/null +++ b/config/samples/claw_v1alpha1_clawdevicepairingrequest.yaml @@ -0,0 +1,9 @@ +apiVersion: claw.sandbox.redhat.com/v1alpha1 +kind: ClawDevicePairingRequest +metadata: + name: example-pairing +spec: + requestID: example-request-id + selector: + matchLabels: + app.kubernetes.io/name: claw diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 48f483f..56cf34e 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -2,3 +2,4 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - claw_v1alpha1_claw.yaml +- claw_v1alpha1_clawdevicepairingrequest.yaml diff --git a/docs/proposals/olm-deployment-design.md b/docs/proposals/olm-deployment-design.md new file mode 100644 index 0000000..ea3b3e5 --- /dev/null +++ b/docs/proposals/olm-deployment-design.md @@ -0,0 +1,233 @@ +# OLM Deployment Model + +**Status:** Final +**Decisions:** [olm-deployment-questions.md](olm-deployment-questions.md) + +## Overview + +The claw-operator currently deploys via raw Kustomize (`make deploy` / `make dev-deploy`) with manually-managed image references. This design migrates the production deployment model to OLM (Operator Lifecycle Manager), aligning with how the codeready-toolchain host-operator and member-operator are managed. + +The migration adds: + +- A ClusterServiceVersion (CSV) base for the operator +- A `make bundle` target using `operator-sdk generate bundle` +- Bundle and catalog image build/push pipeline via self-contained Makefile targets +- CI integration: static bundle validation on PRs, full CD pipeline on master push +- `relatedImages` support for all operator-managed images (manager, proxy, kubectl) +- Commit-count-based versioning with `staging` and `alpha` channels + +## Design Principles + +- **Toolchain alignment** — follow the same OLM patterns used by host-operator and member-operator so that all codeready-toolchain operators are managed consistently +- **Three-image operator** — the claw-operator manages three images (manager, proxy, kubectl); the CSV declares all three via `relatedImages` and the Deployment env vars `PROXY_IMAGE` and `KUBECTL_IMAGE` +- **Dev escape hatch** — retain `make dev-deploy` for rapid local iteration without OLM; OLM is the production deployment path +- **Self-contained** — all CD logic lives in the Makefile (no external toolchain-cicd dependency), tailored to the single-repo three-image model +- **Incremental adoption** — phase the work so that bundle generation and local validation come first, followed by CI/CD automation + +## Architecture + +### Current Deployment Flow + +```mermaid +flowchart LR + dev["Developer"] + dev -->|"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)._ From 8520883b9fcda54ae487b71125be5c4a978f0cd4 Mon Sep 17 00:00:00 2001 From: MikelAlejoBR Date: Thu, 14 May 2026 11:06:29 -0400 Subject: [PATCH 02/14] refactor: pin Kind to a version Pinning the Kind version will make the CD pipeline reproducible. Assisted-by: Cursor with Opus 4.6 --- .github/workflows/cd.yml | 8 +++----- Makefile | 15 +++++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 5b49e21..c59753b 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -44,10 +44,8 @@ jobs: continue-on-error: true id: kind run: | - curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64 - chmod +x ./kind - sudo mv ./kind /usr/local/bin/kind - kind create cluster --name scorecard + make kind + bin/kind create cluster --name scorecard - name: Run scorecard tests if: steps.kind.outcome == 'success' @@ -60,4 +58,4 @@ jobs: - name: Cleanup Kind cluster if: always() && steps.kind.outcome == 'success' - run: kind delete cluster --name scorecard + run: bin/kind delete cluster --name scorecard diff --git a/Makefile b/Makefile index 586b149..7f15088 100644 --- a/Makefile +++ b/Makefile @@ -80,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." ;; \ @@ -408,7 +404,7 @@ $(LOCALBIN): ## Tool Binaries KUBECTL ?= kubectl -KIND ?= kind +KIND ?= $(LOCALBIN)/kind KUSTOMIZE ?= $(LOCALBIN)/kustomize CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen ENVTEST ?= $(LOCALBIN)/setup-envtest @@ -426,6 +422,7 @@ ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk - 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. @@ -467,6 +464,12 @@ $(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 From 3d7e03d49edc4fa3da71f163f681cccba287d3d3 Mon Sep 17 00:00:00 2001 From: MikelAlejoBR Date: Thu, 14 May 2026 11:51:32 -0400 Subject: [PATCH 03/14] refactor: reduce the RBAC privileges There is no need to have permissions granted via cluster permissions for many of the resources. Moving the ones that do not require those broad permissions to the "permissions" hardens security. Assited-by: Cursor with Opus 4.6 Jira ticket: SANDBOX-1843 --- .../claw-operator.clusterserviceversion.yaml | 236 +++++++++--------- config/rbac/kustomization.yaml | 1 + config/rbac/manager_role_binding.yaml | 15 ++ config/rbac/role.yaml | 19 +- .../claw_device_pairing_request_controller.go | 10 +- .../controller/claw_resource_controller.go | 26 +- 6 files changed, 175 insertions(+), 132 deletions(-) create mode 100644 config/rbac/manager_role_binding.yaml diff --git a/bundle/manifests/claw-operator.clusterserviceversion.yaml b/bundle/manifests/claw-operator.clusterserviceversion.yaml index 080ee68..5470755 100644 --- a/bundle/manifests/claw-operator.clusterserviceversion.yaml +++ b/bundle/manifests/claw-operator.clusterserviceversion.yaml @@ -75,6 +75,129 @@ spec: spec: clusterPermissions: - rules: + - apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterroles + verbs: + - create + - get + - list + - patch + - update + - watch + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create + serviceAccountName: claw-operator-controller-manager + deployments: + - label: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: claw-operator + control-plane: controller-manager + name: claw-operator-controller-manager + spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: claw-operator + control-plane: controller-manager + strategy: {} + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + app.kubernetes.io/name: claw-operator + control-plane: controller-manager + spec: + containers: + - args: + - --metrics-bind-address=:8443 + - --leader-elect + - --health-probe-bind-address=:8081 + command: + - /manager + env: + - name: PROXY_IMAGE + value: claw-proxy:latest + - name: KUBECTL_IMAGE + value: quay.io/openshift/origin-cli:4.21 + image: claw-operator:latest + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + serviceAccountName: claw-operator-controller-manager + terminationGracePeriodSeconds: 10 + permissions: + - rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch - apiGroups: - "" resources: @@ -174,7 +297,6 @@ spec: - apiGroups: - rbac.authorization.k8s.io resources: - - clusterroles - rolebindings verbs: - create @@ -202,118 +324,6 @@ spec: verbs: - create - update - - apiGroups: - - authentication.k8s.io - resources: - - tokenreviews - verbs: - - create - - apiGroups: - - authorization.k8s.io - resources: - - subjectaccessreviews - verbs: - - create - serviceAccountName: claw-operator-controller-manager - deployments: - - label: - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: claw-operator - control-plane: controller-manager - name: claw-operator-controller-manager - spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: claw-operator - control-plane: controller-manager - strategy: {} - template: - metadata: - annotations: - kubectl.kubernetes.io/default-container: manager - labels: - app.kubernetes.io/name: claw-operator - control-plane: controller-manager - spec: - containers: - - args: - - --metrics-bind-address=:8443 - - --leader-elect - - --health-probe-bind-address=:8081 - command: - - /manager - env: - - name: PROXY_IMAGE - value: claw-proxy:latest - - name: KUBECTL_IMAGE - value: quay.io/openshift/origin-cli:4.21 - image: claw-operator:latest - imagePullPolicy: IfNotPresent - livenessProbe: - httpGet: - path: /healthz - port: 8081 - initialDelaySeconds: 15 - periodSeconds: 20 - name: manager - readinessProbe: - httpGet: - path: /readyz - port: 8081 - initialDelaySeconds: 5 - periodSeconds: 10 - resources: - limits: - cpu: 500m - memory: 128Mi - requests: - cpu: 10m - memory: 64Mi - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - securityContext: - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault - serviceAccountName: claw-operator-controller-manager - terminationGracePeriodSeconds: 10 - permissions: - - rules: - - apiGroups: - - "" - resources: - - configmaps - verbs: - - get - - list - - watch - - create - - update - - patch - - delete - - apiGroups: - - coordination.k8s.io - resources: - - leases - verbs: - - get - - list - - watch - - create - - update - - patch - - delete - - apiGroups: - - "" - resources: - - events - verbs: - - create - - patch serviceAccountName: claw-operator-controller-manager strategy: deployment installModes: diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index 5619aa0..1763afb 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -7,6 +7,7 @@ resources: - service_account.yaml - role.yaml - role_binding.yaml +- manager_role_binding.yaml - leader_election_role.yaml - leader_election_role_binding.yaml # The following RBAC configurations are used to protect diff --git a/config/rbac/manager_role_binding.yaml b/config/rbac/manager_role_binding.yaml new file mode 100644 index 0000000..873342b --- /dev/null +++ b/config/rbac/manager_role_binding.yaml @@ -0,0 +1,15 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: claw-operator + app.kubernetes.io/managed-by: kustomize + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: manager-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 9b717dd..14aa1b0 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -4,6 +4,24 @@ kind: ClusterRole metadata: name: manager-role rules: +- apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterroles + verbs: + - create + - get + - list + - patch + - update + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: manager-role + namespace: system +rules: - apiGroups: - "" resources: @@ -103,7 +121,6 @@ rules: - apiGroups: - rbac.authorization.k8s.io resources: - - clusterroles - rolebindings verbs: - create diff --git a/internal/controller/claw_device_pairing_request_controller.go b/internal/controller/claw_device_pairing_request_controller.go index 61f77b8..f69168b 100644 --- a/internal/controller/claw_device_pairing_request_controller.go +++ b/internal/controller/claw_device_pairing_request_controller.go @@ -59,11 +59,11 @@ type ClawDevicePairingRequestReconciler struct { ExecFn PodExecFunc } -// +kubebuilder:rbac:groups=claw.sandbox.redhat.com,resources=clawdevicepairingrequests,verbs=get;list;watch;create;update;patch -// +kubebuilder:rbac:groups=claw.sandbox.redhat.com,resources=clawdevicepairingrequests/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=claw.sandbox.redhat.com,resources=clawdevicepairingrequests/finalizers,verbs=update -// +kubebuilder:rbac:groups="",resources=pods,verbs=list -// +kubebuilder:rbac:groups="",resources=pods/exec,verbs=create +// +kubebuilder:rbac:groups=claw.sandbox.redhat.com,namespace=system,resources=clawdevicepairingrequests,verbs=get;list;watch;create;update;patch +// +kubebuilder:rbac:groups=claw.sandbox.redhat.com,namespace=system,resources=clawdevicepairingrequests/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=claw.sandbox.redhat.com,namespace=system,resources=clawdevicepairingrequests/finalizers,verbs=update +// +kubebuilder:rbac:groups="",namespace=system,resources=pods,verbs=list +// +kubebuilder:rbac:groups="",namespace=system,resources=pods/exec,verbs=create // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. diff --git a/internal/controller/claw_resource_controller.go b/internal/controller/claw_resource_controller.go index 201d72f..c129546 100644 --- a/internal/controller/claw_resource_controller.go +++ b/internal/controller/claw_resource_controller.go @@ -373,20 +373,20 @@ type ClawResourceReconciler struct { ImagePullPolicy string } -// +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 -// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch -// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch -// +kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get;list;watch;create;update;patch -// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch -// +kubebuilder:rbac:groups=networking.k8s.io,resources=networkpolicies,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch;create;update;patch -// +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 +// +kubebuilder:rbac:groups=claw.sandbox.redhat.com,namespace=system,resources=claws,verbs=get;list;watch +// +kubebuilder:rbac:groups=claw.sandbox.redhat.com,namespace=system,resources=claws/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=claw.sandbox.redhat.com,namespace=system,resources=claws/finalizers,verbs=update +// +kubebuilder:rbac:groups="",namespace=system,resources=configmaps,verbs=get;list;watch;create;update;patch +// +kubebuilder:rbac:groups="",namespace=system,resources=secrets,verbs=get;list;watch;create;update;patch +// +kubebuilder:rbac:groups="",namespace=system,resources=persistentvolumeclaims,verbs=get;list;watch;create;update;patch +// +kubebuilder:rbac:groups=apps,namespace=system,resources=deployments,verbs=get;list;watch;create;update;patch +// +kubebuilder:rbac:groups=networking.k8s.io,namespace=system,resources=networkpolicies,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="",namespace=system,resources=serviceaccounts,verbs=get;list;watch;create;update;patch +// +kubebuilder:rbac:groups="",namespace=system,resources=services,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=route.openshift.io,namespace=system,resources=routes,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=route.openshift.io,namespace=system,resources=routes/custom-host,verbs=create;update +// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,namespace=system,resources=rolebindings,verbs=get;list;watch;create;update;patch // Reconcile manages the complete lifecycle of resources for Claw instances func (r *ClawResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { From dfd2de164971972b88511063bef794d6f5f74184 Mon Sep 17 00:00:00 2001 From: MikelAlejoBR Date: Thu, 14 May 2026 12:42:10 -0400 Subject: [PATCH 04/14] refactor: add versioning to the CSV install spec Instead of relying on the "latest" tag, we should be using versioning to build the operators, so that they are easily pullable. Assisted-by: Cursor with OPus 4.6 Jira ticket: SANDBOX-1843 --- Makefile | 3 +++ bundle/manifests/claw-operator.clusterserviceversion.yaml | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 7f15088..d5ad28b 100644 --- a/Makefile +++ b/Makefile @@ -318,6 +318,9 @@ bundle: manifests kustomize operator-sdk ## Generate OLM bundle manifests and va 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 diff --git a/bundle/manifests/claw-operator.clusterserviceversion.yaml b/bundle/manifests/claw-operator.clusterserviceversion.yaml index 5470755..82e14be 100644 --- a/bundle/manifests/claw-operator.clusterserviceversion.yaml +++ b/bundle/manifests/claw-operator.clusterserviceversion.yaml @@ -129,10 +129,10 @@ spec: - /manager env: - name: PROXY_IMAGE - value: claw-proxy:latest + value: REPLACE_PROXY_IMAGE - name: KUBECTL_IMAGE - value: quay.io/openshift/origin-cli:4.21 - image: claw-operator:latest + value: REPLACE_KUBECTL_IMAGE + image: REPLACE_IMAGE imagePullPolicy: IfNotPresent livenessProbe: httpGet: From 6aae9d030bf79c9a320147a667733bf1f9f3221c Mon Sep 17 00:00:00 2001 From: MikelAlejoBR Date: Thu, 14 May 2026 14:04:29 -0400 Subject: [PATCH 05/14] refactor: restrict MCP transport's values Instead of using a free form string for the MCP servers' transport, it's better to restrict it at the CRD level to avoid silent invalid configurations. Assisted-by: Cursor with Opus 4.6 Jira ticket: SANDBOX-1843 --- api/v1alpha1/claw_types.go | 11 ++++++++++- config/crd/bases/claw.sandbox.redhat.com_claws.yaml | 3 +++ internal/controller/claw_mcp.go | 2 +- internal/controller/claw_mcp_test.go | 10 +++++----- internal/controller/claw_proxy_test.go | 2 +- 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/api/v1alpha1/claw_types.go b/api/v1alpha1/claw_types.go index d4dede8..7d8b7ff 100644 --- a/api/v1alpha1/claw_types.go +++ b/api/v1alpha1/claw_types.go @@ -45,6 +45,15 @@ const ( ConfigModeOverwrite ConfigMode = "overwrite" ) +// McpTransport selects the HTTP transport type for remote MCP servers. +// +kubebuilder:validation:Enum=streamable-http;sse +type McpTransport string + +const ( + McpTransportStreamableHTTP McpTransport = "streamable-http" + McpTransportSSE McpTransport = "sse" +) + // Condition types for Claw status. const ( ConditionTypeReady = "Ready" @@ -237,7 +246,7 @@ type McpServerSpec struct { // Transport selects the HTTP transport type ("streamable-http" or "sse"). // Only valid when url is set. // +optional - Transport string `json:"transport,omitempty"` + Transport McpTransport `json:"transport,omitempty"` // Env are plain environment variables passed to the stdio server process // and written into the MCP server config in operator.json. diff --git a/config/crd/bases/claw.sandbox.redhat.com_claws.yaml b/config/crd/bases/claw.sandbox.redhat.com_claws.yaml index 24dd13d..314672f 100644 --- a/config/crd/bases/claw.sandbox.redhat.com_claws.yaml +++ b/config/crd/bases/claw.sandbox.redhat.com_claws.yaml @@ -309,6 +309,9 @@ spec: description: |- Transport selects the HTTP transport type ("streamable-http" or "sse"). Only valid when url is set. + enum: + - streamable-http + - sse type: string url: description: URL is the endpoint for an HTTP MCP server. 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..3cd9963 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, }, }, }, diff --git a/internal/controller/claw_proxy_test.go b/internal/controller/claw_proxy_test.go index e73d545..3db21db 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) From 1fd4a1acc92fa78dab08d27c96e6715f7841726e Mon Sep 17 00:00:00 2001 From: MikelAlejoBR Date: Thu, 14 May 2026 14:11:30 -0400 Subject: [PATCH 06/14] refactor: use skip range instead of relying on previous SHA The "skipRange" directive already takes care of specifying the version range for the bundles to be upgraded. Assisted-by: Cursor with Opus 4.6 Jira ticket: SANDBOX-1843 --- Makefile | 3 --- 1 file changed, 3 deletions(-) diff --git a/Makefile b/Makefile index d5ad28b..c4bb0cf 100644 --- a/Makefile +++ b/Makefile @@ -349,9 +349,7 @@ 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) -GIT_PREVIOUS_SHORT_SHA = $(shell git rev-parse --short HEAD~1) CD_VERSION = 0.0.$(GIT_COMMIT_COUNT)-commit-$(GIT_SHORT_SHA) -CD_PREVIOUS_VERSION = 0.0.$(shell expr $(GIT_COMMIT_COUNT) - 1)-commit-$(GIT_PREVIOUS_SHORT_SHA) .PHONY: push-to-quay-staging push-to-quay-staging: generate-cd-release-manifests ## Build and push bundle + catalog images for staging channel. @@ -371,7 +369,6 @@ generate-cd-release-manifests: opm ## Generate bundle with CD version, images, a 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 's|^ version: \(.*\)| replaces: claw-operator.v$(CD_PREVIOUS_VERSION)\n version: \1|' $(BUNDLE_CSV) sed -i '/^ createdAt:/a\ olm.skipRange: ">=0.0.0 <$(CD_VERSION)"' $(BUNDLE_CSV) .PHONY: build-and-push-catalog From 190486e9818b9f4e8167994788a8df9019fcf5fe Mon Sep 17 00:00:00 2001 From: MikelAlejoBR Date: Thu, 14 May 2026 14:43:18 -0400 Subject: [PATCH 07/14] refactor: curl silencing errors curl was saving errors pages as binaries instead of failing on HTTP errors. Assisted-by: Cursor with Opus 4.6 Jira ticket: SANDBOX-1843 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index c4bb0cf..2880d63 100644 --- a/Makefile +++ b/Makefile @@ -478,7 +478,7 @@ define download-tool @[ -f "$(1)-$(2)" ] || { \ set -e; \ echo "Downloading $(1) $(2)"; \ -curl -sSLo $(1)-$(2) $(3); \ +curl --silent --show-error --location --fail --retry 3 --output $(1)-$(2) $(3); \ chmod +x $(1)-$(2); \ }; \ ln -sf $(1)-$(2) $(1) From 431131d2d3f168299c1910b88211c4a27330d0e6 Mon Sep 17 00:00:00 2001 From: MikelAlejoBR Date: Thu, 14 May 2026 15:06:41 -0400 Subject: [PATCH 08/14] refactor: remove "system" namespace restriction The RBAC permissions were being too restrictive in the sense that the operator would not be able to see individual namespaces' resources when reconciling. Assisted-by: Cursor with Opus 4.6 Jira ticket: SANDBOX-1843 --- config/rbac/role.yaml | 64 +++++++++++-------- .../claw_device_pairing_request_controller.go | 10 +-- 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 14aa1b0..5a5827f 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -4,6 +4,43 @@ kind: ClusterRole metadata: name: manager-role rules: +- apiGroups: + - "" + resources: + - pods + verbs: + - list +- apiGroups: + - "" + resources: + - pods/exec + verbs: + - create +- apiGroups: + - claw.sandbox.redhat.com + resources: + - clawdevicepairingrequests + verbs: + - create + - get + - list + - patch + - update + - watch +- apiGroups: + - claw.sandbox.redhat.com + resources: + - clawdevicepairingrequests/finalizers + verbs: + - update +- apiGroups: + - claw.sandbox.redhat.com + resources: + - clawdevicepairingrequests/status + verbs: + - get + - patch + - update - apiGroups: - rbac.authorization.k8s.io resources: @@ -36,18 +73,6 @@ rules: - patch - update - watch -- apiGroups: - - "" - resources: - - pods - verbs: - - list -- apiGroups: - - "" - resources: - - pods/exec - verbs: - - create - apiGroups: - "" resources: @@ -74,38 +99,25 @@ rules: - apiGroups: - claw.sandbox.redhat.com resources: - - clawdevicepairingrequests + - claws verbs: - - create - get - list - - patch - - update - watch - apiGroups: - claw.sandbox.redhat.com resources: - - clawdevicepairingrequests/finalizers - claws/finalizers verbs: - update - apiGroups: - claw.sandbox.redhat.com resources: - - clawdevicepairingrequests/status - claws/status verbs: - get - patch - update -- apiGroups: - - claw.sandbox.redhat.com - resources: - - claws - verbs: - - get - - list - - watch - apiGroups: - networking.k8s.io resources: diff --git a/internal/controller/claw_device_pairing_request_controller.go b/internal/controller/claw_device_pairing_request_controller.go index f69168b..61f77b8 100644 --- a/internal/controller/claw_device_pairing_request_controller.go +++ b/internal/controller/claw_device_pairing_request_controller.go @@ -59,11 +59,11 @@ type ClawDevicePairingRequestReconciler struct { ExecFn PodExecFunc } -// +kubebuilder:rbac:groups=claw.sandbox.redhat.com,namespace=system,resources=clawdevicepairingrequests,verbs=get;list;watch;create;update;patch -// +kubebuilder:rbac:groups=claw.sandbox.redhat.com,namespace=system,resources=clawdevicepairingrequests/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=claw.sandbox.redhat.com,namespace=system,resources=clawdevicepairingrequests/finalizers,verbs=update -// +kubebuilder:rbac:groups="",namespace=system,resources=pods,verbs=list -// +kubebuilder:rbac:groups="",namespace=system,resources=pods/exec,verbs=create +// +kubebuilder:rbac:groups=claw.sandbox.redhat.com,resources=clawdevicepairingrequests,verbs=get;list;watch;create;update;patch +// +kubebuilder:rbac:groups=claw.sandbox.redhat.com,resources=clawdevicepairingrequests/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=claw.sandbox.redhat.com,resources=clawdevicepairingrequests/finalizers,verbs=update +// +kubebuilder:rbac:groups="",resources=pods,verbs=list +// +kubebuilder:rbac:groups="",resources=pods/exec,verbs=create // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. From 40b5966d78b780287f0aacbdd891c0290e2c5c9a Mon Sep 17 00:00:00 2001 From: MikelAlejoBR Date: Thu, 14 May 2026 15:16:13 -0400 Subject: [PATCH 09/14] refactor: restrict transport to be set only when url is set The "Transport" field for the MCP Servers is only accepted when the "URL" field is set too, so these changes enforce that condition. Assisted-by: Cursor with Opus 4.6 Jira ticket: SANDBOX-1843 --- api/v1alpha1/claw_types.go | 1 + .../bases/claw.sandbox.redhat.com_claws.yaml | 2 ++ internal/controller/claw_mcp_test.go | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/api/v1alpha1/claw_types.go b/api/v1alpha1/claw_types.go index 7d8b7ff..a5adf2b 100644 --- a/api/v1alpha1/claw_types.go +++ b/api/v1alpha1/claw_types.go @@ -230,6 +230,7 @@ type CredentialSpec struct { // +kubebuilder:validation:XValidation:rule="has(self.command) || has(self.url)",message="either command (stdio) or url (HTTP) must be set" // +kubebuilder:validation:XValidation:rule="!has(self.command) || !has(self.url)",message="command and url are mutually exclusive" // +kubebuilder:validation:XValidation:rule="!has(self.url) || !has(self.envFrom) || size(self.envFrom) == 0",message="envFrom is only allowed for stdio MCP servers (command), not HTTP (url)" +// +kubebuilder:validation:XValidation:rule="!has(self.transport) || has(self.url)",message="transport is only allowed for HTTP MCP servers (url)" type McpServerSpec struct { // Command is the executable for a stdio MCP server. // +optional diff --git a/config/crd/bases/claw.sandbox.redhat.com_claws.yaml b/config/crd/bases/claw.sandbox.redhat.com_claws.yaml index 314672f..265d132 100644 --- a/config/crd/bases/claw.sandbox.redhat.com_claws.yaml +++ b/config/crd/bases/claw.sandbox.redhat.com_claws.yaml @@ -326,6 +326,8 @@ spec: not HTTP (url) rule: '!has(self.url) || !has(self.envFrom) || size(self.envFrom) == 0' + - message: transport is only allowed for HTTP MCP servers (url) + rule: '!has(self.transport) || has(self.url)' description: |- McpServers declares MCP servers injected into OpenClaw's config. Map keys are server names as they appear in the mcp.servers config. diff --git a/internal/controller/claw_mcp_test.go b/internal/controller/claw_mcp_test.go index 3cd9963..58f6877 100644 --- a/internal/controller/claw_mcp_test.go +++ b/internal/controller/claw_mcp_test.go @@ -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)") + }) } From bbdc6c7f58e8856784c28b813d25f94336a870a9 Mon Sep 17 00:00:00 2001 From: MikelAlejoBR Date: Thu, 14 May 2026 15:29:49 -0400 Subject: [PATCH 10/14] refactor: rely on shell parameter expnasion to get the tag Instead of using "cut" which might grab the wrong tags in images with colons in them, we want to use shell parameter expansions to always get the text that comes after the last colon. Assisted-by: Cursor with Opus 4.6 Jira ticket: SANDBOX-1843 --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 2880d63..8105857 100644 --- a/Makefile +++ b/Makefile @@ -179,7 +179,7 @@ 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 @@ -189,8 +189,8 @@ endef # Usage: $(call generate-bundle-overlay,) define generate-bundle-overlay @rm -rf config/.bundle && mkdir -p config/.bundle - @printf 'apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nresources:\n- ../manifests\nimages:\n- name: controller\n newName: %s\n newTag: %s\n' \ - "$$(echo $(1) | cut -d: -f1)" "$$(echo $(1) | cut -d: -f2)" > config/.bundle/kustomization.yaml + @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 From cd0b0c2107f51ea88328b99700cc4a89da3cc47a Mon Sep 17 00:00:00 2001 From: MikelAlejoBR Date: Thu, 14 May 2026 19:22:19 -0400 Subject: [PATCH 11/14] docs: instructions on how to install the operator Assited-by: Cursor with Opus 4.6 Jira ticket: SANDBOX-1843 --- README.md | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) 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 - < Date: Thu, 14 May 2026 19:36:35 -0400 Subject: [PATCH 12/14] fix: align BUNDLE_IMG with CD_VERSION in publish-current-bundle target Assisted-by: Cursor with Opus 4.6 Jira ticket: SANDBOX-1843 --- Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 8105857..67fec10 100644 --- a/Makefile +++ b/Makefile @@ -380,17 +380,17 @@ build-and-push-catalog: opm ## Validate, build, and push FBC catalog image from rm -rf catalog/ .PHONY: publish-current-bundle -publish-current-bundle: opm ## One-shot publish for testing OLM install (alpha channel, no replaces). +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 - $(MAKE) bundle-push + $(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_IMG) -o yaml > catalog/claw-operator/bundle.yaml + $(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 From 9429c9155aa31f3fcbe25631abd5318b3f4fb2f8 Mon Sep 17 00:00:00 2001 From: MikelAlejoBR Date: Fri, 15 May 2026 09:54:53 -0400 Subject: [PATCH 13/14] fix: E2E tests fail due to improper permissions Assisted-by: Cursor with Opus 4.6 Jira ticket: SANDBOX-1843 --- config/rbac/kustomization.yaml | 1 - config/rbac/manager_role_binding.yaml | 15 ---- config/rbac/role.yaml | 73 ++++++------------- .../controller/claw_resource_controller.go | 26 +++---- 4 files changed, 35 insertions(+), 80 deletions(-) delete mode 100644 config/rbac/manager_role_binding.yaml diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index 1763afb..5619aa0 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -7,7 +7,6 @@ resources: - service_account.yaml - role.yaml - role_binding.yaml -- manager_role_binding.yaml - leader_election_role.yaml - leader_election_role_binding.yaml # The following RBAC configurations are used to protect diff --git a/config/rbac/manager_role_binding.yaml b/config/rbac/manager_role_binding.yaml deleted file mode 100644 index 873342b..0000000 --- a/config/rbac/manager_role_binding.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - labels: - app.kubernetes.io/name: claw-operator - app.kubernetes.io/managed-by: kustomize - name: manager-rolebinding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: manager-role -subjects: -- kind: ServiceAccount - name: controller-manager - namespace: system diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 5a5827f..9b717dd 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -7,19 +7,10 @@ rules: - apiGroups: - "" resources: - - pods - verbs: - - list -- apiGroups: - - "" - resources: - - pods/exec - verbs: - - create -- apiGroups: - - claw.sandbox.redhat.com - resources: - - clawdevicepairingrequests + - configmaps + - persistentvolumeclaims + - secrets + - serviceaccounts verbs: - create - get @@ -28,51 +19,17 @@ rules: - update - watch - apiGroups: - - claw.sandbox.redhat.com - resources: - - clawdevicepairingrequests/finalizers - verbs: - - update -- apiGroups: - - claw.sandbox.redhat.com - resources: - - clawdevicepairingrequests/status - verbs: - - get - - patch - - update -- apiGroups: - - rbac.authorization.k8s.io + - "" resources: - - clusterroles + - pods verbs: - - create - - get - list - - patch - - update - - watch ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: manager-role - namespace: system -rules: - apiGroups: - "" resources: - - configmaps - - persistentvolumeclaims - - secrets - - serviceaccounts + - pods/exec verbs: - create - - get - - list - - patch - - update - - watch - apiGroups: - "" resources: @@ -99,25 +56,38 @@ rules: - apiGroups: - claw.sandbox.redhat.com resources: - - claws + - clawdevicepairingrequests verbs: + - create - get - list + - patch + - update - watch - apiGroups: - claw.sandbox.redhat.com resources: + - clawdevicepairingrequests/finalizers - claws/finalizers verbs: - update - apiGroups: - claw.sandbox.redhat.com resources: + - clawdevicepairingrequests/status - claws/status verbs: - get - patch - update +- apiGroups: + - claw.sandbox.redhat.com + resources: + - claws + verbs: + - get + - list + - watch - apiGroups: - networking.k8s.io resources: @@ -133,6 +103,7 @@ rules: - apiGroups: - rbac.authorization.k8s.io resources: + - clusterroles - rolebindings verbs: - create diff --git a/internal/controller/claw_resource_controller.go b/internal/controller/claw_resource_controller.go index 9efa6b0..545b7b7 100644 --- a/internal/controller/claw_resource_controller.go +++ b/internal/controller/claw_resource_controller.go @@ -374,19 +374,19 @@ type ClawResourceReconciler struct { } // +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles,verbs=get;list;watch;create;update;patch -// +kubebuilder:rbac:groups=claw.sandbox.redhat.com,namespace=system,resources=claws,verbs=get;list;watch -// +kubebuilder:rbac:groups=claw.sandbox.redhat.com,namespace=system,resources=claws/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=claw.sandbox.redhat.com,namespace=system,resources=claws/finalizers,verbs=update -// +kubebuilder:rbac:groups="",namespace=system,resources=configmaps,verbs=get;list;watch;create;update;patch -// +kubebuilder:rbac:groups="",namespace=system,resources=secrets,verbs=get;list;watch;create;update;patch -// +kubebuilder:rbac:groups="",namespace=system,resources=persistentvolumeclaims,verbs=get;list;watch;create;update;patch -// +kubebuilder:rbac:groups=apps,namespace=system,resources=deployments,verbs=get;list;watch;create;update;patch -// +kubebuilder:rbac:groups=networking.k8s.io,namespace=system,resources=networkpolicies,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups="",namespace=system,resources=serviceaccounts,verbs=get;list;watch;create;update;patch -// +kubebuilder:rbac:groups="",namespace=system,resources=services,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=route.openshift.io,namespace=system,resources=routes,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=route.openshift.io,namespace=system,resources=routes/custom-host,verbs=create;update -// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,namespace=system,resources=rolebindings,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 +// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch +// +kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get;list;watch;create;update;patch +// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch +// +kubebuilder:rbac:groups=networking.k8s.io,resources=networkpolicies,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch;create;update;patch +// +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=rolebindings,verbs=get;list;watch;create;update;patch // Reconcile manages the complete lifecycle of resources for Claw instances func (r *ClawResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { //nolint:gocyclo From f4c82244d8b1af0bae616860e371676dd29353c6 Mon Sep 17 00:00:00 2001 From: MikelAlejoBR Date: Fri, 15 May 2026 11:33:44 -0400 Subject: [PATCH 14/14] refactor: ignore the "bundle/" directory The bundle directory gets generated automatically, so it is not needed in the git repository. Assisted-by: Cursor with Opus 4.6 Jira ticket: SANDBOX-1843 --- .github/workflows/lint-bundle.yml | 3 - .gitignore | 3 + ...er-manager-metrics-service_v1_service.yaml | 20 - ...c.authorization.k8s.io_v1_clusterrole.yaml | 10 - .../claw-operator.clusterserviceversion.yaml | 361 --------------- ....redhat.com_clawdevicepairingrequests.yaml | 189 -------- .../claw.sandbox.redhat.com_claws.yaml | 413 ------------------ bundle/metadata/annotations.yaml | 15 - bundle/tests/scorecard/config.yaml | 70 --- 9 files changed, 3 insertions(+), 1081 deletions(-) delete mode 100644 bundle/manifests/claw-operator-controller-manager-metrics-service_v1_service.yaml delete mode 100644 bundle/manifests/claw-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml delete mode 100644 bundle/manifests/claw-operator.clusterserviceversion.yaml delete mode 100644 bundle/manifests/claw.sandbox.redhat.com_clawdevicepairingrequests.yaml delete mode 100644 bundle/manifests/claw.sandbox.redhat.com_claws.yaml delete mode 100644 bundle/metadata/annotations.yaml delete mode 100644 bundle/tests/scorecard/config.yaml diff --git a/.github/workflows/lint-bundle.yml b/.github/workflows/lint-bundle.yml index 53b701f..2fb5d13 100644 --- a/.github/workflows/lint-bundle.yml +++ b/.github/workflows/lint-bundle.yml @@ -26,6 +26,3 @@ jobs: - name: Validate bundle run: bin/operator-sdk bundle validate ./bundle - - - name: Check bundle consistency - run: git diff --exit-code bundle/ diff --git a/.gitignore b/.gitignore index e983c3b..39fdbd1 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,9 @@ tmp config/.deploy/ config/.bundle/ +# Generated OLM bundle (created by make bundle) +bundle/ + # Ephemeral catalog directory (created by CD pipeline) catalog/ diff --git a/bundle/manifests/claw-operator-controller-manager-metrics-service_v1_service.yaml b/bundle/manifests/claw-operator-controller-manager-metrics-service_v1_service.yaml deleted file mode 100644 index f5253cd..0000000 --- a/bundle/manifests/claw-operator-controller-manager-metrics-service_v1_service.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - creationTimestamp: null - labels: - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: claw-operator - control-plane: controller-manager - name: claw-operator-controller-manager-metrics-service -spec: - ports: - - name: https - port: 8443 - protocol: TCP - targetPort: 8443 - selector: - app.kubernetes.io/name: claw-operator - control-plane: controller-manager -status: - loadBalancer: {} diff --git a/bundle/manifests/claw-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml b/bundle/manifests/claw-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml deleted file mode 100644 index 1ead909..0000000 --- a/bundle/manifests/claw-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - creationTimestamp: null - name: claw-operator-metrics-reader -rules: -- nonResourceURLs: - - /metrics - verbs: - - get diff --git a/bundle/manifests/claw-operator.clusterserviceversion.yaml b/bundle/manifests/claw-operator.clusterserviceversion.yaml deleted file mode 100644 index 82e14be..0000000 --- a/bundle/manifests/claw-operator.clusterserviceversion.yaml +++ /dev/null @@ -1,361 +0,0 @@ -apiVersion: operators.coreos.com/v1alpha1 -kind: ClusterServiceVersion -metadata: - annotations: - alm-examples: |- - [ - { - "apiVersion": "claw.sandbox.redhat.com/v1alpha1", - "kind": "Claw", - "metadata": { - "name": "instance" - }, - "spec": { - "credentials": [ - { - "name": "gemini", - "provider": "google", - "secretRef": [ - { - "key": "api-key", - "name": "gemini-api-key" - } - ], - "type": "apiKey" - } - ] - } - }, - { - "apiVersion": "claw.sandbox.redhat.com/v1alpha1", - "kind": "ClawDevicePairingRequest", - "metadata": { - "name": "example-pairing" - }, - "spec": { - "requestID": "example-request-id", - "selector": { - "matchLabels": { - "app.kubernetes.io/name": "claw" - } - } - } - } - ] - capabilities: Basic Install - createdAt: "REPLACE_CREATED_AT" - operators.operatorframework.io/builder: operator-sdk-v1.42.0 - operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 - name: claw-operator.v0.0.0 - namespace: placeholder -spec: - apiservicedefinitions: {} - customresourcedefinitions: - owned: - - description: ClawDevicePairingRequest is the Schema for the clawdevicepairingrequests - API - displayName: Claw Device Pairing Request - kind: ClawDevicePairingRequest - name: clawdevicepairingrequests.claw.sandbox.redhat.com - version: v1alpha1 - - description: Claw is the Schema for the claws API - displayName: Claw - kind: Claw - name: claws.claw.sandbox.redhat.com - version: v1alpha1 - description: |- - Kubernetes operator that manages OpenClaw instances on OpenShift/Kubernetes. - Handles deployment, credential injection via MITM proxy, LLM provider routing, - and messaging channel integrations. - displayName: Claw Operator - icon: - - base64data: "" - mediatype: image/png - install: - spec: - clusterPermissions: - - rules: - - apiGroups: - - rbac.authorization.k8s.io - resources: - - clusterroles - verbs: - - create - - get - - list - - patch - - update - - watch - - apiGroups: - - authentication.k8s.io - resources: - - tokenreviews - verbs: - - create - - apiGroups: - - authorization.k8s.io - resources: - - subjectaccessreviews - verbs: - - create - serviceAccountName: claw-operator-controller-manager - deployments: - - label: - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: claw-operator - control-plane: controller-manager - name: claw-operator-controller-manager - spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: claw-operator - control-plane: controller-manager - strategy: {} - template: - metadata: - annotations: - kubectl.kubernetes.io/default-container: manager - labels: - app.kubernetes.io/name: claw-operator - control-plane: controller-manager - spec: - containers: - - args: - - --metrics-bind-address=:8443 - - --leader-elect - - --health-probe-bind-address=:8081 - command: - - /manager - env: - - name: PROXY_IMAGE - value: REPLACE_PROXY_IMAGE - - name: KUBECTL_IMAGE - value: REPLACE_KUBECTL_IMAGE - image: REPLACE_IMAGE - imagePullPolicy: IfNotPresent - livenessProbe: - httpGet: - path: /healthz - port: 8081 - initialDelaySeconds: 15 - periodSeconds: 20 - name: manager - readinessProbe: - httpGet: - path: /readyz - port: 8081 - initialDelaySeconds: 5 - periodSeconds: 10 - resources: - limits: - cpu: 500m - memory: 128Mi - requests: - cpu: 10m - memory: 64Mi - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - securityContext: - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault - serviceAccountName: claw-operator-controller-manager - terminationGracePeriodSeconds: 10 - permissions: - - rules: - - apiGroups: - - "" - resources: - - configmaps - verbs: - - get - - list - - watch - - create - - update - - patch - - delete - - apiGroups: - - coordination.k8s.io - resources: - - leases - verbs: - - get - - list - - watch - - create - - update - - patch - - delete - - apiGroups: - - "" - resources: - - events - verbs: - - create - - patch - - apiGroups: - - "" - resources: - - configmaps - - persistentvolumeclaims - - secrets - - serviceaccounts - verbs: - - create - - get - - list - - patch - - update - - watch - - apiGroups: - - "" - resources: - - pods - verbs: - - list - - apiGroups: - - "" - resources: - - pods/exec - verbs: - - create - - apiGroups: - - "" - resources: - - services - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - apps - resources: - - deployments - verbs: - - create - - get - - list - - patch - - update - - watch - - apiGroups: - - claw.sandbox.redhat.com - resources: - - clawdevicepairingrequests - verbs: - - create - - get - - list - - patch - - update - - watch - - apiGroups: - - claw.sandbox.redhat.com - resources: - - clawdevicepairingrequests/finalizers - - claws/finalizers - verbs: - - update - - apiGroups: - - claw.sandbox.redhat.com - resources: - - clawdevicepairingrequests/status - - claws/status - verbs: - - get - - patch - - update - - apiGroups: - - claw.sandbox.redhat.com - resources: - - claws - verbs: - - get - - list - - watch - - apiGroups: - - networking.k8s.io - resources: - - networkpolicies - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - rbac.authorization.k8s.io - resources: - - rolebindings - verbs: - - create - - get - - list - - patch - - update - - watch - - apiGroups: - - route.openshift.io - resources: - - routes - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - route.openshift.io - resources: - - routes/custom-host - verbs: - - create - - update - serviceAccountName: claw-operator-controller-manager - strategy: deployment - installModes: - - supported: true - type: OwnNamespace - - supported: true - type: SingleNamespace - - supported: false - type: MultiNamespace - - supported: false - type: AllNamespaces - keywords: - - ai - - llm - - openclaw - - developer-sandbox - links: - - name: Claw Operator - url: https://github.com/codeready-toolchain/claw-operator - maintainers: - - email: devsandbox@redhat.com - name: Developer Sandbox Team - maturity: alpha - minKubeVersion: 1.28.0 - provider: - name: Red Hat - url: https://developers.redhat.com - relatedImages: - - image: REPLACE_IMAGE - name: manager - - image: REPLACE_PROXY_IMAGE - name: proxy - - image: REPLACE_KUBECTL_IMAGE - name: kubectl - version: 0.0.0 diff --git a/bundle/manifests/claw.sandbox.redhat.com_clawdevicepairingrequests.yaml b/bundle/manifests/claw.sandbox.redhat.com_clawdevicepairingrequests.yaml deleted file mode 100644 index cca8051..0000000 --- a/bundle/manifests/claw.sandbox.redhat.com_clawdevicepairingrequests.yaml +++ /dev/null @@ -1,189 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.19.0 - creationTimestamp: null - name: clawdevicepairingrequests.claw.sandbox.redhat.com -spec: - group: claw.sandbox.redhat.com - names: - kind: ClawDevicePairingRequest - listKind: ClawDevicePairingRequestList - plural: clawdevicepairingrequests - singular: clawdevicepairingrequest - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .spec.requestID - name: RequestID - type: string - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: ClawDevicePairingRequest is the Schema for the clawdevicepairingrequests - API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: ClawDevicePairingRequestSpec defines the desired state of - ClawDevicePairingRequest - properties: - requestID: - description: RequestID is the unique identifier for this pairing request - minLength: 1 - type: string - selector: - description: Selector specifies which pod should process this device - pairing request - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. - The requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that the selector applies - to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - x-kubernetes-validations: - - message: selector must include at least one matchLabels or matchExpressions - entry - rule: (has(self.matchLabels) && size(self.matchLabels) > 0) || (has(self.matchExpressions) - && size(self.matchExpressions) > 0) - required: - - requestID - - selector - type: object - status: - description: ClawDevicePairingRequestStatus defines the observed state - of ClawDevicePairingRequest - properties: - conditions: - description: Conditions represent the latest available observations - of the ClawDevicePairingRequest's state - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - type: object - required: - - spec - type: object - served: true - storage: true - subresources: - status: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: null - storedVersions: null diff --git a/bundle/manifests/claw.sandbox.redhat.com_claws.yaml b/bundle/manifests/claw.sandbox.redhat.com_claws.yaml deleted file mode 100644 index 9dcd298..0000000 --- a/bundle/manifests/claw.sandbox.redhat.com_claws.yaml +++ /dev/null @@ -1,413 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.19.0 - creationTimestamp: null - name: claws.claw.sandbox.redhat.com -spec: - group: claw.sandbox.redhat.com - names: - kind: Claw - listKind: ClawList - plural: claws - singular: claw - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .status.conditions[?(@.type=="Ready")].status - name: Ready - type: string - - jsonPath: .status.conditions[?(@.type=="Ready")].reason - name: Reason - type: string - name: v1alpha1 - schema: - openAPIV3Schema: - description: Claw is the Schema for the claws API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: ClawSpec defines the desired state of Claw - properties: - configMode: - default: merge - description: |- - ConfigMode controls how operator config is applied on pod start. - "merge" (default) deep-merges operator settings into the existing - user config, preserving user-owned keys. "overwrite" fully replaces - the config on every pod start. - enum: - - merge - - overwrite - type: string - credentials: - description: Credentials configures proxy credential injection per - domain. - items: - description: CredentialSpec defines a single credential entry for - proxy injection. - properties: - allowedPaths: - description: |- - AllowedPaths restricts which URL paths the proxy permits for this domain. - Each entry is a path prefix (e.g., "/v1/api/"). If empty, all paths are allowed. - items: - type: string - type: array - apiKey: - description: APIKey configures custom header injection. Required - when type is "apiKey". - properties: - header: - description: Header name where the API key is injected (e.g., - "x-goog-api-key", "x-api-key") - minLength: 1 - type: string - valuePrefix: - description: |- - ValuePrefix is prepended to the secret value before injection. - Examples: "Bot " (Discord), "Basic " (pre-encoded basic auth). - type: string - required: - - header - type: object - channel: - description: |- - Channel declares this credential as a messaging channel integration. - When set, the operator enables the channel in OpenClaw's config and - infers proxy defaults (type, domain, injection config, companion routes). - Known values: telegram, discord, slack, whatsapp. - Mutually exclusive with provider. - type: string - channelConfig: - description: |- - ChannelConfig is opaque JSON deep-merged into the channel's config block - in operator.json. Use for channel-specific settings (dmPolicy, allowFrom, etc.). - type: object - x-kubernetes-preserve-unknown-fields: true - defaultHeaders: - additionalProperties: - type: string - description: |- - DefaultHeaders are injected on every proxied request for this credential, - in addition to the credential itself (e.g., "anthropic-version: 2023-06-01"). - type: object - domain: - description: |- - Domain the proxy matches against the request Host header. - Exact match: "api.github.com". Suffix match: ".googleapis.com" (leading dot). - Optional for known providers and channels — the operator infers the default domain. - type: string - gcp: - description: GCP configures GCP service account credential injection. - Required when type is "gcp". - properties: - location: - description: Location is the GCP region (e.g., us-central1) - minLength: 1 - type: string - project: - description: Project is the GCP project ID - minLength: 1 - type: string - required: - - location - - project - type: object - name: - description: Name uniquely identifies this credential entry. - minLength: 1 - type: string - oauth2: - description: OAuth2 configures client credentials token exchange. - Required when type is "oauth2". - properties: - clientID: - description: ClientID for the OAuth2 client credentials - flow - minLength: 1 - type: string - scopes: - description: Scopes requested during token exchange - items: - type: string - type: array - tokenURL: - description: TokenURL is the OAuth2 token endpoint - minLength: 1 - type: string - required: - - clientID - - tokenURL - type: object - pathToken: - description: PathToken configures URL path token injection. - Required when type is "pathToken". - properties: - prefix: - description: Prefix is prepended before the token in the - URL path (e.g., "/bot" for Telegram) - minLength: 1 - type: string - required: - - prefix - type: object - provider: - description: |- - Provider maps this credential to an OpenClaw LLM provider (e.g., "google", "anthropic", "openai", "openrouter"). - When set, the controller configures gateway routing and generates the provider entry in openclaw.json. - Mutually exclusive with channel. - type: string - secretRef: - description: |- - SecretRef references Kubernetes Secrets holding credential values. - For single-secret credentials, use a one-element array. - For multi-secret channels (e.g., Slack), use role to distinguish entries. - Not required for type "none" (proxy allowlist, no auth) or channels that use - non-secret auth (e.g., WhatsApp QR pairing). - items: - description: SecretRefEntry references a specific key in a - Secret. - properties: - key: - description: Key is the key in the Secret's data map - minLength: 1 - type: string - name: - description: Name is the name of the Secret - minLength: 1 - type: string - role: - description: |- - Role distinguishes multiple secrets for the same credential. - Required when multiple secretRef entries are present (e.g., Slack botToken/appToken). - maxLength: 63 - type: string - required: - - key - - name - type: object - type: array - type: - description: |- - Type selects the credential injection mechanism. - Optional when channel is set — the operator infers the type from the channel defaults. - enum: - - apiKey - - bearer - - gcp - - pathToken - - oauth2 - - none - - kubernetes - type: string - required: - - name - type: object - x-kubernetes-validations: - - message: either type or channel must be set - rule: has(self.type) || has(self.channel) - - message: provider and channel are mutually exclusive - rule: '!has(self.provider) || !has(self.channel)' - - message: secretRef is required unless type is none or channel - is set - rule: has(self.channel) || self.type == 'none' || has(self.secretRef) - - message: apiKey config is required when type is apiKey without - inferred defaults - rule: self.type != 'apiKey' || has(self.apiKey) || has(self.provider) - || has(self.channel) - - message: gcp config is required when type is gcp without inferred - defaults - rule: self.type != 'gcp' || has(self.gcp) || has(self.channel) - - message: pathToken config is required when type is pathToken without - inferred defaults - rule: self.type != 'pathToken' || has(self.pathToken) || has(self.channel) - - message: oauth2 config is required when type is oauth2 without - inferred defaults - rule: self.type != 'oauth2' || has(self.oauth2) || has(self.channel) - type: array - mcpServers: - additionalProperties: - description: McpServerSpec defines an MCP server the operator injects - into OpenClaw's config. - properties: - args: - description: Args are command-line arguments for the stdio server. - items: - type: string - type: array - command: - description: Command is the executable for a stdio MCP server. - type: string - env: - additionalProperties: - type: string - description: |- - Env are plain environment variables passed to the stdio server process - and written into the MCP server config in operator.json. - Use for non-secret values and tier-2 placeholder tokens. - type: object - envFrom: - description: |- - EnvFrom are secret-backed environment variables mounted on the gateway - container and inherited by the stdio server subprocess (tier 3). - Use only when the proxy-placeholder pattern (tier 2) is not viable. - items: - description: |- - McpEnvFromSecret maps a Kubernetes Secret key to an environment variable - on the gateway container for tier 3 MCP secret injection. - properties: - name: - description: Name is the environment variable name. - minLength: 1 - type: string - secretRef: - description: SecretRef references a key in a Kubernetes - Secret. - properties: - key: - description: Key is the key in the Secret's data map - minLength: 1 - type: string - name: - description: Name is the name of the Secret - minLength: 1 - type: string - role: - description: |- - Role distinguishes multiple secrets for the same credential. - Required when multiple secretRef entries are present (e.g., Slack botToken/appToken). - maxLength: 63 - type: string - required: - - key - - name - type: object - required: - - name - - secretRef - type: object - type: array - transport: - description: |- - Transport selects the HTTP transport type ("streamable-http" or "sse"). - Only valid when url is set. - type: string - url: - description: URL is the endpoint for an HTTP MCP server. - type: string - type: object - x-kubernetes-validations: - - message: either command (stdio) or url (HTTP) must be set - rule: has(self.command) || has(self.url) - - message: command and url are mutually exclusive - rule: '!has(self.command) || !has(self.url)' - - message: envFrom is only allowed for stdio MCP servers (command), - not HTTP (url) - rule: '!has(self.url) || !has(self.envFrom) || size(self.envFrom) - == 0' - description: |- - McpServers declares MCP servers injected into OpenClaw's config. - Map keys are server names as they appear in the mcp.servers config. - type: object - type: object - status: - description: ClawStatus defines the observed state of Claw - properties: - conditions: - description: Conditions represent the latest available observations - of the Claw's state - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - gatewayTokenSecretRef: - description: GatewayTokenSecretRef is the name of the Secret containing - the gateway authentication token - type: string - url: - description: URL is the HTTPS URL for accessing the Claw instance - type: string - type: object - type: object - served: true - storage: true - subresources: - status: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: null - storedVersions: null diff --git a/bundle/metadata/annotations.yaml b/bundle/metadata/annotations.yaml deleted file mode 100644 index 4f7770e..0000000 --- a/bundle/metadata/annotations.yaml +++ /dev/null @@ -1,15 +0,0 @@ -annotations: - # Core bundle annotations. - operators.operatorframework.io.bundle.mediatype.v1: registry+v1 - operators.operatorframework.io.bundle.manifests.v1: manifests/ - operators.operatorframework.io.bundle.metadata.v1: metadata/ - operators.operatorframework.io.bundle.package.v1: claw-operator - operators.operatorframework.io.bundle.channels.v1: staging - operators.operatorframework.io.bundle.channel.default.v1: staging - operators.operatorframework.io.metrics.builder: operator-sdk-v1.42.0 - operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 - operators.operatorframework.io.metrics.project_layout: go.kubebuilder.io/v4 - - # Annotations for testing. - operators.operatorframework.io.test.mediatype.v1: scorecard+v1 - operators.operatorframework.io.test.config.v1: tests/scorecard/ diff --git a/bundle/tests/scorecard/config.yaml b/bundle/tests/scorecard/config.yaml deleted file mode 100644 index 6a6e414..0000000 --- a/bundle/tests/scorecard/config.yaml +++ /dev/null @@ -1,70 +0,0 @@ -apiVersion: scorecard.operatorframework.io/v1alpha3 -kind: Configuration -metadata: - name: config -stages: -- parallel: true - tests: - - entrypoint: - - scorecard-test - - basic-check-spec - image: quay.io/operator-framework/scorecard-test:v1.42.0 - labels: - suite: basic - test: basic-check-spec-test - storage: - spec: - mountPath: {} - - entrypoint: - - scorecard-test - - olm-bundle-validation - image: quay.io/operator-framework/scorecard-test:v1.42.0 - labels: - suite: olm - test: olm-bundle-validation-test - storage: - spec: - mountPath: {} - - entrypoint: - - scorecard-test - - olm-crds-have-validation - image: quay.io/operator-framework/scorecard-test:v1.42.0 - labels: - suite: olm - test: olm-crds-have-validation-test - storage: - spec: - mountPath: {} - - entrypoint: - - scorecard-test - - olm-crds-have-resources - image: quay.io/operator-framework/scorecard-test:v1.42.0 - labels: - suite: olm - test: olm-crds-have-resources-test - storage: - spec: - mountPath: {} - - entrypoint: - - scorecard-test - - olm-spec-descriptors - image: quay.io/operator-framework/scorecard-test:v1.42.0 - labels: - suite: olm - test: olm-spec-descriptors-test - storage: - spec: - mountPath: {} - - entrypoint: - - scorecard-test - - olm-status-descriptors - image: quay.io/operator-framework/scorecard-test:v1.42.0 - labels: - suite: olm - test: olm-status-descriptors-test - storage: - spec: - mountPath: {} -storage: - spec: - mountPath: {}