From 20f8419f3300b31c880f3d9c7033bd25ccd925e8 Mon Sep 17 00:00:00 2001 From: Tomba Leishangthem <10569680+tomba7@users.noreply.github.com> Date: Wed, 4 Nov 2026 13:59:56 -0800 Subject: [PATCH] feat(admission): validate adoption annotations with admission policies --- api/cloud/v1alpha1/namespace.go | 18 +++--- .../cloud_resource_validation_policy.yaml | 55 +++++++++++++++++++ deploy/kustomize/admission/kustomization.yaml | 3 + .../namespace_validation_policy.yaml | 34 ++++++++++++ .../bases/cloud.temporal.io_namespaces.yaml | 12 ++-- deploy/kustomize/default/kustomization.yaml | 1 + ...amespace-adopt-or-create-without-spec.yaml | 8 +++ .../adoption/namespace-adopt-or-create.yaml | 14 +++++ .../adoption/namespace-adopt-with-spec.yaml | 12 ++++ .../namespace-adopt-without-resource-id.yaml | 7 +++ .../namespace-adopt-without-spec.yaml | 8 +++ .../namespace-invalid-adoption-policy.yaml | 13 +++++ 12 files changed, 172 insertions(+), 13 deletions(-) create mode 100644 deploy/kustomize/admission/cloud_resource_validation_policy.yaml create mode 100644 deploy/kustomize/admission/kustomization.yaml create mode 100644 deploy/kustomize/admission/namespace_validation_policy.yaml create mode 100644 examples/adoption/namespace-adopt-or-create-without-spec.yaml create mode 100644 examples/adoption/namespace-adopt-or-create.yaml create mode 100644 examples/adoption/namespace-adopt-with-spec.yaml create mode 100644 examples/adoption/namespace-adopt-without-resource-id.yaml create mode 100644 examples/adoption/namespace-adopt-without-spec.yaml create mode 100644 examples/adoption/namespace-invalid-adoption-policy.yaml diff --git a/api/cloud/v1alpha1/namespace.go b/api/cloud/v1alpha1/namespace.go index 38b6580..669d05b 100644 --- a/api/cloud/v1alpha1/namespace.go +++ b/api/cloud/v1alpha1/namespace.go @@ -26,9 +26,11 @@ type Namespace struct { // +optional metav1.ObjectMeta `json:"metadata,omitempty"` - // Spec defines the desired state of the Namespace. - // +required - Spec NamespaceSpec `json:"spec"` + // Spec defines the desired state of the Namespace. It may be omitted only + // when adopting an existing Temporal Cloud Namespace, i.e. when the + // cloud.temporal.io/adoption-policy annotation is "adopt". + // +optional + Spec NamespaceSpec `json:"spec,omitzero"` // +optional // Status describes the current status of a Namespace. Status NamespaceStatus `json:"status,omitempty"` @@ -87,16 +89,18 @@ type NamespaceSpec struct { Tags map[string]string `json:"tags,omitempty"` // Replicas are the regions where the Namespace is placed. // Maps to Temporal Cloud proto `NamespaceSpec.replicas`. - // +required + // Required and is enforced by the temporal-cloud-namespace-validation admission policy. + // +optional // +listType=map // +listMapKey=region // +kubebuilder:validation:MinItems=1 - Replicas []ReplicaSpec `json:"replicas"` + Replicas []ReplicaSpec `json:"replicas,omitempty"` // RetentionDays is the number of days that closed workflow histories are // kept. // Maps to Temporal Cloud proto `NamespaceSpec.retention_days`. - // +required - RetentionDays int32 `json:"retentionDays"` + // Required and is enforced by the temporal-cloud-namespace-validation admission policy. + // +optional + RetentionDays int32 `json:"retentionDays,omitempty"` // MTLSAuth configures mTLS client authentication. // Maps to Temporal Cloud proto `NamespaceSpec.mtls_auth`. // +optional diff --git a/deploy/kustomize/admission/cloud_resource_validation_policy.yaml b/deploy/kustomize/admission/cloud_resource_validation_policy.yaml new file mode 100644 index 0000000..b70d0e2 --- /dev/null +++ b/deploy/kustomize/admission/cloud_resource_validation_policy.yaml @@ -0,0 +1,55 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: temporal-cloud-resource-validation +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: ["cloud.temporal.io"] + apiVersions: ["*"] + operations: ["CREATE", "UPDATE"] + resources: ["namespaces"] + variables: + - name: adoptionPolicy + expression: "object.?metadata.annotations['cloud.temporal.io/adoption-policy']" + - name: adopt + expression: "variables.adoptionPolicy.orValue('') == 'adopt'" + - name: resourceID + expression: "object.?metadata.annotations['cloud.temporal.io/resource-id'].orValue('')" + validations: + # No adoption annotation or if there is, it has to be either of the two values + - expression: >- + !variables.adoptionPolicy.hasValue() || + variables.adoptionPolicy.value() in ['adopt', 'adopt-or-create'] + messageExpression: >- + 'metadata.annotations[cloud.temporal.io/adoption-policy]: Unsupported value: "' + + variables.adoptionPolicy.value() + '": supported values: "adopt", "adopt-or-create"' + reason: Invalid + # If adoption is requested, a resource ID must be set. + - expression: >- + !(variables.adoptionPolicy.orValue('') in ['adopt', 'adopt-or-create']) || + variables.resourceID != '' + message: >- + metadata.annotations[cloud.temporal.io/resource-id]: Required value when the + cloud.temporal.io/adoption-policy annotation is "adopt" or "adopt-or-create" + reason: Invalid + # A resource ID is only used for adoption. + - expression: "variables.adoptionPolicy.hasValue() || variables.resourceID == ''" + message: >- + metadata.annotations[cloud.temporal.io/resource-id]: Forbidden: requires the + cloud.temporal.io/adoption-policy annotation + reason: Invalid + - expression: "variables.adopt || has(object.spec)" + message: >- + spec: Required value unless the cloud.temporal.io/adoption-policy + annotation is "adopt" + reason: Invalid +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: temporal-cloud-resource-validation +spec: + policyName: temporal-cloud-resource-validation + validationActions: [Deny] diff --git a/deploy/kustomize/admission/kustomization.yaml b/deploy/kustomize/admission/kustomization.yaml new file mode 100644 index 0000000..31c269f --- /dev/null +++ b/deploy/kustomize/admission/kustomization.yaml @@ -0,0 +1,3 @@ +resources: +- cloud_resource_validation_policy.yaml +- namespace_validation_policy.yaml diff --git a/deploy/kustomize/admission/namespace_validation_policy.yaml b/deploy/kustomize/admission/namespace_validation_policy.yaml new file mode 100644 index 0000000..f74a3ad --- /dev/null +++ b/deploy/kustomize/admission/namespace_validation_policy.yaml @@ -0,0 +1,34 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: temporal-cloud-namespace-validation +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: ["cloud.temporal.io"] + apiVersions: ["*"] + operations: ["CREATE", "UPDATE"] + resources: ["namespaces"] + variables: + - name: adopt + expression: "object.?metadata.annotations['cloud.temporal.io/adoption-policy'].orValue('') == 'adopt'" + - name: missingSpecFields + expression: >- + !has(object.spec) ? [] : + (has(object.spec.replicas) ? [] : ['spec.replicas']) + + (has(object.spec.retentionDays) ? [] : ['spec.retentionDays']) + validations: + - expression: "size(variables.missingSpecFields) == 0" + messageExpression: >- + variables.missingSpecFields.join(', ') + ': Required value' + + (variables.adopt ? '; to adopt the Namespace as is, omit spec' : '') + reason: Invalid +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: temporal-cloud-namespace-validation +spec: + policyName: temporal-cloud-namespace-validation + validationActions: [Deny] diff --git a/deploy/kustomize/crd/bases/cloud.temporal.io_namespaces.yaml b/deploy/kustomize/crd/bases/cloud.temporal.io_namespaces.yaml index e4cc6a4..78d574a 100644 --- a/deploy/kustomize/crd/bases/cloud.temporal.io_namespaces.yaml +++ b/deploy/kustomize/crd/bases/cloud.temporal.io_namespaces.yaml @@ -50,7 +50,10 @@ spec: metadata: type: object spec: - description: Spec defines the desired state of the Namespace. + description: |- + Spec defines the desired state of the Namespace. It may be omitted only + when adopting an existing Temporal Cloud Namespace, i.e. when the + cloud.temporal.io/adoption-policy annotation is "adopt". properties: apiKeyAuth: description: |- @@ -294,6 +297,7 @@ spec: description: |- Replicas are the regions where the Namespace is placed. Maps to Temporal Cloud proto `NamespaceSpec.replicas`. + Required and is enforced by the temporal-cloud-namespace-validation admission policy. items: description: ReplicaSpec places a replica of the Namespace in a region. @@ -316,6 +320,7 @@ spec: RetentionDays is the number of days that closed workflow histories are kept. Maps to Temporal Cloud proto `NamespaceSpec.retention_days`. + Required and is enforced by the temporal-cloud-namespace-validation admission policy. format: int32 type: integer searchAttributes: @@ -343,9 +348,6 @@ spec: Tags are key-value pairs to attach to the Namespace. Maps to Temporal Cloud proto `Namespace.tags`. type: object - required: - - replicas - - retentionDays type: object x-kubernetes-validations: - fieldPath: .project @@ -470,8 +472,6 @@ spec: - Expired type: string type: object - required: - - spec type: object served: true storage: true diff --git a/deploy/kustomize/default/kustomization.yaml b/deploy/kustomize/default/kustomization.yaml index ed3e705..660a4ae 100644 --- a/deploy/kustomize/default/kustomization.yaml +++ b/deploy/kustomize/default/kustomization.yaml @@ -11,5 +11,6 @@ labels: resources: - namespace.yaml - ../crd +- ../admission - ../rbac - ../cloud-operator diff --git a/examples/adoption/namespace-adopt-or-create-without-spec.yaml b/examples/adoption/namespace-adopt-or-create-without-spec.yaml new file mode 100644 index 0000000..104858c --- /dev/null +++ b/examples/adoption/namespace-adopt-or-create-without-spec.yaml @@ -0,0 +1,8 @@ +# Fails: adopt-or-create may create the Namespace, so spec is required. +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: adopt-or-create-without-spec + annotations: + cloud.temporal.io/adoption-policy: adopt-or-create + cloud.temporal.io/resource-id: adopt-or-create-without-spec.abc12 diff --git a/examples/adoption/namespace-adopt-or-create.yaml b/examples/adoption/namespace-adopt-or-create.yaml new file mode 100644 index 0000000..0df5689 --- /dev/null +++ b/examples/adoption/namespace-adopt-or-create.yaml @@ -0,0 +1,14 @@ +# Passes: adopt-or-create with a resource ID and a full spec to create from. +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: adopt-or-create + annotations: + cloud.temporal.io/adoption-policy: adopt-or-create + cloud.temporal.io/resource-id: adopt-or-create.abc12 +spec: + replicas: + - region: aws-us-west-2 + retentionDays: 30 + apiKeyAuth: + enabled: true diff --git a/examples/adoption/namespace-adopt-with-spec.yaml b/examples/adoption/namespace-adopt-with-spec.yaml new file mode 100644 index 0000000..479e908 --- /dev/null +++ b/examples/adoption/namespace-adopt-with-spec.yaml @@ -0,0 +1,12 @@ +# Fails: once spec is set, replicas and retentionDays are required, even when adopting. +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: adopt-with-spec + annotations: + cloud.temporal.io/adoption-policy: adopt + cloud.temporal.io/resource-id: adopt-with-spec.abc12 +spec: + description: Payments namespace + tags: + team: payments diff --git a/examples/adoption/namespace-adopt-without-resource-id.yaml b/examples/adoption/namespace-adopt-without-resource-id.yaml new file mode 100644 index 0000000..0a689c9 --- /dev/null +++ b/examples/adoption/namespace-adopt-without-resource-id.yaml @@ -0,0 +1,7 @@ +# Fails: adopting requires the cloud.temporal.io/resource-id annotation. +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: adopt-without-resource-id + annotations: + cloud.temporal.io/adoption-policy: adopt diff --git a/examples/adoption/namespace-adopt-without-spec.yaml b/examples/adoption/namespace-adopt-without-spec.yaml new file mode 100644 index 0000000..d2febf0 --- /dev/null +++ b/examples/adoption/namespace-adopt-without-spec.yaml @@ -0,0 +1,8 @@ +# Passes: spec may be omitted when adopting with a resource ID. +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: adopt-without-spec + annotations: + cloud.temporal.io/adoption-policy: adopt + cloud.temporal.io/resource-id: adopt-without-spec.abc12 diff --git a/examples/adoption/namespace-invalid-adoption-policy.yaml b/examples/adoption/namespace-invalid-adoption-policy.yaml new file mode 100644 index 0000000..4a86ba9 --- /dev/null +++ b/examples/adoption/namespace-invalid-adoption-policy.yaml @@ -0,0 +1,13 @@ +# Fails: the adoption policy must be "adopt" or "adopt-or-create". +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: invalid-adoption-policy + annotations: + cloud.temporal.io/adoption-policy: create-new +spec: + replicas: + - region: aws-us-west-2 + retentionDays: 30 + apiKeyAuth: + enabled: true