Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions api/v1/clusterextension_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,25 @@ const (

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

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

// namespace is the resolved namespace where the extension is installed.
// For user-provided namespaces, this mirrors spec.namespace.
// For managed namespaces, this shows the name resolved from bundle metadata.
//
// +optional
Namespace string `json:"namespace,omitempty"`

// activeRevisions holds a list of currently active (non-archived) ClusterObjectSets,
// including both installed and rolling out revisions.
// +listType=map
Expand Down
13 changes: 8 additions & 5 deletions applyconfigurations/api/v1/clusterextensionspec.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 13 additions & 1 deletion applyconfigurations/api/v1/clusterextensionstatus.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions applyconfigurations/internal/internal.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions cmd/operator-controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,7 @@ func (c *boxcutterReconcilerConfigurator) Configure(ceReconciler *controllers.Cl
controllers.RetrieveRevisionStates(revisionStatesGetter),
controllers.ResolveBundle(c.resolver, c.mgr.GetClient()),
controllers.UnpackBundle(c.imagePuller, c.imageCache),
controllers.ResolveNamespace(coreClient),
controllers.ApplyBundleWithBoxcutter(appl.Apply),
}

Expand Down Expand Up @@ -746,6 +747,7 @@ func (c *helmReconcilerConfigurator) Configure(ceReconciler *controllers.Cluster
controllers.RetrieveRevisionStates(revisionStatesGetter),
controllers.ResolveBundle(c.resolver, c.mgr.GetClient()),
controllers.UnpackBundle(c.imagePuller, c.imageCache),
controllers.ResolveNamespace(coreClient),
controllers.ApplyBundle(appl),
}
Comment thread
nader-ziada marked this conversation as resolved.

Expand Down
45 changes: 45 additions & 0 deletions docs/concepts/managed-namespaces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Managed Namespaces

## What is a managed namespace?

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`.

When you specify `spec.namespace`, the namespace must already exist on the cluster and operator-controller installs into it without managing its lifecycle.

The mode is locked at creation time: you cannot switch between managed and user-provided after the ClusterExtension is created.

## Namespace resolution

In managed mode, the namespace name is resolved from bundle CSV annotations in this order:

1. `operatorframework.io/suggested-namespace-template`: the `metadata.name` field from the JSON template
2. `operators.operatorframework.io/suggested-namespace`: a plain string with the preferred name
3. `<packageName>-system`: convention fallback

## What belongs in a managed namespace

- The operator's own workloads (deployments, services, configmaps)
- The operator's RBAC resources (service accounts, roles, role bindings)
- CRDs and webhooks installed by the operator

## What does NOT belong in a managed namespace

- User application workloads
- Shared services used by multiple operators
- Persistent data that should survive operator uninstallation

## Deletion behavior

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.

If you need the namespace to persist beyond the operator's lifecycle, use `spec.namespace` to point at an existing namespace you manage yourself.

## PSA labels

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.

## Drift protection

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.

Labels or annotations added by other actors that don't conflict with controller-owned fields are preserved.
58 changes: 58 additions & 0 deletions docs/howto/namespace-configuration-for-authors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Namespace Configuration for Bundle Authors

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`.

## Annotations

### `operatorframework.io/suggested-namespace-template`

Full namespace template with metadata. Use this when your operator needs specific labels or annotations on its namespace (e.g., PSA labels).

```yaml
apiVersion: operators.coreos.com/v1alpha1
kind: ClusterServiceVersion
metadata:
name: my-operator.v1.0.0
annotations:
operatorframework.io/suggested-namespace-template: |
{
"apiVersion": "v1",
"kind": "Namespace",
"metadata": {
"name": "my-operator-system",
"labels": {
"pod-security.kubernetes.io/enforce": "privileged",
"pod-security.kubernetes.io/audit": "privileged",
"pod-security.kubernetes.io/warn": "privileged"
}
}
}
```

### `operators.operatorframework.io/suggested-namespace`

Simple namespace name without metadata. Use this when you want a specific name but don't need labels or annotations.

```yaml
annotations:
operators.operatorframework.io/suggested-namespace: my-operator-system
```

### No annotation

If neither annotation is present, operator-controller uses `<packageName>-system` as the namespace name.

## Priority

If both annotations are present, `suggested-namespace-template` takes priority.

## Guidelines

- Always include PSA labels if your operator runs privileged containers.
- Use a descriptive, unique namespace name that includes your package name to avoid collisions.
- Do not assume the namespace name will be exactly what you suggest as cluster admins can override it by setting `spec.namespace`.
- 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.

## Consistency across bundle formats

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.
Original file line number Diff line number Diff line change
Expand Up @@ -147,23 +147,29 @@ spec:
rule: has(self.preflight)
namespace:
description: |-
namespace specifies a Kubernetes namespace.
It designates the default namespace where namespace-scoped resources for the extension are applied to the cluster.
Some extensions may contain namespace-scoped resources to be applied in other namespaces.
This namespace must exist.
namespace is optional. When set, it specifies an existing namespace where
namespace-scoped resources for the extension are applied. The namespace must
already exist on the cluster. When omitted, operator-controller resolves and
creates a managed namespace from bundle metadata.

The namespace field is required, immutable, and follows the DNS label standard as defined in [RFC 1123].
The mode (set vs omitted) is locked at creation time and cannot be changed.
When set, the value is immutable.

The namespace field follows the DNS label standard as defined in [RFC 1123].
It must contain only lowercase alphanumeric characters or hyphens (-), start and end with an alphanumeric character,
and be no longer than 63 characters.

[RFC 1123]: https://tools.ietf.org/html/rfc1123
maxLength: 63
type: string
x-kubernetes-validations:
- message: namespace is immutable
rule: self == oldSelf
- message: namespace must be a valid DNS1123 label
rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")
rule: self == '' || self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")
- message: namespace is immutable once set
rule: oldSelf == '' || self == oldSelf
- message: namespace cannot be set after creation; mode is locked
at creation time
rule: oldSelf != '' || self == ''
progressDeadlineMinutes:
description: |-
progressDeadlineMinutes is an optional field that defines the maximum period
Expand Down Expand Up @@ -493,7 +499,6 @@ spec:
rule: 'has(self.sourceType) && self.sourceType == ''Catalog'' ?
has(self.catalog) : !has(self.catalog)'
required:
- namespace
- source
type: object
status:
Expand Down Expand Up @@ -725,6 +730,12 @@ spec:
required:
- bundle
type: object
namespace:
description: |-
namespace is the resolved namespace where the extension is installed.
For user-provided namespaces, this mirrors spec.namespace.
For managed namespaces, this shows the name resolved from bundle metadata.
type: string
type: object
type: object
served: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,23 +109,29 @@ spec:
rule: has(self.preflight)
namespace:
description: |-
namespace specifies a Kubernetes namespace.
It designates the default namespace where namespace-scoped resources for the extension are applied to the cluster.
Some extensions may contain namespace-scoped resources to be applied in other namespaces.
This namespace must exist.
namespace is optional. When set, it specifies an existing namespace where
namespace-scoped resources for the extension are applied. The namespace must
already exist on the cluster. When omitted, operator-controller resolves and
creates a managed namespace from bundle metadata.

The namespace field is required, immutable, and follows the DNS label standard as defined in [RFC 1123].
The mode (set vs omitted) is locked at creation time and cannot be changed.
When set, the value is immutable.

The namespace field follows the DNS label standard as defined in [RFC 1123].
It must contain only lowercase alphanumeric characters or hyphens (-), start and end with an alphanumeric character,
and be no longer than 63 characters.

[RFC 1123]: https://tools.ietf.org/html/rfc1123
maxLength: 63
type: string
x-kubernetes-validations:
- message: namespace is immutable
rule: self == oldSelf
- message: namespace must be a valid DNS1123 label
rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")
rule: self == '' || self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")
- message: namespace is immutable once set
rule: oldSelf == '' || self == oldSelf
- message: namespace cannot be set after creation; mode is locked
at creation time
rule: oldSelf != '' || self == ''
serviceAccount:
description: |-
serviceAccount is a deprecated field and is completely ignored.
Expand Down Expand Up @@ -445,7 +451,6 @@ spec:
rule: 'has(self.sourceType) && self.sourceType == ''Catalog'' ?
has(self.catalog) : !has(self.catalog)'
required:
- namespace
- source
type: object
status:
Expand Down Expand Up @@ -569,6 +574,12 @@ spec:
required:
- bundle
type: object
namespace:
description: |-
namespace is the resolved namespace where the extension is installed.
For user-provided namespaces, this mirrors spec.namespace.
For managed namespaces, this shows the name resolved from bundle metadata.
type: string
type: object
type: object
served: true
Expand Down
Loading