Skip to content

Commit 734cbe2

Browse files
joelanfordclaude
andcommitted
docs: add single-tenant simplification design
Add a design document proposing changes to re-affirm OLM v1's single-tenant, cluster-admin-only operational model. The design covers deprecating the service account field, removing SingleNamespace/OwnNamespace install mode support, automating namespace management, and simplifying the content manager. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 02bfdfc commit 734cbe2

9 files changed

+616
-0
lines changed

docs/draft/project/single-tenant-simplification.md

Lines changed: 240 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Work Item: Grant operator-controller cluster-admin
2+
3+
**Parent:** [Single-Tenant Simplification](../single-tenant-simplification.md)
4+
**Status:** Not started
5+
**Depends on:** Nothing (first work item)
6+
7+
## Summary
8+
9+
Grant operator-controller's ServiceAccount `cluster-admin` privileges via its Helm-managed ClusterRoleBinding. This is a prerequisite for all other simplification work items: once operator-controller has cluster-admin, per-extension service account scoping becomes unnecessary.
10+
11+
## Current RBAC architecture
12+
13+
The Helm chart defines RBAC in `helm/olmv1/templates/rbac/clusterrole-operator-controller-manager-role.yml`. The ClusterRole is structured in two tiers, conditional on the `BoxcutterRuntime` feature gate:
14+
15+
### When BoxcutterRuntime is NOT enabled (line 1)
16+
17+
The entire ClusterRole is gated behind `{{- if and .Values.options.operatorController.enabled (not (has "BoxcutterRuntime" .Values.operatorConrollerFeatures)) }}`. It grants:
18+
19+
- `serviceaccounts/token` create and `serviceaccounts` get — for per-CE SA impersonation
20+
- `customresourcedefinitions` get
21+
- `clustercatalogs` get/list/watch
22+
- `clusterextensions` get/list/patch/update/watch, plus finalizers and status updates
23+
- `clusterrolebindings`, `clusterroles`, `rolebindings`, `roles` list/watch — for `PreflightPermissions` RBAC validation
24+
- OpenShift SCC `use` (conditional on `.Values.options.openshift.enabled`)
25+
26+
### When BoxcutterRuntime IS enabled (lines 81-113)
27+
28+
Additional rules are appended within the same ClusterRole:
29+
30+
- `*/*` list/watch — broad permissions needed for the Boxcutter tracking cache
31+
- `clusterextensionrevisions` full CRUD, status, and finalizers
32+
33+
### When BoxcutterRuntime is enabled but the outer guard is false
34+
35+
If BoxcutterRuntime is in `.Values.operatorConrollerFeatures`, the outer guard on line 1 is false, so the entire ClusterRole (including the Boxcutter-specific rules) is not rendered. This suggests the Boxcutter path uses a different RBAC mechanism or that there is a separate ClusterRole for that case.
36+
37+
## Proposed changes
38+
39+
### Helm RBAC
40+
41+
- Replace the conditionally-scoped ClusterRole with a `cluster-admin` ClusterRoleBinding (binding operator-controller's ServiceAccount to the built-in `cluster-admin` ClusterRole).
42+
- Remove the conditional RBAC templating based on `BoxcutterRuntime`. With cluster-admin, the feature-gate-conditional rules are all subsumed.
43+
- Remove the `serviceaccounts/token` create and `serviceaccounts` get rules (no longer needed since operator-controller uses its own identity).
44+
- Remove the RBAC list/watch rules that existed for `PreflightPermissions` validation.
45+
- The OpenShift SCC `use` permission is also subsumed by cluster-admin.
46+
47+
### Key files
48+
49+
- `helm/olmv1/templates/rbac/clusterrole-operator-controller-manager-role.yml` — Replace with cluster-admin ClusterRoleBinding
50+
- Any other Helm RBAC templates that may need updating
51+
52+
## Notes
53+
54+
- There is a typo in the Helm template: `.Values.operatorConrollerFeatures` (missing "t" in "Controller"). This can be fixed as part of this work or separately.
55+
- The cluster-admin grant is intentionally broad. The security boundary shifts from "what can operator-controller do" to "who can create ClusterExtension/ClusterCatalog resources." See the [security analysis](../single-tenant-simplification.md#security-analysis) in the design doc.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Work Item: Deprecate and ignore spec.serviceAccount
2+
3+
**Parent:** [Single-Tenant Simplification](../single-tenant-simplification.md)
4+
**Status:** Not started
5+
**Depends on:** [01-cluster-admin](01-cluster-admin.md)
6+
7+
## Summary
8+
9+
Mark `ClusterExtension.spec.serviceAccount` as deprecated in the OpenAPI schema and make the controller ignore it. operator-controller uses its own ServiceAccount for all Kubernetes API interactions when managing ClusterExtension resources.
10+
11+
## Scope
12+
13+
- Mark `spec.serviceAccount` as deprecated in the CRD/OpenAPI schema.
14+
- Modify the controller to stop reading `spec.serviceAccount` and instead use operator-controller's own identity for all API calls.
15+
- The `RestConfigMapper` / `clientRestConfigMapper` plumbing in `cmd/operator-controller/main.go` that creates per-CE service account-scoped clients becomes unnecessary. The Helm `ActionConfigGetter` should use operator-controller's own config.
16+
- The field remains in the API for backward compatibility but is ignored.
17+
18+
## Helm applier
19+
20+
- The `RestConfigMapper` / `clientRestConfigMapper` plumbing in `cmd/operator-controller/main.go` that creates per-CE service account-scoped clients becomes unnecessary. The Helm `ActionConfigGetter` should use operator-controller's own config.
21+
22+
## Boxcutter applier
23+
24+
The Boxcutter applier reads `spec.serviceAccount` and propagates it through the revision lifecycle:
25+
26+
- `applier/boxcutter.go:208-209``buildClusterExtensionRevision` writes `ServiceAccountNameKey` and `ServiceAccountNamespaceKey` annotations on `ClusterExtensionRevision` objects, sourced from `ext.Spec.ServiceAccount.Name` and `ext.Spec.Namespace`.
27+
- `controllers/revision_engine_factory.go:82-98``getServiceAccount` reads those annotations back from the CER.
28+
- `controllers/revision_engine_factory.go:101-122``createScopedClient` creates an anonymous REST config wrapped with `TokenInjectingRoundTripper` to impersonate the SA.
29+
- `controllers/revision_engine_factory.go:57-80``CreateRevisionEngine` passes the scoped client into the boxcutter `machinery.NewObjectEngine` and `machinery.NewRevisionEngine`.
30+
31+
With single-tenant simplification:
32+
33+
- Stop writing SA annotations on CERs.
34+
- `RevisionEngineFactory` uses operator-controller's own client directly instead of creating per-CER scoped clients. The factory struct fields `BaseConfig` and `TokenGetter` become unnecessary.
35+
- Remove `getServiceAccount` and `createScopedClient` from the factory.
36+
- Remove the `internal/operator-controller/authentication/` package (`TokenGetter` and `TokenInjectingRoundTripper`), which is only used for SA impersonation.
37+
- Remove `ServiceAccountNameKey` / `ServiceAccountNamespaceKey` label constants.
38+
39+
Note: The Boxcutter `TrackingCache` (informers) is already shared across all CERs — no informer consolidation is needed. The per-CER isolation was only at the client level for SA scoping.
40+
41+
## Key files
42+
43+
- `api/v1/clusterextension_types.go` — Mark field deprecated
44+
- `cmd/operator-controller/main.go:697-708``clientRestConfigMapper` and Helm `ActionConfigGetter` setup
45+
- `internal/operator-controller/authentication/``TokenGetter` / `TokenInjectingRoundTripper` (remove entirely)
46+
- `internal/operator-controller/applier/boxcutter.go:208-209` — SA annotations written on CERs
47+
- `internal/operator-controller/controllers/revision_engine_factory.go` — Per-CER scoped client creation
48+
- `internal/operator-controller/labels/labels.go:29-41``ServiceAccountNameKey` / `ServiceAccountNamespaceKey` constants
49+
50+
## Migration
51+
52+
- Existing ClusterExtensions that specify `spec.serviceAccount` continue to function; the field is simply ignored.
53+
- Cluster-admins can clean up the ServiceAccount, ClusterRole, ClusterRoleBinding, Role, and RoleBinding resources they previously created at their convenience.
54+
55+
## Notes
56+
57+
- Full removal of `spec.serviceAccount` from the API happens in a future API version (Phase 2 in the design doc).
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Work Item: Remove PreflightPermissions feature gate and code
2+
3+
**Parent:** [Single-Tenant Simplification](../single-tenant-simplification.md)
4+
**Status:** Not started
5+
**Depends on:** Nothing (independent)
6+
7+
## Summary
8+
9+
Remove the `PreflightPermissions` feature gate (currently Alpha, default off) and all associated RBAC pre-authorization code. This also enables removing the `k8s.io/kubernetes` dependency from `go.mod`, along with 30 `replace` directives that exist solely to align `k8s.io/kubernetes` sub-module versions.
10+
11+
## Scope
12+
13+
### Remove feature gate and code
14+
15+
- Remove the `PreflightPermissions` feature gate definition from `internal/operator-controller/features/features.go:26-30`.
16+
- Remove the `internal/operator-controller/authorization/` package entirely. This package contains the `PreAuthorizer` interface and the RBAC-based implementation that validates user permissions before applying manifests.
17+
- Remove the `PreAuthorizer` field from the Boxcutter applier (`internal/operator-controller/applier/boxcutter.go:421`).
18+
- Remove the conditional `PreAuthorizer` setup in `cmd/operator-controller/main.go:722-725`.
19+
- Remove related tests (`internal/operator-controller/authorization/rbac_test.go`).
20+
21+
### Remove `k8s.io/kubernetes` dependency
22+
23+
The `authorization/rbac.go` imports four packages from `k8s.io/kubernetes` (lines 28-32):
24+
25+
- `k8s.io/kubernetes/pkg/apis/rbac`
26+
- `k8s.io/kubernetes/pkg/apis/rbac/v1`
27+
- `k8s.io/kubernetes/pkg/registry/rbac`
28+
- `k8s.io/kubernetes/pkg/registry/rbac/validation`
29+
- `k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac`
30+
31+
These are the **only** imports of `k8s.io/kubernetes` in the entire codebase. Removing the authorization package enables:
32+
33+
- Removing `k8s.io/kubernetes v1.35.0` from `go.mod` (line 46).
34+
- Removing all 30 `replace` directives (lines 260-319) that exist to align `k8s.io/kubernetes` sub-module versions (`k8s.io/api`, `k8s.io/apiextensions-apiserver`, `k8s.io/apimachinery`, `k8s.io/apiserver`, `k8s.io/cli-runtime`, `k8s.io/client-go`, `k8s.io/cloud-provider`, `k8s.io/cluster-bootstrap`, etc.).
35+
- Running `go mod tidy` to clean up any transitive dependencies that were only pulled in by `k8s.io/kubernetes`.
36+
37+
### Remove k8s-pin tooling
38+
39+
The `hack/tools/k8smaintainer/` program (invoked via `make k8s-pin`) exists to generate and maintain the `k8s.io/*` `replace` directives in `go.mod`. It reads the `k8s.io/kubernetes` version, enumerates all `k8s.io/*` staging modules in the dependency graph, and pins them to matching versions. The `make verify` target runs `k8s-pin` as a prerequisite.
40+
41+
With `k8s.io/kubernetes` removed from `go.mod`, this tool has nothing to do — the remaining `k8s.io/*` dependencies (`k8s.io/api`, `k8s.io/client-go`, etc.) are used directly and managed normally by `go mod tidy` without `replace` directives.
42+
43+
- Remove `hack/tools/k8smaintainer/` (including `main.go` and `README.md`).
44+
- Remove the `k8s-pin` Makefile target and its invocation in the `verify` target (replace with just `tidy`).
45+
46+
### Remove RBAC list/watch permissions
47+
48+
With `PreflightPermissions` removed, operator-controller no longer needs the `clusterrolebindings`, `clusterroles`, `rolebindings`, `roles` list/watch permissions in the Helm ClusterRole. These can be removed as part of [01-cluster-admin](01-cluster-admin.md) or this work item.
49+
50+
## Key files
51+
52+
- `internal/operator-controller/authorization/rbac.go` — Only consumer of `k8s.io/kubernetes`
53+
- `internal/operator-controller/authorization/rbac_test.go` — Tests
54+
- `internal/operator-controller/features/features.go` — Feature gate definition
55+
- `internal/operator-controller/applier/boxcutter.go``PreAuthorizer` field
56+
- `cmd/operator-controller/main.go` — Conditional setup
57+
- `go.mod``k8s.io/kubernetes` dependency and replace directives
58+
- `hack/tools/k8smaintainer/``k8s-pin` tool that generates replace directives
59+
- `Makefile``k8s-pin` target (line 157) and its use in `verify` (line 205)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Work Item: Remove SyntheticPermissions feature gate and code
2+
3+
**Parent:** [Single-Tenant Simplification](../single-tenant-simplification.md)
4+
**Status:** Not started
5+
**Depends on:** Nothing (independent)
6+
7+
## Summary
8+
9+
Remove the `SyntheticPermissions` feature gate (currently Alpha, default off) and all associated synthetic user permission model code.
10+
11+
## Scope
12+
13+
- Remove the `SyntheticPermissions` feature gate definition.
14+
- Remove the synthetic user REST config mapper and related code.
15+
- Remove related tests.
16+
17+
## Key files
18+
19+
- `internal/operator-controller/features/` - Feature gate definitions
20+
- `cmd/operator-controller/main.go:698-699` - Conditional wrapping with `SyntheticUserRestConfigMapper`
21+
- Any code implementing the synthetic permissions model
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Work Item: Remove SingleNamespace/OwnNamespace install mode support
2+
3+
**Parent:** [Single-Tenant Simplification](../single-tenant-simplification.md)
4+
**Status:** Not started
5+
**Depends on:** Nothing (independent)
6+
7+
## Summary
8+
9+
Remove the `SingleOwnNamespaceInstallSupport` feature gate (currently GA, default on) and all associated code. Operators are always installed in AllNamespaces mode. The `watchNamespace` configuration option is removed from the bundle config schema.
10+
11+
## Scope
12+
13+
### Remove feature gate
14+
15+
- Remove the `SingleOwnNamespaceInstallSupport` feature gate definition from `internal/operator-controller/features/features.go:35-39`.
16+
- Remove all references to `IsSingleOwnNamespaceEnabled` / `SingleOwnNamespaceInstallSupport` throughout the codebase.
17+
18+
### Remove watchNamespace from the config schema
19+
20+
- Remove the `watchNamespace` property from the bundle config JSON schema (`internal/operator-controller/rukpak/bundle/registryv1bundleconfig.json`).
21+
- Remove `GetWatchNamespace()` from `internal/operator-controller/config/config.go:88-106`.
22+
- Remove the conditional block in `internal/operator-controller/applier/provider.go:73-79` that extracts `watchNamespace` and passes it to the renderer via `render.WithTargetNamespaces()`.
23+
- Remove related tests in `internal/operator-controller/config/config_test.go` and `internal/operator-controller/applier/provider_test.go`.
24+
25+
With `watchNamespace` removed from the schema, any ClusterExtension that specifies `spec.config.inline.watchNamespace` will fail schema validation automatically — no explicit validation error needs to be added.
26+
27+
### Update install mode handling
28+
29+
- If a CSV only declares support for `SingleNamespace` and/or `OwnNamespace` (and not `AllNamespaces`), OLM v1 installs it in AllNamespaces mode anyway. OLM v1 takes the position that watching all namespaces is always correct for a cluster-scoped controller installation.
30+
- Remove the `render.WithTargetNamespaces()` option and related rendering logic that generates namespace-scoped configurations.
31+
32+
## Key files
33+
34+
- `internal/operator-controller/features/features.go` — Feature gate definition
35+
- `internal/operator-controller/config/config.go``GetWatchNamespace()` method
36+
- `internal/operator-controller/applier/provider.go` — Conditional `IsSingleOwnNamespaceEnabled` block
37+
- `internal/operator-controller/rukpak/bundle/registryv1bundleconfig.json` — Bundle config schema
38+
- `internal/operator-controller/rukpak/bundle/registryv1.go` — Bundle config schema handling
39+
- `hack/tools/schema-generator/main.go` — Schema generator
40+
- `docs/draft/howto/single-ownnamespace-install.md` — To be archived/removed (see [09-documentation](09-documentation.md))
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Work Item: Make spec.namespace optional with automatic namespace management
2+
3+
**Parent:** [Single-Tenant Simplification](../single-tenant-simplification.md)
4+
**Status:** Not started
5+
**Depends on:** [02-deprecate-service-account](02-deprecate-service-account.md)
6+
7+
## Summary
8+
9+
Change `ClusterExtension.spec.namespace` from required to optional. When not specified, operator-controller determines the installation namespace automatically using CSV annotations or a fallback convention. The installation namespace becomes a managed object of the ClusterExtension.
10+
11+
## Namespace determination precedence
12+
13+
operator-controller determines the installation namespace using the following precedence (highest to lowest):
14+
15+
1. **`ClusterExtension.spec.namespace`** — If specified by the user, this is the namespace name used.
16+
2. **`operatorframework.io/suggested-namespace` CSV annotation** — If the CSV provides a suggested namespace name, it is used.
17+
3. **`operatorframework.io/suggested-namespace-template` CSV annotation** — If the CSV provides a namespace template (a full JSON Namespace object), its `metadata.name` is used.
18+
4. **`<packageName>-system` fallback** — If none of the above are present, operator-controller generates a namespace name from the package name. If `<packageName>-system` exceeds the maximum namespace name length, `<packageName>` is used instead.
19+
20+
## Namespace body/template
21+
22+
- If the CSV contains the `operatorframework.io/suggested-namespace-template` annotation, its value (a full JSON Namespace object) is used as the template for creating the namespace. This allows bundle authors to specify labels, annotations, and other namespace metadata.
23+
- If `spec.namespace` or `suggested-namespace` specifies a different name than what appears in the template, the template body is still used but with the name overridden.
24+
- If no template annotation is present, operator-controller creates a plain namespace with the determined name.
25+
26+
### Behavior when both annotations are present
27+
28+
When a CSV defines both `operatorframework.io/suggested-namespace` and `operatorframework.io/suggested-namespace-template`:
29+
30+
- The **name** comes from `suggested-namespace` (unless overridden by `spec.namespace`).
31+
- The **body** (labels, annotations, other metadata) comes from `suggested-namespace-template`.
32+
- If the template's `metadata.name` differs from the name determined by `suggested-namespace`, the template's name is overridden.
33+
34+
In other words, `suggested-namespace` controls naming and `suggested-namespace-template` controls the namespace shape. When both are present, the name from `suggested-namespace` takes precedence over any name embedded in the template.
35+
36+
## Namespace lifecycle
37+
38+
- The installation namespace is a **managed object** of the ClusterExtension. It follows the same ownership rules as all other managed objects:
39+
- It is created by operator-controller if it does not exist.
40+
- A pre-existing namespace results in a conflict error, consistent with the [single-owner objects](../../concepts/single-owner-objects.md) design.
41+
- It is deleted when the ClusterExtension is deleted (along with all other managed objects).
42+
- The immutability constraint on `spec.namespace` is retained — once set (explicitly or by auto-determination), the namespace cannot be changed.
43+
44+
## Migration
45+
46+
- Existing ClusterExtensions that specify `spec.namespace` continue to function identically.
47+
- For existing installations where the namespace was manually created before the ClusterExtension, operator-controller should adopt the namespace during the migration period (one-time reconciliation to add ownership metadata). The pre-existence error applies only to new installations going forward.
48+
49+
## Scope
50+
51+
### API changes
52+
53+
- Mark `spec.namespace` as optional in `api/v1/clusterextension_types.go` (remove the `required` validation or make the field a pointer).
54+
- Update CRD/OpenAPI schema generation.
55+
56+
### Controller changes
57+
58+
- Add namespace determination logic implementing the precedence rules above.
59+
- Add namespace creation/management as a reconciliation step (likely a new `ReconcileStepFunc` in `internal/operator-controller/controllers/clusterextension_reconcile_steps.go`).
60+
- Read `operatorframework.io/suggested-namespace` and `operatorframework.io/suggested-namespace-template` from the resolved CSV.
61+
- Handle the migration path: adopt pre-existing namespaces for existing installations.
62+
63+
### Key files
64+
65+
- `api/v1/clusterextension_types.go` — Make `Namespace` optional
66+
- `internal/operator-controller/controllers/clusterextension_reconcile_steps.go` — New namespace management step
67+
- `internal/operator-controller/controllers/clusterextension_controller.go` — Integration
68+
- CSV annotation reading (location TBD based on where bundle metadata is accessed)

0 commit comments

Comments
 (0)