Skip to content

Commit 14b722f

Browse files
committed
feat(api): make spec.namespace optional with managed namespace support and PSA support
When spec.namespace is omitted, operator-controller resolves a managed namespace from bundle metadata using the fallback chain: suggested-namespace-template > suggested-namespace > <packageName>-system. The managed namespace is included as a ClusterObjectSet object with collision protection. Pod Security Admission labels from the bundle's suggested-namespace-template annotation are applied to the managed namespace, enabling operators to declare their PSA requirements Signed-off-by: Nader Ziada <nziada@redhat.com>
1 parent a7e2fcd commit 14b722f

29 files changed

Lines changed: 1659 additions & 203 deletions

api/v1/clusterextension_types.go

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,21 +49,25 @@ const (
4949

5050
// ClusterExtensionSpec defines the desired state of ClusterExtension
5151
type ClusterExtensionSpec struct {
52-
// namespace specifies a Kubernetes namespace.
53-
// It designates the default namespace where namespace-scoped resources for the extension are applied to the cluster.
54-
// Some extensions may contain namespace-scoped resources to be applied in other namespaces.
55-
// This namespace must exist.
52+
// namespace is optional. When set, it specifies an existing namespace where
53+
// namespace-scoped resources for the extension are applied. The namespace must
54+
// already exist on the cluster. When omitted, operator-controller resolves and
55+
// creates a managed namespace from bundle metadata.
5656
//
57-
// The namespace field is required, immutable, and follows the DNS label standard as defined in [RFC 1123].
57+
// The mode (set vs omitted) is locked at creation time and cannot be changed.
58+
// When set, the value is immutable.
59+
//
60+
// The namespace field follows the DNS label standard as defined in [RFC 1123].
5861
// It must contain only lowercase alphanumeric characters or hyphens (-), start and end with an alphanumeric character,
5962
// and be no longer than 63 characters.
6063
//
6164
// [RFC 1123]: https://tools.ietf.org/html/rfc1123
6265
//
6366
// +kubebuilder:validation:MaxLength:=63
64-
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="namespace is immutable"
65-
// +kubebuilder:validation:XValidation:rule="self.matches(\"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$\")",message="namespace must be a valid DNS1123 label"
66-
// +required
67+
// +kubebuilder:validation:XValidation:rule="self == '' || self.matches(\"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$\")",message="namespace must be a valid DNS1123 label"
68+
// +kubebuilder:validation:XValidation:rule="oldSelf == '' || self == oldSelf",message="namespace is immutable once set"
69+
// +kubebuilder:validation:XValidation:rule="oldSelf != '' || self == ''",message="namespace cannot be set after creation; mode is locked at creation time"
70+
// +optional
6771
Namespace string `json:"namespace"`
6872

6973
// serviceAccount is a deprecated field and is completely ignored.
@@ -545,6 +549,13 @@ type ClusterExtensionStatus struct {
545549
// +optional
546550
Install *ClusterExtensionInstallStatus `json:"install,omitempty"`
547551

552+
// namespace is the resolved namespace where the extension is installed.
553+
// For user-provided namespaces, this mirrors spec.namespace.
554+
// For managed namespaces, this shows the name resolved from bundle metadata.
555+
//
556+
// +optional
557+
Namespace string `json:"namespace,omitempty"`
558+
548559
// activeRevisions holds a list of currently active (non-archived) ClusterObjectSets,
549560
// including both installed and rolling out revisions.
550561
// +listType=map

applyconfigurations/api/v1/clusterextensionspec.go

Lines changed: 8 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

applyconfigurations/api/v1/clusterextensionstatus.go

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

applyconfigurations/internal/internal.go

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/operator-controller/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,7 @@ func (c *boxcutterReconcilerConfigurator) Configure(ceReconciler *controllers.Cl
659659
controllers.RetrieveRevisionStates(revisionStatesGetter),
660660
controllers.ResolveBundle(c.resolver, c.mgr.GetClient()),
661661
controllers.UnpackBundle(c.imagePuller, c.imageCache),
662+
controllers.ResolveNamespace(coreClient),
662663
controllers.ApplyBundleWithBoxcutter(appl.Apply),
663664
}
664665

@@ -746,6 +747,7 @@ func (c *helmReconcilerConfigurator) Configure(ceReconciler *controllers.Cluster
746747
controllers.RetrieveRevisionStates(revisionStatesGetter),
747748
controllers.ResolveBundle(c.resolver, c.mgr.GetClient()),
748749
controllers.UnpackBundle(c.imagePuller, c.imageCache),
750+
controllers.ResolveNamespace(coreClient),
749751
controllers.ApplyBundle(appl),
750752
}
751753

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Managed Namespaces
2+
3+
## What is a managed namespace?
4+
5+
When you create a ClusterExtension without specifying `spec.namespace`, operator-controller automatically creates and manages a namespace for the operator. The namespace name comes from the bundle's metadata or defaults to `<packageName>-system`.
6+
7+
When you specify `spec.namespace`, the namespace must already exist on the cluster and operator-controller installs into it without managing its lifecycle.
8+
9+
The mode is locked at creation time: you cannot switch between managed and user-provided after the ClusterExtension is created.
10+
11+
## Namespace resolution
12+
13+
In managed mode, the namespace name is resolved from bundle CSV annotations in this order:
14+
15+
1. `operatorframework.io/suggested-namespace-template`: the `metadata.name` field from the JSON template
16+
2. `operators.operatorframework.io/suggested-namespace`: a plain string with the preferred name
17+
3. `<packageName>-system`: convention fallback
18+
19+
## What belongs in a managed namespace
20+
21+
- The operator's own workloads (deployments, services, configmaps)
22+
- The operator's RBAC resources (service accounts, roles, role bindings)
23+
- CRDs and webhooks installed by the operator
24+
25+
## What does NOT belong in a managed namespace
26+
27+
- User application workloads
28+
- Shared services used by multiple operators
29+
- Persistent data that should survive operator uninstallation
30+
31+
## Deletion behavior
32+
33+
Deleting a ClusterExtension with a managed namespace **deletes the entire namespace and everything in it.** If you have created resources in the managed namespace that are not part of the operator, they will be lost.
34+
35+
If you need the namespace to persist beyond the operator's lifecycle, use `spec.namespace` to point at an existing namespace you manage yourself.
36+
37+
## PSA labels
38+
39+
If the bundle declares PSA requirements via `operatorframework.io/suggested-namespace-template`, those labels are applied to the managed namespace automatically. This ensures the namespace has the correct Pod Security Admission level for the operator's workloads without manual configuration.
40+
41+
## Drift protection
42+
43+
Managed namespaces are reconciled by the ClusterObjectSet controller. If someone manually modifies or removes labels that the controller owns (e.g., PSA labels from the template), they are automatically restored.
44+
45+
Labels or annotations added by other actors that don't conflict with controller-owned fields are preserved.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Namespace Configuration for Bundle Authors
2+
3+
Bundle authors can specify their preferred namespace configuration through CSV annotations. These annotations are used by operator-controller when the cluster admin does not provide an explicit `spec.namespace`.
4+
5+
## Annotations
6+
7+
### `operatorframework.io/suggested-namespace-template`
8+
9+
Full namespace template with metadata. Use this when your operator needs specific labels or annotations on its namespace (e.g., PSA labels).
10+
11+
```yaml
12+
apiVersion: operators.coreos.com/v1alpha1
13+
kind: ClusterServiceVersion
14+
metadata:
15+
name: my-operator.v1.0.0
16+
annotations:
17+
operatorframework.io/suggested-namespace-template: |
18+
{
19+
"apiVersion": "v1",
20+
"kind": "Namespace",
21+
"metadata": {
22+
"name": "my-operator-system",
23+
"labels": {
24+
"pod-security.kubernetes.io/enforce": "privileged",
25+
"pod-security.kubernetes.io/audit": "privileged",
26+
"pod-security.kubernetes.io/warn": "privileged"
27+
}
28+
}
29+
}
30+
```
31+
32+
### `operators.operatorframework.io/suggested-namespace`
33+
34+
Simple namespace name without metadata. Use this when you want a specific name but don't need labels or annotations.
35+
36+
```yaml
37+
annotations:
38+
operators.operatorframework.io/suggested-namespace: my-operator-system
39+
```
40+
41+
### No annotation
42+
43+
If neither annotation is present, operator-controller uses `<packageName>-system` as the namespace name.
44+
45+
## Priority
46+
47+
If both annotations are present, `suggested-namespace-template` takes priority.
48+
49+
## Guidelines
50+
51+
- Always include PSA labels if your operator runs privileged containers.
52+
- Use a descriptive, unique namespace name that includes your package name to avoid collisions.
53+
- Do not assume the namespace name will be exactly what you suggest as cluster admins can override it by setting `spec.namespace`.
54+
- The namespace name from the template is used only when `spec.namespace` is omitted. When set, the admin's choice takes precedence and no namespace object is created.
55+
56+
## Consistency across bundle formats
57+
58+
The `operatorframework.io/suggested-namespace-template` and `operators.operatorframework.io/suggested-namespace` annotations are the canonical way to declare namespace preferences. Future bundle formats should use the same annotation keys to avoid divergence across the ecosystem.

helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensions.yaml

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -147,23 +147,29 @@ spec:
147147
rule: has(self.preflight)
148148
namespace:
149149
description: |-
150-
namespace specifies a Kubernetes namespace.
151-
It designates the default namespace where namespace-scoped resources for the extension are applied to the cluster.
152-
Some extensions may contain namespace-scoped resources to be applied in other namespaces.
153-
This namespace must exist.
150+
namespace is optional. When set, it specifies an existing namespace where
151+
namespace-scoped resources for the extension are applied. The namespace must
152+
already exist on the cluster. When omitted, operator-controller resolves and
153+
creates a managed namespace from bundle metadata.
154154
155-
The namespace field is required, immutable, and follows the DNS label standard as defined in [RFC 1123].
155+
The mode (set vs omitted) is locked at creation time and cannot be changed.
156+
When set, the value is immutable.
157+
158+
The namespace field follows the DNS label standard as defined in [RFC 1123].
156159
It must contain only lowercase alphanumeric characters or hyphens (-), start and end with an alphanumeric character,
157160
and be no longer than 63 characters.
158161
159162
[RFC 1123]: https://tools.ietf.org/html/rfc1123
160163
maxLength: 63
161164
type: string
162165
x-kubernetes-validations:
163-
- message: namespace is immutable
164-
rule: self == oldSelf
165166
- message: namespace must be a valid DNS1123 label
166-
rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")
167+
rule: self == '' || self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")
168+
- message: namespace is immutable once set
169+
rule: oldSelf == '' || self == oldSelf
170+
- message: namespace cannot be set after creation; mode is locked
171+
at creation time
172+
rule: oldSelf != '' || self == ''
167173
progressDeadlineMinutes:
168174
description: |-
169175
progressDeadlineMinutes is an optional field that defines the maximum period
@@ -493,7 +499,6 @@ spec:
493499
rule: 'has(self.sourceType) && self.sourceType == ''Catalog'' ?
494500
has(self.catalog) : !has(self.catalog)'
495501
required:
496-
- namespace
497502
- source
498503
type: object
499504
status:
@@ -725,6 +730,12 @@ spec:
725730
required:
726731
- bundle
727732
type: object
733+
namespace:
734+
description: |-
735+
namespace is the resolved namespace where the extension is installed.
736+
For user-provided namespaces, this mirrors spec.namespace.
737+
For managed namespaces, this shows the name resolved from bundle metadata.
738+
type: string
728739
type: object
729740
type: object
730741
served: true

helm/olmv1/base/operator-controller/crd/standard/olm.operatorframework.io_clusterextensions.yaml

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -109,23 +109,29 @@ spec:
109109
rule: has(self.preflight)
110110
namespace:
111111
description: |-
112-
namespace specifies a Kubernetes namespace.
113-
It designates the default namespace where namespace-scoped resources for the extension are applied to the cluster.
114-
Some extensions may contain namespace-scoped resources to be applied in other namespaces.
115-
This namespace must exist.
112+
namespace is optional. When set, it specifies an existing namespace where
113+
namespace-scoped resources for the extension are applied. The namespace must
114+
already exist on the cluster. When omitted, operator-controller resolves and
115+
creates a managed namespace from bundle metadata.
116116
117-
The namespace field is required, immutable, and follows the DNS label standard as defined in [RFC 1123].
117+
The mode (set vs omitted) is locked at creation time and cannot be changed.
118+
When set, the value is immutable.
119+
120+
The namespace field follows the DNS label standard as defined in [RFC 1123].
118121
It must contain only lowercase alphanumeric characters or hyphens (-), start and end with an alphanumeric character,
119122
and be no longer than 63 characters.
120123
121124
[RFC 1123]: https://tools.ietf.org/html/rfc1123
122125
maxLength: 63
123126
type: string
124127
x-kubernetes-validations:
125-
- message: namespace is immutable
126-
rule: self == oldSelf
127128
- message: namespace must be a valid DNS1123 label
128-
rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")
129+
rule: self == '' || self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")
130+
- message: namespace is immutable once set
131+
rule: oldSelf == '' || self == oldSelf
132+
- message: namespace cannot be set after creation; mode is locked
133+
at creation time
134+
rule: oldSelf != '' || self == ''
129135
serviceAccount:
130136
description: |-
131137
serviceAccount is a deprecated field and is completely ignored.
@@ -445,7 +451,6 @@ spec:
445451
rule: 'has(self.sourceType) && self.sourceType == ''Catalog'' ?
446452
has(self.catalog) : !has(self.catalog)'
447453
required:
448-
- namespace
449454
- source
450455
type: object
451456
status:
@@ -569,6 +574,12 @@ spec:
569574
required:
570575
- bundle
571576
type: object
577+
namespace:
578+
description: |-
579+
namespace is the resolved namespace where the extension is installed.
580+
For user-provided namespaces, this mirrors spec.namespace.
581+
For managed namespaces, this shows the name resolved from bundle metadata.
582+
type: string
572583
type: object
573584
type: object
574585
served: true

0 commit comments

Comments
 (0)