From 9ca6e1b0a866a2cd78d61d982a70d800e28d5c5b Mon Sep 17 00:00:00 2001 From: Tomba Leishangthem <10569680+tomba7@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:57:58 -0700 Subject: [PATCH 1/4] feat(crd): define Namespace spec and validation --- .gitignore | 1 + .mk/gen.mk | 9 +- .mk/test.mk | 6 +- README.md | 71 ++++ api/cloud/v1alpha1/namespace.go | 295 ++++++++++++++- api/cloud/v1alpha1/zz_generated.deepcopy.go | 313 ++++++++++++++++ .../bases/cloud.temporal.io_namespaces.yaml | 342 +++++++++++++++++- examples/namespace.yaml | 27 ++ test/e2e/namespace/happy-default.yaml | 14 +- test/e2e/namespace/testdata/minimal.yaml | 12 + test/e2e/namespace/validation.yaml | 269 ++++++++++++++ 11 files changed, 1338 insertions(+), 21 deletions(-) create mode 100644 examples/namespace.yaml create mode 100644 test/e2e/namespace/testdata/minimal.yaml create mode 100644 test/e2e/namespace/validation.yaml diff --git a/.gitignore b/.gitignore index 6b7c331..1640eff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.idea *.swp *.swo *~ diff --git a/.mk/gen.mk b/.mk/gen.mk index de096f2..a8b46e7 100644 --- a/.mk/gen.mk +++ b/.mk/gen.mk @@ -45,17 +45,10 @@ gen-kube-register: $(KUBE_CODEGEN_PATH) ## Generate API scheme and groupversion .PHONY: gen-kube-manifests CONTROLLER_ROLE_NAME ?= temporal-resource-operator -# Note that the option maxDescLen=0 was added in the default scaffold in -# order to sort out the issue Too long: must have at most 262144 bytes. By -# using kubectl apply to create / update resources an annotation is created -# by K8s API to store the latest version of the resource ( -# kubectl.kubernetes.io/last-applied-configuration). However, it has a -# size limit and if the CRD is too big with so many long descriptions as -# this one it will cause the failure. gen-kube-manifests: $(CONTROLLER_GEN) ## Generate WebhookConfiguration, ClusterRole objects. @echo -n "generating Kubernetes manifests ... " @$(CONTROLLER_GEN) rbac:roleName=$(CONTROLLER_ROLE_NAME) \ - crd:maxDescLen=0 webhook paths="./api/...;./controller/..." \ + crd webhook paths="./api/...;./controller/..." \ output:crd:dir=deploy/kustomize/crd/bases \ output:rbac:dir=deploy/kustomize/rbac @echo "ok." diff --git a/.mk/test.mk b/.mk/test.mk index 67d5bb7..781e6a2 100644 --- a/.mk/test.mk +++ b/.mk/test.mk @@ -93,7 +93,7 @@ test-clean-build: test-clean: test-clean-build test-cluster-delete ## Clean up the test environment. .PHONY: test-e2e -test-e2e: test-e2e-install test-e2e-namespace ## Run all end to end tests. +test-e2e: test-e2e-install test-e2e-namespace-validation test-e2e-namespace ## Run all end to end tests. .PHONY: test-e2e-install test-e2e-install: $(GDT) ## Test that expected artifacts are all present in test cluster. @@ -102,3 +102,7 @@ test-e2e-install: $(GDT) ## Test that expected artifacts are all present in test .PHONY: test-e2e-namespace test-e2e-namespace: $(GDT) ## Test lifecycle management of Temporal Cloud Namespace by the temporal-resource-operator. @TEST_USER_NAMESPACE=tro-e2e-cluster $(GDT) $(TEST_GDT_ARGS) run test/e2e/namespace/happy-default.yaml + +.PHONY: test-e2e-namespace-validation +test-e2e-namespace-validation: $(GDT) ## Test that the Namespace CRD accepts valid and rejects invalid Namespace manifests. + @TEST_USER_NAMESPACE=tro-e2e-validation $(GDT) $(TEST_GDT_ARGS) run test/e2e/namespace/validation.yaml diff --git a/README.md b/README.md index da957f7..c54fa01 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,12 @@ kind: Namespace metadata: name: my-org-namespace namespace: temporal-cloud # NOTE: this is the Kubernetes Namespace, not the Temporal Cloud Namespace. +spec: + replicas: + - region: aws-us-west-2 + retentionDays: 30 + apiKeyAuth: + enabled: true EOF ``` @@ -58,6 +64,63 @@ $ kubectl apply -n temporal-resource-operator -k "https://github.com/temporalio/ ## Create a new Temporal Cloud Namespace +Describe the Temporal Cloud Namespace in a `Namespace` manifest, such as +[`examples/namespace.yaml`](examples/namespace.yaml): + +```yaml +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: payments-prod +spec: + tags: + environment: production + team: payments + replicas: + - region: aws-us-west-2 + retentionDays: 30 + apiKeyAuth: + enabled: true + capacity: + onDemand: {} + description: Payment workflows for production. +``` + +Apply it to the Kubernetes Namespace that holds your Temporal Cloud +resources: + +```bash +$ kubectl apply -n temporal-cloud -f examples/namespace.yaml +namespace.cloud.temporal.io/payments-prod created +``` + +The Temporal Cloud Namespace takes its name from `metadata.name`, which must +be a valid Temporal Cloud Namespace name. To list every `spec` field with the +Temporal Cloud field it maps to, run: + +```bash +$ kubectl explain namespaces.spec --api-version=cloud.temporal.io/v1alpha1 +``` + +The Kubernetes API server checks the structure of the manifest when you apply +it: + +```bash +$ kubectl apply -n temporal-cloud -f examples/namespace.yaml # with both onDemand and provisioned capacity +The Namespace "payments-prod" is invalid: spec.capacity: Invalid value: exactly one of onDemand or provisioned must be set +``` + +The API server checks that: + +* `replicas` and `retentionDays` are set, and `replicas` is not empty. +* Enum values are valid, replica regions are unique, and exactly one capacity + mode is set. +* Updates do not change `projectID`, and do not remove search attributes or + change their type. + +Temporal Cloud reports all other errors, such as invalid tags or a retention +period out of range. + ## Modify a Temporal Cloud Namespace ## Delete a Temporal Cloud Namespace @@ -181,4 +244,12 @@ manages the lifecycle of Temporal Cloud resources: make test-e2e-cluster ``` +To run functional tests that verify that the Namespace CRD accepts valid +manifests, including `examples/namespace.yaml`, and rejects invalid ones at +`kubectl apply`: + +``` +make test-e2e-namespace-validation +``` + [gdt]: https://github.com/gdt-dev/gdt diff --git a/api/cloud/v1alpha1/namespace.go b/api/cloud/v1alpha1/namespace.go index cfc902c..5566e9b 100644 --- a/api/cloud/v1alpha1/namespace.go +++ b/api/cloud/v1alpha1/namespace.go @@ -9,28 +9,303 @@ import ( // Namespace represents an instance of a Temporal Cloud Namespace. // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:subresource:status type Namespace struct { - metav1.TypeMeta `json:",inline"` + metav1.TypeMeta `json:",inline"` + // +optional metav1.ObjectMeta `json:"metadata,omitempty"` - Spec NamespaceSpec `json:"spec,omitempty"` + // Spec defines the desired state of the Namespace. + // +required + Spec NamespaceSpec `json:"spec"` + // +optional + // Status describes the current status of a Namespace. Status NamespaceStatus `json:"status,omitempty"` } // NamespaceSpec describes the desired state of a Temporal Cloud Namespace. // -// Ref: https://saas-api.tmprl.cloud/docs/httpapi.html#tag/namespaces/POST/cloud/namespaces +// Ref: https://github.com/temporalio/cloud-api/blob/main/temporal/api/cloud/namespace/v1/message.proto // +k8s:openapi-gen=true +// +kubebuilder:validation:XValidation:rule="has(self.projectID) == has(oldSelf.projectID) && (!has(self.projectID) || self.projectID == oldSelf.projectID)",message="projectID cannot be added, changed or removed after creation",fieldPath=".projectID" +// +kubebuilder:validation:XValidation:rule="!has(oldSelf.searchAttributes) || (has(self.searchAttributes) && oldSelf.searchAttributes.all(k, k in self.searchAttributes && self.searchAttributes[k] == oldSelf.searchAttributes[k]))",message="searchAttributes cannot be removed or have their type changed",fieldPath=".searchAttributes" type NamespaceSpec struct { - // ProjectID is the The id of the project in which the Temporal Cloud - // Namespace belongs. If not set, defaults to the account's default - // project. + // ProjectID is the ID of the Temporal Cloud project that the Namespace + // belongs to. + // Maps to Temporal Cloud proto `Namespace.project_id`. + // +optional + // +kubebuilder:validation:MinLength=1 ProjectID string `json:"projectID,omitempty"` - // Tags is the collection of string tags to attach to the Namespace. - Tags []string `json:"tags,omitempty"` + // Tags are key-value pairs to attach to the Namespace. + // Maps to Temporal Cloud proto `Namespace.tags`. + // +optional + Tags map[string]string `json:"tags,omitempty"` + // Replicas are the regions where the Namespace is placed. + // Maps to Temporal Cloud proto `NamespaceSpec.replicas`. + // +required + // +listType=map + // +listMapKey=region + // +kubebuilder:validation:MinItems=1 + Replicas []ReplicaSpec `json:"replicas"` + // 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"` + // MTLSAuth configures mTLS client authentication. + // Maps to Temporal Cloud proto `NamespaceSpec.mtls_auth`. + // +optional + MTLSAuth *MTLSAuthSpec `json:"mtlsAuth,omitempty"` + // APIKeyAuth configures API key client authentication. + // Maps to Temporal Cloud proto `NamespaceSpec.api_key_auth`. + // +optional + APIKeyAuth *APIKeyAuthSpec `json:"apiKeyAuth,omitempty"` + // SearchAttributes are the custom search attributes of the Namespace, + // keyed by name. + // Maps to Temporal Cloud proto `NamespaceSpec.search_attributes`. + // +optional + SearchAttributes map[string]SearchAttributeType `json:"searchAttributes,omitempty"` + // CodecServer configures the codec server used by the Temporal Cloud UI. + // Maps to Temporal Cloud proto `NamespaceSpec.codec_server`. + // +optional + CodecServer *CodecServerSpec `json:"codecServer,omitempty"` + // Lifecycle configures lifecycle settings. + // Maps to Temporal Cloud proto `NamespaceSpec.lifecycle`. + // +optional + Lifecycle *LifecycleSpec `json:"lifecycle,omitempty"` + // HighAvailability configures failover behavior. + // Maps to Temporal Cloud proto `NamespaceSpec.high_availability`. + // +optional + HighAvailability *HighAvailabilitySpec `json:"highAvailability,omitempty"` + // ConnectivityRuleIDs are the IDs of the connectivity rules to apply. + // Maps to Temporal Cloud proto `NamespaceSpec.connectivity_rule_ids`. + // +optional + // +listType=set + ConnectivityRuleIDs []string `json:"connectivityRuleIDs,omitempty"` + // Capacity selects the capacity mode. + // Maps to Temporal Cloud proto `NamespaceSpec.capacity_spec`. + // +optional + Capacity *CapacitySpec `json:"capacity,omitempty"` + // Fairness configures task queue fairness. + // Maps to Temporal Cloud proto `NamespaceSpec.fairness`. + // +optional + Fairness *FairnessSpec `json:"fairness,omitempty"` + // EncryptionValidation configures payload encryption validation. + // Maps to Temporal Cloud proto `NamespaceSpec.encryption_validation`. + // +optional + EncryptionValidation *EncryptionValidationSpec `json:"encryptionValidation,omitempty"` + // Description is a human-readable description of the Namespace. + // Maps to Temporal Cloud proto `NamespaceSpec.description`. + // +optional + Description string `json:"description,omitempty"` +} + +// ReplicaSpec places a replica of the Namespace in a region. +type ReplicaSpec struct { + // Region is the ID of the region to place the replica in. + // Maps to Temporal Cloud proto `ReplicaSpec.region`. + // +required + Region string `json:"region"` +} + +// MTLSAuthSpec configures mTLS client authentication for a Namespace. +type MTLSAuthSpec struct { + // Enabled turns mTLS authentication on or off. + // Maps to Temporal Cloud proto `MtlsAuthSpec.enabled`. + // +required + Enabled bool `json:"enabled"` + // AcceptedClientCA is the PEM-encoded CA certificate, or chain, that + // client certificates must be issued by. + // Maps to Temporal Cloud proto `MtlsAuthSpec.accepted_client_ca`. + // +optional + AcceptedClientCA string `json:"acceptedClientCA,omitempty"` + // CertificateFilters restrict client certificates by subject. + // Maps to Temporal Cloud proto `MtlsAuthSpec.certificate_filters`. + // +optional + CertificateFilters []CertificateFilterSpec `json:"certificateFilters,omitempty"` +} + +// CertificateFilterSpec matches client certificates by subject fields. +type CertificateFilterSpec struct { + // CommonName is the certificate's common name. + // Maps to Temporal Cloud proto `CertificateFilterSpec.common_name`. + // +optional + CommonName string `json:"commonName,omitempty"` + // Organization is the certificate's organization. + // Maps to Temporal Cloud proto `CertificateFilterSpec.organization`. + // +optional + Organization string `json:"organization,omitempty"` + // OrganizationalUnit is the certificate's organizational unit. + // Maps to Temporal Cloud proto `CertificateFilterSpec.organizational_unit`. + // +optional + OrganizationalUnit string `json:"organizationalUnit,omitempty"` + // SubjectAlternativeName is the certificate's subject alternative name. + // Maps to Temporal Cloud proto `CertificateFilterSpec.subject_alternative_name`. + // +optional + SubjectAlternativeName string `json:"subjectAlternativeName,omitempty"` +} + +// APIKeyAuthSpec configures API key client authentication for a Namespace. +type APIKeyAuthSpec struct { + // Enabled turns API key authentication on or off. + // Maps to Temporal Cloud proto `ApiKeyAuthSpec.enabled`. + // +required + Enabled bool `json:"enabled"` +} + +// SearchAttributeType is the type of a custom search attribute. +// +kubebuilder:validation:Enum=Text;Keyword;Int;Double;Bool;Datetime;KeywordList +type SearchAttributeType string + +const ( + SearchAttributeTypeText SearchAttributeType = "Text" + SearchAttributeTypeKeyword SearchAttributeType = "Keyword" + SearchAttributeTypeInt SearchAttributeType = "Int" + SearchAttributeTypeDouble SearchAttributeType = "Double" + SearchAttributeTypeBool SearchAttributeType = "Bool" + SearchAttributeTypeDatetime SearchAttributeType = "Datetime" + SearchAttributeTypeKeywordList SearchAttributeType = "KeywordList" +) + +// CodecServerSpec configures the codec server used by the Temporal Cloud UI. +type CodecServerSpec struct { + // Endpoint is the URL of the codec server. + // Maps to Temporal Cloud proto `CodecServerSpec.endpoint`. + // +required + Endpoint string `json:"endpoint"` + // PassAccessToken passes the user's access token to the endpoint. + // Maps to Temporal Cloud proto `CodecServerSpec.pass_access_token`. + // +optional + PassAccessToken bool `json:"passAccessToken,omitempty"` + // IncludeCrossOriginCredentials includes cross-origin credentials in + // requests to the endpoint. + // Maps to Temporal Cloud proto `CodecServerSpec.include_cross_origin_credentials`. + // +optional + IncludeCrossOriginCredentials bool `json:"includeCrossOriginCredentials,omitempty"` + // CustomErrorMessage is shown in the UI when the codec server returns an + // error. + // Maps to Temporal Cloud proto `CodecServerSpec.custom_error_message`. + // +optional + CustomErrorMessage *CodecServerCustomErrorMessage `json:"customErrorMessage,omitempty"` +} + +// CodecServerCustomErrorMessage configures the messages shown for codec +// server errors. +type CodecServerCustomErrorMessage struct { + // Default is shown for any codec server error. + // Maps to Temporal Cloud proto `CodecServerSpec.CustomErrorMessage.default`. + // +optional + Default *CodecServerErrorMessage `json:"default,omitempty"` +} + +// CodecServerErrorMessage is a message shown for codec server errors. +type CodecServerErrorMessage struct { + // Message is the text to show. + // Maps to Temporal Cloud proto `CodecServerSpec.CustomErrorMessage.ErrorMessage.message`. + // +optional + Message string `json:"message,omitempty"` + // Link is a URL shown next to the message. + // Maps to Temporal Cloud proto `CodecServerSpec.CustomErrorMessage.ErrorMessage.link`. + // +optional + Link string `json:"link,omitempty"` +} + +// LifecycleSpec configures lifecycle settings for a Namespace. +type LifecycleSpec struct { + // EnableDeleteProtection prevents the Namespace from being deleted. + // Maps to Temporal Cloud proto `LifecycleSpec.enable_delete_protection`. + // +optional + EnableDeleteProtection bool `json:"enableDeleteProtection,omitempty"` +} + +// HighAvailabilitySpec configures failover behavior for a Namespace. +type HighAvailabilitySpec struct { + // DisableManagedFailover turns off failover managed by Temporal Cloud. + // Maps to Temporal Cloud proto `HighAvailabilitySpec.disable_managed_failover`. + // +optional + DisableManagedFailover bool `json:"disableManagedFailover,omitempty"` + // DisablePassivePollerForwarding turns off forwarding of polls from + // passive replicas to the active replica. + // Maps to Temporal Cloud proto `HighAvailabilitySpec.disable_passive_poller_forwarding`. + // +optional + DisablePassivePollerForwarding bool `json:"disablePassivePollerForwarding,omitempty"` } -// NamespaceStatus is the status for a Namespace resource +// CapacitySpec selects the capacity mode of a Namespace. +// +kubebuilder:validation:XValidation:rule="has(self.onDemand) != has(self.provisioned)",message="exactly one of onDemand or provisioned must be set" +type CapacitySpec struct { + // OnDemand selects the on-demand capacity mode. + // Maps to Temporal Cloud proto `CapacitySpec.on_demand`. + // +optional + OnDemand *OnDemandCapacity `json:"onDemand,omitempty"` + // Provisioned selects the provisioned capacity mode. + // Maps to Temporal Cloud proto `CapacitySpec.provisioned`. + // +optional + Provisioned *ProvisionedCapacity `json:"provisioned,omitempty"` +} + +// OnDemandCapacity selects the on-demand capacity mode. +type OnDemandCapacity struct{} + +// ProvisionedCapacity selects the provisioned capacity mode. +type ProvisionedCapacity struct { + // Value is the capacity in Temporal Resource Units (TRUs). + // Maps to Temporal Cloud proto `CapacitySpec.Provisioned.value`. + // +required + Value int32 `json:"value"` +} + +// FairnessSpec configures task queue fairness for a Namespace. +type FairnessSpec struct { + // TaskQueueFairnessEnabled turns on task queue fairness. + // Maps to Temporal Cloud proto `FairnessSpec.task_queue_fairness_enabled`. + // +optional + TaskQueueFairnessEnabled bool `json:"taskQueueFairnessEnabled,omitempty"` +} + +// EncryptionValidationSpec configures payload encryption validation for a +// Namespace. +type EncryptionValidationSpec struct { + // Mode is the payload encryption validation mode. + // Maps to Temporal Cloud proto `EncryptionValidationSpec.mode`. + // +required + Mode EncryptionValidationMode `json:"mode"` + // MetadataKey is the payload metadata key that identifies encrypted + // payloads. + // Maps to Temporal Cloud proto `EncryptionValidationSpec.metadata_key`. + // +optional + MetadataKey string `json:"metadataKey,omitempty"` + // MetadataValues are the payload metadata values that identify encrypted + // payloads. + // Maps to Temporal Cloud proto `EncryptionValidationSpec.metadata_values`. + // +optional + MetadataValues []string `json:"metadataValues,omitempty"` + // InspectHeader includes workflow headers in validation. + // Maps to Temporal Cloud proto `EncryptionValidationSpec.inspect_header`. + // +optional + InspectHeader bool `json:"inspectHeader,omitempty"` + // InspectFailure includes failure messages and stack traces in + // validation. + // Maps to Temporal Cloud proto `EncryptionValidationSpec.inspect_failure`. + // +optional + InspectFailure bool `json:"inspectFailure,omitempty"` +} + +// EncryptionValidationMode is the payload encryption validation mode. +// +kubebuilder:validation:Enum=Disabled;Warn;Deny +type EncryptionValidationMode string + +const ( + // EncryptionValidationModeDisabled turns validation off. + EncryptionValidationModeDisabled EncryptionValidationMode = "Disabled" + // EncryptionValidationModeWarn allows unencrypted payloads and emits + // warning metrics. + EncryptionValidationModeWarn EncryptionValidationMode = "Warn" + // EncryptionValidationModeDeny rejects unencrypted payloads. + EncryptionValidationModeDeny EncryptionValidationMode = "Deny" +) + +// NamespaceStatus describes the current status of a Temporal Cloud namespace. // +k8s:openapi-gen=true type NamespaceStatus struct { tmprlcorev1.StatusBase `json:",inline"` @@ -40,6 +315,8 @@ type NamespaceStatus struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type NamespaceList struct { metav1.TypeMeta `json:",inline"` + // Standard list metadata. + // +optional metav1.ListMeta `json:"metadata,omitempty"` Items []Namespace `json:"items"` diff --git a/api/cloud/v1alpha1/zz_generated.deepcopy.go b/api/cloud/v1alpha1/zz_generated.deepcopy.go index 93f95fc..55a00b4 100644 --- a/api/cloud/v1alpha1/zz_generated.deepcopy.go +++ b/api/cloud/v1alpha1/zz_generated.deepcopy.go @@ -9,6 +9,212 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIKeyAuthSpec) DeepCopyInto(out *APIKeyAuthSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIKeyAuthSpec. +func (in *APIKeyAuthSpec) DeepCopy() *APIKeyAuthSpec { + if in == nil { + return nil + } + out := new(APIKeyAuthSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CapacitySpec) DeepCopyInto(out *CapacitySpec) { + *out = *in + if in.OnDemand != nil { + in, out := &in.OnDemand, &out.OnDemand + *out = new(OnDemandCapacity) + **out = **in + } + if in.Provisioned != nil { + in, out := &in.Provisioned, &out.Provisioned + *out = new(ProvisionedCapacity) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CapacitySpec. +func (in *CapacitySpec) DeepCopy() *CapacitySpec { + if in == nil { + return nil + } + out := new(CapacitySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CertificateFilterSpec) DeepCopyInto(out *CertificateFilterSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateFilterSpec. +func (in *CertificateFilterSpec) DeepCopy() *CertificateFilterSpec { + if in == nil { + return nil + } + out := new(CertificateFilterSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CodecServerCustomErrorMessage) DeepCopyInto(out *CodecServerCustomErrorMessage) { + *out = *in + if in.Default != nil { + in, out := &in.Default, &out.Default + *out = new(CodecServerErrorMessage) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CodecServerCustomErrorMessage. +func (in *CodecServerCustomErrorMessage) DeepCopy() *CodecServerCustomErrorMessage { + if in == nil { + return nil + } + out := new(CodecServerCustomErrorMessage) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CodecServerErrorMessage) DeepCopyInto(out *CodecServerErrorMessage) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CodecServerErrorMessage. +func (in *CodecServerErrorMessage) DeepCopy() *CodecServerErrorMessage { + if in == nil { + return nil + } + out := new(CodecServerErrorMessage) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CodecServerSpec) DeepCopyInto(out *CodecServerSpec) { + *out = *in + if in.CustomErrorMessage != nil { + in, out := &in.CustomErrorMessage, &out.CustomErrorMessage + *out = new(CodecServerCustomErrorMessage) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CodecServerSpec. +func (in *CodecServerSpec) DeepCopy() *CodecServerSpec { + if in == nil { + return nil + } + out := new(CodecServerSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EncryptionValidationSpec) DeepCopyInto(out *EncryptionValidationSpec) { + *out = *in + if in.MetadataValues != nil { + in, out := &in.MetadataValues, &out.MetadataValues + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EncryptionValidationSpec. +func (in *EncryptionValidationSpec) DeepCopy() *EncryptionValidationSpec { + if in == nil { + return nil + } + out := new(EncryptionValidationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FairnessSpec) DeepCopyInto(out *FairnessSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FairnessSpec. +func (in *FairnessSpec) DeepCopy() *FairnessSpec { + if in == nil { + return nil + } + out := new(FairnessSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HighAvailabilitySpec) DeepCopyInto(out *HighAvailabilitySpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HighAvailabilitySpec. +func (in *HighAvailabilitySpec) DeepCopy() *HighAvailabilitySpec { + if in == nil { + return nil + } + out := new(HighAvailabilitySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LifecycleSpec) DeepCopyInto(out *LifecycleSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LifecycleSpec. +func (in *LifecycleSpec) DeepCopy() *LifecycleSpec { + if in == nil { + return nil + } + out := new(LifecycleSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MTLSAuthSpec) DeepCopyInto(out *MTLSAuthSpec) { + *out = *in + if in.CertificateFilters != nil { + in, out := &in.CertificateFilters, &out.CertificateFilters + *out = make([]CertificateFilterSpec, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MTLSAuthSpec. +func (in *MTLSAuthSpec) DeepCopy() *MTLSAuthSpec { + if in == nil { + return nil + } + out := new(MTLSAuthSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Namespace) DeepCopyInto(out *Namespace) { *out = *in @@ -75,9 +281,68 @@ func (in *NamespaceSpec) DeepCopyInto(out *NamespaceSpec) { *out = *in if in.Tags != nil { in, out := &in.Tags, &out.Tags + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = make([]ReplicaSpec, len(*in)) + copy(*out, *in) + } + if in.MTLSAuth != nil { + in, out := &in.MTLSAuth, &out.MTLSAuth + *out = new(MTLSAuthSpec) + (*in).DeepCopyInto(*out) + } + if in.APIKeyAuth != nil { + in, out := &in.APIKeyAuth, &out.APIKeyAuth + *out = new(APIKeyAuthSpec) + **out = **in + } + if in.SearchAttributes != nil { + in, out := &in.SearchAttributes, &out.SearchAttributes + *out = make(map[string]SearchAttributeType, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.CodecServer != nil { + in, out := &in.CodecServer, &out.CodecServer + *out = new(CodecServerSpec) + (*in).DeepCopyInto(*out) + } + if in.Lifecycle != nil { + in, out := &in.Lifecycle, &out.Lifecycle + *out = new(LifecycleSpec) + **out = **in + } + if in.HighAvailability != nil { + in, out := &in.HighAvailability, &out.HighAvailability + *out = new(HighAvailabilitySpec) + **out = **in + } + if in.ConnectivityRuleIDs != nil { + in, out := &in.ConnectivityRuleIDs, &out.ConnectivityRuleIDs *out = make([]string, len(*in)) copy(*out, *in) } + if in.Capacity != nil { + in, out := &in.Capacity, &out.Capacity + *out = new(CapacitySpec) + (*in).DeepCopyInto(*out) + } + if in.Fairness != nil { + in, out := &in.Fairness, &out.Fairness + *out = new(FairnessSpec) + **out = **in + } + if in.EncryptionValidation != nil { + in, out := &in.EncryptionValidation, &out.EncryptionValidation + *out = new(EncryptionValidationSpec) + (*in).DeepCopyInto(*out) + } return } @@ -107,3 +372,51 @@ func (in *NamespaceStatus) DeepCopy() *NamespaceStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OnDemandCapacity) DeepCopyInto(out *OnDemandCapacity) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnDemandCapacity. +func (in *OnDemandCapacity) DeepCopy() *OnDemandCapacity { + if in == nil { + return nil + } + out := new(OnDemandCapacity) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProvisionedCapacity) DeepCopyInto(out *ProvisionedCapacity) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProvisionedCapacity. +func (in *ProvisionedCapacity) DeepCopy() *ProvisionedCapacity { + if in == nil { + return nil + } + out := new(ProvisionedCapacity) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReplicaSpec) DeepCopyInto(out *ReplicaSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReplicaSpec. +func (in *ReplicaSpec) DeepCopy() *ReplicaSpec { + if in == nil { + return nil + } + out := new(ReplicaSpec) + in.DeepCopyInto(out) + return out +} diff --git a/deploy/kustomize/crd/bases/cloud.temporal.io_namespaces.yaml b/deploy/kustomize/crd/bases/cloud.temporal.io_namespaces.yaml index a565bfa..b523a61 100644 --- a/deploy/kustomize/crd/bases/cloud.temporal.io_namespaces.yaml +++ b/deploy/kustomize/crd/bases/cloud.temporal.io_namespaces.yaml @@ -17,49 +17,383 @@ spec: - name: v1alpha1 schema: openAPIV3Schema: + description: Namespace represents an instance of a Temporal Cloud Namespace. 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: Spec defines the desired state of the Namespace. properties: + apiKeyAuth: + description: |- + APIKeyAuth configures API key client authentication. + Maps to Temporal Cloud proto `NamespaceSpec.api_key_auth`. + properties: + enabled: + description: |- + Enabled turns API key authentication on or off. + Maps to Temporal Cloud proto `ApiKeyAuthSpec.enabled`. + type: boolean + required: + - enabled + type: object + capacity: + description: |- + Capacity selects the capacity mode. + Maps to Temporal Cloud proto `NamespaceSpec.capacity_spec`. + properties: + onDemand: + description: |- + OnDemand selects the on-demand capacity mode. + Maps to Temporal Cloud proto `CapacitySpec.on_demand`. + type: object + provisioned: + description: |- + Provisioned selects the provisioned capacity mode. + Maps to Temporal Cloud proto `CapacitySpec.provisioned`. + properties: + value: + description: |- + Value is the capacity in Temporal Resource Units (TRUs). + Maps to Temporal Cloud proto `CapacitySpec.Provisioned.value`. + format: int32 + type: integer + required: + - value + type: object + type: object + x-kubernetes-validations: + - message: exactly one of onDemand or provisioned must be set + rule: has(self.onDemand) != has(self.provisioned) + codecServer: + description: |- + CodecServer configures the codec server used by the Temporal Cloud UI. + Maps to Temporal Cloud proto `NamespaceSpec.codec_server`. + properties: + customErrorMessage: + description: |- + CustomErrorMessage is shown in the UI when the codec server returns an + error. + Maps to Temporal Cloud proto `CodecServerSpec.custom_error_message`. + properties: + default: + description: |- + Default is shown for any codec server error. + Maps to Temporal Cloud proto `CodecServerSpec.CustomErrorMessage.default`. + properties: + link: + description: |- + Link is a URL shown next to the message. + Maps to Temporal Cloud proto `CodecServerSpec.CustomErrorMessage.ErrorMessage.link`. + type: string + message: + description: |- + Message is the text to show. + Maps to Temporal Cloud proto `CodecServerSpec.CustomErrorMessage.ErrorMessage.message`. + type: string + type: object + type: object + endpoint: + description: |- + Endpoint is the URL of the codec server. + Maps to Temporal Cloud proto `CodecServerSpec.endpoint`. + type: string + includeCrossOriginCredentials: + description: |- + IncludeCrossOriginCredentials includes cross-origin credentials in + requests to the endpoint. + Maps to Temporal Cloud proto `CodecServerSpec.include_cross_origin_credentials`. + type: boolean + passAccessToken: + description: |- + PassAccessToken passes the user's access token to the endpoint. + Maps to Temporal Cloud proto `CodecServerSpec.pass_access_token`. + type: boolean + required: + - endpoint + type: object + connectivityRuleIDs: + description: |- + ConnectivityRuleIDs are the IDs of the connectivity rules to apply. + Maps to Temporal Cloud proto `NamespaceSpec.connectivity_rule_ids`. + items: + type: string + type: array + x-kubernetes-list-type: set + description: + description: |- + Description is a human-readable description of the Namespace. + Maps to Temporal Cloud proto `NamespaceSpec.description`. + type: string + encryptionValidation: + description: |- + EncryptionValidation configures payload encryption validation. + Maps to Temporal Cloud proto `NamespaceSpec.encryption_validation`. + properties: + inspectFailure: + description: |- + InspectFailure includes failure messages and stack traces in + validation. + Maps to Temporal Cloud proto `EncryptionValidationSpec.inspect_failure`. + type: boolean + inspectHeader: + description: |- + InspectHeader includes workflow headers in validation. + Maps to Temporal Cloud proto `EncryptionValidationSpec.inspect_header`. + type: boolean + metadataKey: + description: |- + MetadataKey is the payload metadata key that identifies encrypted + payloads. + Maps to Temporal Cloud proto `EncryptionValidationSpec.metadata_key`. + type: string + metadataValues: + description: |- + MetadataValues are the payload metadata values that identify encrypted + payloads. + Maps to Temporal Cloud proto `EncryptionValidationSpec.metadata_values`. + items: + type: string + type: array + mode: + description: |- + Mode is the payload encryption validation mode. + Maps to Temporal Cloud proto `EncryptionValidationSpec.mode`. + enum: + - Disabled + - Warn + - Deny + type: string + required: + - mode + type: object + fairness: + description: |- + Fairness configures task queue fairness. + Maps to Temporal Cloud proto `NamespaceSpec.fairness`. + properties: + taskQueueFairnessEnabled: + description: |- + TaskQueueFairnessEnabled turns on task queue fairness. + Maps to Temporal Cloud proto `FairnessSpec.task_queue_fairness_enabled`. + type: boolean + type: object + highAvailability: + description: |- + HighAvailability configures failover behavior. + Maps to Temporal Cloud proto `NamespaceSpec.high_availability`. + properties: + disableManagedFailover: + description: |- + DisableManagedFailover turns off failover managed by Temporal Cloud. + Maps to Temporal Cloud proto `HighAvailabilitySpec.disable_managed_failover`. + type: boolean + disablePassivePollerForwarding: + description: |- + DisablePassivePollerForwarding turns off forwarding of polls from + passive replicas to the active replica. + Maps to Temporal Cloud proto `HighAvailabilitySpec.disable_passive_poller_forwarding`. + type: boolean + type: object + lifecycle: + description: |- + Lifecycle configures lifecycle settings. + Maps to Temporal Cloud proto `NamespaceSpec.lifecycle`. + properties: + enableDeleteProtection: + description: |- + EnableDeleteProtection prevents the Namespace from being deleted. + Maps to Temporal Cloud proto `LifecycleSpec.enable_delete_protection`. + type: boolean + type: object + mtlsAuth: + description: |- + MTLSAuth configures mTLS client authentication. + Maps to Temporal Cloud proto `NamespaceSpec.mtls_auth`. + properties: + acceptedClientCA: + description: |- + AcceptedClientCA is the PEM-encoded CA certificate, or chain, that + client certificates must be issued by. + Maps to Temporal Cloud proto `MtlsAuthSpec.accepted_client_ca`. + type: string + certificateFilters: + description: |- + CertificateFilters restrict client certificates by subject. + Maps to Temporal Cloud proto `MtlsAuthSpec.certificate_filters`. + items: + description: CertificateFilterSpec matches client certificates + by subject fields. + properties: + commonName: + description: |- + CommonName is the certificate's common name. + Maps to Temporal Cloud proto `CertificateFilterSpec.common_name`. + type: string + organization: + description: |- + Organization is the certificate's organization. + Maps to Temporal Cloud proto `CertificateFilterSpec.organization`. + type: string + organizationalUnit: + description: |- + OrganizationalUnit is the certificate's organizational unit. + Maps to Temporal Cloud proto `CertificateFilterSpec.organizational_unit`. + type: string + subjectAlternativeName: + description: |- + SubjectAlternativeName is the certificate's subject alternative name. + Maps to Temporal Cloud proto `CertificateFilterSpec.subject_alternative_name`. + type: string + type: object + type: array + enabled: + description: |- + Enabled turns mTLS authentication on or off. + Maps to Temporal Cloud proto `MtlsAuthSpec.enabled`. + type: boolean + required: + - enabled + type: object projectID: + description: |- + ProjectID is the ID of the Temporal Cloud project that the Namespace + belongs to. + Maps to Temporal Cloud proto `Namespace.project_id`. + minLength: 1 type: string - tags: + replicas: + description: |- + Replicas are the regions where the Namespace is placed. + Maps to Temporal Cloud proto `NamespaceSpec.replicas`. items: - type: string + description: ReplicaSpec places a replica of the Namespace in a + region. + properties: + region: + description: |- + Region is the ID of the region to place the replica in. + Maps to Temporal Cloud proto `ReplicaSpec.region`. + type: string + required: + - region + type: object + minItems: 1 type: array + x-kubernetes-list-map-keys: + - region + x-kubernetes-list-type: map + retentionDays: + description: |- + RetentionDays is the number of days that closed workflow histories are + kept. + Maps to Temporal Cloud proto `NamespaceSpec.retention_days`. + format: int32 + type: integer + searchAttributes: + additionalProperties: + description: SearchAttributeType is the type of a custom search + attribute. + enum: + - Text + - Keyword + - Int + - Double + - Bool + - Datetime + - KeywordList + type: string + description: |- + SearchAttributes are the custom search attributes of the Namespace, + keyed by name. + Maps to Temporal Cloud proto `NamespaceSpec.search_attributes`. + type: object + tags: + additionalProperties: + type: string + description: |- + 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: .projectID + message: projectID cannot be added, changed or removed after creation + rule: has(self.projectID) == has(oldSelf.projectID) && (!has(self.projectID) + || self.projectID == oldSelf.projectID) + - fieldPath: .searchAttributes + message: searchAttributes cannot be removed or have their type changed + rule: '!has(oldSelf.searchAttributes) || (has(self.searchAttributes) + && oldSelf.searchAttributes.all(k, k in self.searchAttributes && self.searchAttributes[k] + == oldSelf.searchAttributes[k]))' status: + description: Status describes the current status of a Namespace. properties: conditions: + description: Conditions is the collection of Conditions associated + with the resource. 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 @@ -75,6 +409,10 @@ spec: - type x-kubernetes-list-type: map type: object + required: + - spec type: object served: true storage: true + subresources: + status: {} diff --git a/examples/namespace.yaml b/examples/namespace.yaml new file mode 100644 index 0000000..03cd09d --- /dev/null +++ b/examples/namespace.yaml @@ -0,0 +1,27 @@ +# An example Temporal Cloud Namespace. To create it, run: +# +# kubectl apply -n -f examples/namespace.yaml +# +# metadata.name is used as the Temporal Cloud Namespace name. To list all spec +# fields, run: +# +# kubectl explain namespaces.spec --api-version=cloud.temporal.io/v1alpha1 +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: payments-prod +spec: + # The project to create the Namespace in. If not set, the account's default + # project is used. Cannot be changed after creation. + # projectID: + tags: + environment: production + team: payments + replicas: + - region: aws-us-west-2 + retentionDays: 30 + apiKeyAuth: + enabled: true + capacity: + onDemand: {} + description: Payment workflows for production. diff --git a/test/e2e/namespace/happy-default.yaml b/test/e2e/namespace/happy-default.yaml index d66b86c..320f1ff 100644 --- a/test/e2e/namespace/happy-default.yaml +++ b/test/e2e/namespace/happy-default.yaml @@ -19,11 +19,23 @@ tests: kind: Namespace metadata: name: happy-default + spec: + replicas: + - region: aws-us-west-2 + retentionDays: 30 + apiKeyAuth: + enabled: true - name: check-namespace-spec kube.get: namespaces.cloud.temporal.io/happy-default require: - len: 1 + matches: + spec: + replicas: + - region: aws-us-west-2 + retentionDays: 30 + apiKeyAuth: + enabled: true - name: delete-namespace kube.delete: namespaces.cloud.temporal.io/happy-default diff --git a/test/e2e/namespace/testdata/minimal.yaml b/test/e2e/namespace/testdata/minimal.yaml new file mode 100644 index 0000000..9513ff0 --- /dev/null +++ b/test/e2e/namespace/testdata/minimal.yaml @@ -0,0 +1,12 @@ +# The smallest valid Namespace manifest. Validation tests patch it to make it +# invalid in exactly one way. +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: minimal +spec: + replicas: + - region: aws-us-west-2 + retentionDays: 30 + apiKeyAuth: + enabled: true diff --git a/test/e2e/namespace/validation.yaml b/test/e2e/namespace/validation.yaml new file mode 100644 index 0000000..e9dcdd8 --- /dev/null +++ b/test/e2e/namespace/validation.yaml @@ -0,0 +1,269 @@ +name: namespace-validation +description: | + Checks that the Namespace CRD accepts valid Namespace manifests and rejects + invalid ones. + + Each create case patches testdata/minimal.yaml locally to make it invalid in + exactly one way, then applies it with --dry-run=server. A server dry run + runs the same schema and CEL validation as a real apply but never stores the + object. Update cases patch stored objects with --dry-run=server. + +depends: + - name: kubectl + +defaults: + kube: + namespace: $TEST_USER_NAMESPACE + +tests: + - name: ensure-empty-test-namespace + kube.get: namespaces.cloud.temporal.io + require: + len: 0 + + # Valid manifests + - name: accept-minimal-manifest + exec: >- + kubectl apply --dry-run=server -n $TEST_USER_NAMESPACE + -f testdata/minimal.yaml + require: + out: + contains: namespace.cloud.temporal.io/minimal created (server dry run) + + - name: apply-example + exec: kubectl apply -n $TEST_USER_NAMESPACE -f ../../../examples/namespace.yaml + require: + out: + contains: namespace.cloud.temporal.io/payments-prod created + err: + contains-none-of: Warning + + - name: check-example-spec + kube.get: namespaces.cloud.temporal.io/payments-prod + assert: + matches: + spec: + tags: + environment: production + team: payments + replicas: + - region: aws-us-west-2 + retentionDays: 30 + apiKeyAuth: + enabled: true + capacity: + onDemand: {} + + # Required fields + - name: reject-missing-spec + shell: sh + exec: >- + kubectl patch --local --type=merge -o yaml + -f testdata/minimal.yaml + -p '{"spec":null}' | + kubectl apply --dry-run=server -n $TEST_USER_NAMESPACE -f - + assert: + exit-code: 1 + err: + contains: "spec: Required value" + + - name: reject-missing-replicas + shell: sh + exec: >- + kubectl patch --local --type=merge -o yaml + -f testdata/minimal.yaml + -p '{"spec":{"replicas":null}}' | + kubectl apply --dry-run=server -n $TEST_USER_NAMESPACE -f - + assert: + exit-code: 1 + err: + contains: "spec.replicas: Required value" + + - name: reject-empty-replicas + shell: sh + exec: >- + kubectl patch --local --type=merge -o yaml + -f testdata/minimal.yaml + -p '{"spec":{"replicas":[]}}' | + kubectl apply --dry-run=server -n $TEST_USER_NAMESPACE -f - + assert: + exit-code: 1 + err: + contains: spec.replicas in body should have at least 1 items + + - name: reject-duplicate-replica-regions + shell: sh + exec: >- + kubectl patch --local --type=merge -o yaml + -f testdata/minimal.yaml + -p '{"spec":{"replicas":[{"region":"aws-us-west-2"},{"region":"aws-us-west-2"}]}}' | + kubectl apply --dry-run=server -n $TEST_USER_NAMESPACE -f - + assert: + exit-code: 1 + err: + contains: "spec.replicas[1]: Duplicate value" + + - name: reject-missing-retention-days + shell: sh + exec: >- + kubectl patch --local --type=merge -o yaml + -f testdata/minimal.yaml + -p '{"spec":{"retentionDays":null}}' | + kubectl apply --dry-run=server -n $TEST_USER_NAMESPACE -f - + assert: + exit-code: 1 + err: + contains: "spec.retentionDays: Required value" + + - name: reject-auth-without-enabled + shell: sh + exec: >- + kubectl patch --local --type=merge -o yaml + -f testdata/minimal.yaml + -p '{"spec":{"apiKeyAuth":{"enabled":null}}}' | + kubectl apply --dry-run=server -n $TEST_USER_NAMESPACE -f - + assert: + exit-code: 1 + err: + contains: "spec.apiKeyAuth.enabled: Required value" + + # Enums, sets and one-of rules + - name: reject-unknown-search-attribute-type + shell: sh + exec: >- + kubectl patch --local --type=merge -o yaml + -f testdata/minimal.yaml + -p '{"spec":{"searchAttributes":{"CustomerId":"keyword"}}}' | + kubectl apply --dry-run=server -n $TEST_USER_NAMESPACE -f - + assert: + exit-code: 1 + err: + contains: 'spec.searchAttributes.CustomerId: Unsupported value: "keyword"' + + - name: reject-duplicate-connectivity-rule-ids + shell: sh + exec: >- + kubectl patch --local --type=merge -o yaml + -f testdata/minimal.yaml + -p '{"spec":{"connectivityRuleIDs":["rule-1","rule-1"]}}' | + kubectl apply --dry-run=server -n $TEST_USER_NAMESPACE -f - + assert: + exit-code: 1 + err: + contains: "spec.connectivityRuleIDs[1]: Duplicate value" + + - name: reject-both-capacity-modes + shell: sh + exec: >- + kubectl patch --local --type=merge -o yaml + -f testdata/minimal.yaml + -p '{"spec":{"capacity":{"onDemand":{},"provisioned":{"value":2}}}}' | + kubectl apply --dry-run=server -n $TEST_USER_NAMESPACE -f - + assert: + exit-code: 1 + err: + contains: exactly one of onDemand or provisioned must be set + + - name: reject-no-capacity-mode + shell: sh + exec: >- + kubectl patch --local --type=merge -o yaml + -f testdata/minimal.yaml + -p '{"spec":{"capacity":{}}}' | + kubectl apply --dry-run=server -n $TEST_USER_NAMESPACE -f - + assert: + exit-code: 1 + err: + contains: exactly one of onDemand or provisioned must be set + + - name: reject-unknown-encryption-validation-mode + shell: sh + exec: >- + kubectl patch --local --type=merge -o yaml + -f testdata/minimal.yaml + -p '{"spec":{"encryptionValidation":{"mode":"Strict"}}}' | + kubectl apply --dry-run=server -n $TEST_USER_NAMESPACE -f - + assert: + exit-code: 1 + err: + contains: 'spec.encryptionValidation.mode: Unsupported value: "Strict"' + + # Updates: projectID is immutable, and search attributes cannot be removed or + # have their type changed. + - name: create-namespace-with-project-and-search-attribute + shell: sh + exec: >- + kubectl patch --local --type=merge -o yaml + -f testdata/minimal.yaml + -p '{"metadata":{"name":"immutable-fields"},"spec":{"projectID":"project-1","searchAttributes":{"CustomerId":"Keyword"}}}' | + kubectl apply -n $TEST_USER_NAMESPACE -f - + require: + out: + contains: namespace.cloud.temporal.io/immutable-fields created + + - name: accept-update-of-mutable-fields + exec: >- + kubectl patch namespaces.cloud.temporal.io/immutable-fields + --dry-run=server -n $TEST_USER_NAMESPACE --type=merge + -p '{"spec":{"retentionDays":7,"replicas":[{"region":"aws-us-west-2"},{"region":"aws-us-east-1"}],"searchAttributes":{"OrderId":"Int"}}}' + + - name: reject-adding-project + exec: >- + kubectl patch namespaces.cloud.temporal.io/payments-prod + --dry-run=server -n $TEST_USER_NAMESPACE --type=merge + -p '{"spec":{"projectID":"project-1"}}' + assert: + exit-code: 1 + err: + contains: projectID cannot be added, changed or removed after creation + + - name: reject-changing-project + exec: >- + kubectl patch namespaces.cloud.temporal.io/immutable-fields + --dry-run=server -n $TEST_USER_NAMESPACE --type=merge + -p '{"spec":{"projectID":"project-2"}}' + assert: + exit-code: 1 + err: + contains: projectID cannot be added, changed or removed after creation + + - name: reject-removing-project + exec: >- + kubectl patch namespaces.cloud.temporal.io/immutable-fields + --dry-run=server -n $TEST_USER_NAMESPACE --type=merge + -p '{"spec":{"projectID":null}}' + assert: + exit-code: 1 + err: + contains: projectID cannot be added, changed or removed after creation + + - name: reject-removing-search-attribute + exec: >- + kubectl patch namespaces.cloud.temporal.io/immutable-fields + --dry-run=server -n $TEST_USER_NAMESPACE --type=merge + -p '{"spec":{"searchAttributes":{"CustomerId":null}}}' + assert: + exit-code: 1 + err: + contains: searchAttributes cannot be removed or have their type changed + + - name: reject-changing-search-attribute-type + exec: >- + kubectl patch namespaces.cloud.temporal.io/immutable-fields + --dry-run=server -n $TEST_USER_NAMESPACE --type=merge + -p '{"spec":{"searchAttributes":{"CustomerId":"Text"}}}' + assert: + exit-code: 1 + err: + contains: searchAttributes cannot be removed or have their type changed + + - name: delete-example + kube.delete: namespaces.cloud.temporal.io/payments-prod + + - name: delete-namespace-with-project-and-search-attribute + kube.delete: namespaces.cloud.temporal.io/immutable-fields + + - name: ensure-namespaces-deleted + kube.get: namespaces.cloud.temporal.io + assert: + len: 0 From 01a70218c03cfdbef028a1705980ec6a14707cfa Mon Sep 17 00:00:00 2001 From: Tomba Leishangthem <10569680+tomba7@users.noreply.github.com> Date: Tue, 13 Oct 2026 22:08:01 -0700 Subject: [PATCH 2/4] test(e2e): replace patched manifests with static fixtures --- .../failures/auth-without-enabled.yaml | 9 + .../failures/both-capacity-modes.yaml | 14 ++ .../duplicate-connectivity-rule-ids.yaml | 13 ++ .../failures/duplicate-replica-regions.yaml | 11 + .../manifests/failures/empty-replicas.yaml | 9 + .../manifests/failures/missing-replicas.yaml | 8 + .../failures/missing-retention-days.yaml | 9 + .../manifests/failures/missing-spec.yaml | 4 + .../manifests/failures/no-capacity-mode.yaml | 11 + .../unknown-encryption-validation-mode.yaml | 12 ++ .../unknown-search-attribute-type.yaml | 12 ++ .../namespace/manifests/immutable-fields.yaml | 13 ++ .../{testdata => manifests}/minimal.yaml | 0 .../namespace/manifests/mutable-fields.yaml | 13 ++ .../manifests/updates/add-project.yaml | 11 + .../manifests/updates/change-project.yaml | 13 ++ .../updates/change-search-attribute-type.yaml | 13 ++ .../manifests/updates/mutable-fields.yaml | 15 ++ .../manifests/updates/remove-project.yaml | 12 ++ .../updates/remove-search-attribute.yaml | 11 + test/e2e/namespace/validation.yaml | 193 ++++++------------ 21 files changed, 279 insertions(+), 127 deletions(-) create mode 100644 test/e2e/namespace/manifests/failures/auth-without-enabled.yaml create mode 100644 test/e2e/namespace/manifests/failures/both-capacity-modes.yaml create mode 100644 test/e2e/namespace/manifests/failures/duplicate-connectivity-rule-ids.yaml create mode 100644 test/e2e/namespace/manifests/failures/duplicate-replica-regions.yaml create mode 100644 test/e2e/namespace/manifests/failures/empty-replicas.yaml create mode 100644 test/e2e/namespace/manifests/failures/missing-replicas.yaml create mode 100644 test/e2e/namespace/manifests/failures/missing-retention-days.yaml create mode 100644 test/e2e/namespace/manifests/failures/missing-spec.yaml create mode 100644 test/e2e/namespace/manifests/failures/no-capacity-mode.yaml create mode 100644 test/e2e/namespace/manifests/failures/unknown-encryption-validation-mode.yaml create mode 100644 test/e2e/namespace/manifests/failures/unknown-search-attribute-type.yaml create mode 100644 test/e2e/namespace/manifests/immutable-fields.yaml rename test/e2e/namespace/{testdata => manifests}/minimal.yaml (100%) create mode 100644 test/e2e/namespace/manifests/mutable-fields.yaml create mode 100644 test/e2e/namespace/manifests/updates/add-project.yaml create mode 100644 test/e2e/namespace/manifests/updates/change-project.yaml create mode 100644 test/e2e/namespace/manifests/updates/change-search-attribute-type.yaml create mode 100644 test/e2e/namespace/manifests/updates/mutable-fields.yaml create mode 100644 test/e2e/namespace/manifests/updates/remove-project.yaml create mode 100644 test/e2e/namespace/manifests/updates/remove-search-attribute.yaml diff --git a/test/e2e/namespace/manifests/failures/auth-without-enabled.yaml b/test/e2e/namespace/manifests/failures/auth-without-enabled.yaml new file mode 100644 index 0000000..44dc485 --- /dev/null +++ b/test/e2e/namespace/manifests/failures/auth-without-enabled.yaml @@ -0,0 +1,9 @@ +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: auth-without-enabled +spec: + replicas: + - region: aws-us-west-2 + retentionDays: 30 + apiKeyAuth: {} diff --git a/test/e2e/namespace/manifests/failures/both-capacity-modes.yaml b/test/e2e/namespace/manifests/failures/both-capacity-modes.yaml new file mode 100644 index 0000000..923a512 --- /dev/null +++ b/test/e2e/namespace/manifests/failures/both-capacity-modes.yaml @@ -0,0 +1,14 @@ +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: both-capacity-modes +spec: + replicas: + - region: aws-us-west-2 + retentionDays: 30 + apiKeyAuth: + enabled: true + capacity: + onDemand: {} + provisioned: + value: 2 diff --git a/test/e2e/namespace/manifests/failures/duplicate-connectivity-rule-ids.yaml b/test/e2e/namespace/manifests/failures/duplicate-connectivity-rule-ids.yaml new file mode 100644 index 0000000..36248fd --- /dev/null +++ b/test/e2e/namespace/manifests/failures/duplicate-connectivity-rule-ids.yaml @@ -0,0 +1,13 @@ +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: duplicate-connectivity-rule-ids +spec: + replicas: + - region: aws-us-west-2 + retentionDays: 30 + apiKeyAuth: + enabled: true + connectivityRuleIDs: + - rule-1 + - rule-1 diff --git a/test/e2e/namespace/manifests/failures/duplicate-replica-regions.yaml b/test/e2e/namespace/manifests/failures/duplicate-replica-regions.yaml new file mode 100644 index 0000000..6ad2f2c --- /dev/null +++ b/test/e2e/namespace/manifests/failures/duplicate-replica-regions.yaml @@ -0,0 +1,11 @@ +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: duplicate-replica-regions +spec: + replicas: + - region: aws-us-west-2 + - region: aws-us-west-2 + retentionDays: 30 + apiKeyAuth: + enabled: true diff --git a/test/e2e/namespace/manifests/failures/empty-replicas.yaml b/test/e2e/namespace/manifests/failures/empty-replicas.yaml new file mode 100644 index 0000000..6bf4eda --- /dev/null +++ b/test/e2e/namespace/manifests/failures/empty-replicas.yaml @@ -0,0 +1,9 @@ +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: empty-replicas +spec: + replicas: [] + retentionDays: 30 + apiKeyAuth: + enabled: true diff --git a/test/e2e/namespace/manifests/failures/missing-replicas.yaml b/test/e2e/namespace/manifests/failures/missing-replicas.yaml new file mode 100644 index 0000000..2cbf545 --- /dev/null +++ b/test/e2e/namespace/manifests/failures/missing-replicas.yaml @@ -0,0 +1,8 @@ +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: missing-replicas +spec: + retentionDays: 30 + apiKeyAuth: + enabled: true diff --git a/test/e2e/namespace/manifests/failures/missing-retention-days.yaml b/test/e2e/namespace/manifests/failures/missing-retention-days.yaml new file mode 100644 index 0000000..f2c6ed9 --- /dev/null +++ b/test/e2e/namespace/manifests/failures/missing-retention-days.yaml @@ -0,0 +1,9 @@ +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: missing-retention-days +spec: + replicas: + - region: aws-us-west-2 + apiKeyAuth: + enabled: true diff --git a/test/e2e/namespace/manifests/failures/missing-spec.yaml b/test/e2e/namespace/manifests/failures/missing-spec.yaml new file mode 100644 index 0000000..204d642 --- /dev/null +++ b/test/e2e/namespace/manifests/failures/missing-spec.yaml @@ -0,0 +1,4 @@ +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: missing-spec diff --git a/test/e2e/namespace/manifests/failures/no-capacity-mode.yaml b/test/e2e/namespace/manifests/failures/no-capacity-mode.yaml new file mode 100644 index 0000000..7db6129 --- /dev/null +++ b/test/e2e/namespace/manifests/failures/no-capacity-mode.yaml @@ -0,0 +1,11 @@ +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: no-capacity-mode +spec: + replicas: + - region: aws-us-west-2 + retentionDays: 30 + apiKeyAuth: + enabled: true + capacity: {} diff --git a/test/e2e/namespace/manifests/failures/unknown-encryption-validation-mode.yaml b/test/e2e/namespace/manifests/failures/unknown-encryption-validation-mode.yaml new file mode 100644 index 0000000..74e5abf --- /dev/null +++ b/test/e2e/namespace/manifests/failures/unknown-encryption-validation-mode.yaml @@ -0,0 +1,12 @@ +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: unknown-encryption-validation-mode +spec: + replicas: + - region: aws-us-west-2 + retentionDays: 30 + apiKeyAuth: + enabled: true + encryptionValidation: + mode: Strict diff --git a/test/e2e/namespace/manifests/failures/unknown-search-attribute-type.yaml b/test/e2e/namespace/manifests/failures/unknown-search-attribute-type.yaml new file mode 100644 index 0000000..ed41cc9 --- /dev/null +++ b/test/e2e/namespace/manifests/failures/unknown-search-attribute-type.yaml @@ -0,0 +1,12 @@ +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: unknown-search-attribute-type +spec: + replicas: + - region: aws-us-west-2 + retentionDays: 30 + apiKeyAuth: + enabled: true + searchAttributes: + CustomerId: keyword diff --git a/test/e2e/namespace/manifests/immutable-fields.yaml b/test/e2e/namespace/manifests/immutable-fields.yaml new file mode 100644 index 0000000..9050505 --- /dev/null +++ b/test/e2e/namespace/manifests/immutable-fields.yaml @@ -0,0 +1,13 @@ +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: immutable-fields +spec: + projectID: project-1 + replicas: + - region: aws-us-west-2 + retentionDays: 30 + apiKeyAuth: + enabled: true + searchAttributes: + CustomerId: Keyword diff --git a/test/e2e/namespace/testdata/minimal.yaml b/test/e2e/namespace/manifests/minimal.yaml similarity index 100% rename from test/e2e/namespace/testdata/minimal.yaml rename to test/e2e/namespace/manifests/minimal.yaml diff --git a/test/e2e/namespace/manifests/mutable-fields.yaml b/test/e2e/namespace/manifests/mutable-fields.yaml new file mode 100644 index 0000000..d291881 --- /dev/null +++ b/test/e2e/namespace/manifests/mutable-fields.yaml @@ -0,0 +1,13 @@ +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: mutable-fields +spec: + projectID: project-1 + replicas: + - region: aws-us-west-2 + retentionDays: 30 + apiKeyAuth: + enabled: true + searchAttributes: + CustomerId: Keyword diff --git a/test/e2e/namespace/manifests/updates/add-project.yaml b/test/e2e/namespace/manifests/updates/add-project.yaml new file mode 100644 index 0000000..0cf1104 --- /dev/null +++ b/test/e2e/namespace/manifests/updates/add-project.yaml @@ -0,0 +1,11 @@ +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: minimal +spec: + projectID: project-1 + replicas: + - region: aws-us-west-2 + retentionDays: 30 + apiKeyAuth: + enabled: true diff --git a/test/e2e/namespace/manifests/updates/change-project.yaml b/test/e2e/namespace/manifests/updates/change-project.yaml new file mode 100644 index 0000000..85d7b87 --- /dev/null +++ b/test/e2e/namespace/manifests/updates/change-project.yaml @@ -0,0 +1,13 @@ +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: immutable-fields +spec: + projectID: project-2 + replicas: + - region: aws-us-west-2 + retentionDays: 30 + apiKeyAuth: + enabled: true + searchAttributes: + CustomerId: Keyword diff --git a/test/e2e/namespace/manifests/updates/change-search-attribute-type.yaml b/test/e2e/namespace/manifests/updates/change-search-attribute-type.yaml new file mode 100644 index 0000000..6977a28 --- /dev/null +++ b/test/e2e/namespace/manifests/updates/change-search-attribute-type.yaml @@ -0,0 +1,13 @@ +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: immutable-fields +spec: + projectID: project-1 + replicas: + - region: aws-us-west-2 + retentionDays: 30 + apiKeyAuth: + enabled: true + searchAttributes: + CustomerId: Text diff --git a/test/e2e/namespace/manifests/updates/mutable-fields.yaml b/test/e2e/namespace/manifests/updates/mutable-fields.yaml new file mode 100644 index 0000000..86241b4 --- /dev/null +++ b/test/e2e/namespace/manifests/updates/mutable-fields.yaml @@ -0,0 +1,15 @@ +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: mutable-fields +spec: + projectID: project-1 + replicas: + - region: aws-us-west-2 + - region: aws-us-east-1 + retentionDays: 7 + apiKeyAuth: + enabled: true + searchAttributes: + CustomerId: Keyword + OrderId: Int diff --git a/test/e2e/namespace/manifests/updates/remove-project.yaml b/test/e2e/namespace/manifests/updates/remove-project.yaml new file mode 100644 index 0000000..7fb0061 --- /dev/null +++ b/test/e2e/namespace/manifests/updates/remove-project.yaml @@ -0,0 +1,12 @@ +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: immutable-fields +spec: + replicas: + - region: aws-us-west-2 + retentionDays: 30 + apiKeyAuth: + enabled: true + searchAttributes: + CustomerId: Keyword diff --git a/test/e2e/namespace/manifests/updates/remove-search-attribute.yaml b/test/e2e/namespace/manifests/updates/remove-search-attribute.yaml new file mode 100644 index 0000000..a80341c --- /dev/null +++ b/test/e2e/namespace/manifests/updates/remove-search-attribute.yaml @@ -0,0 +1,11 @@ +apiVersion: cloud.temporal.io/v1alpha1 +kind: Namespace +metadata: + name: immutable-fields +spec: + projectID: project-1 + replicas: + - region: aws-us-west-2 + retentionDays: 30 + apiKeyAuth: + enabled: true diff --git a/test/e2e/namespace/validation.yaml b/test/e2e/namespace/validation.yaml index e9dcdd8..6e7bc95 100644 --- a/test/e2e/namespace/validation.yaml +++ b/test/e2e/namespace/validation.yaml @@ -3,10 +3,9 @@ description: | Checks that the Namespace CRD accepts valid Namespace manifests and rejects invalid ones. - Each create case patches testdata/minimal.yaml locally to make it invalid in - exactly one way, then applies it with --dry-run=server. A server dry run - runs the same schema and CEL validation as a real apply but never stores the - object. Update cases patch stored objects with --dry-run=server. + Create cases apply one manifest per case from manifests/failures/, each + invalid in exactly one way. Update cases replace a stored Namespace with a + manifest from manifests/updates/. See the comment above the update cases. depends: - name: kubectl @@ -23,20 +22,13 @@ tests: # Valid manifests - name: accept-minimal-manifest - exec: >- - kubectl apply --dry-run=server -n $TEST_USER_NAMESPACE - -f testdata/minimal.yaml - require: - out: - contains: namespace.cloud.temporal.io/minimal created (server dry run) + kube.apply: manifests/minimal.yaml + + - name: delete-minimal-manifest + kube.delete: namespaces.cloud.temporal.io/minimal - name: apply-example - exec: kubectl apply -n $TEST_USER_NAMESPACE -f ../../../examples/namespace.yaml - require: - out: - contains: namespace.cloud.temporal.io/payments-prod created - err: - contains-none-of: Warning + kube.apply: ../../../examples/namespace.yaml - name: check-example-spec kube.get: namespaces.cloud.temporal.io/payments-prod @@ -56,72 +48,42 @@ tests: # Required fields - name: reject-missing-spec - shell: sh - exec: >- - kubectl patch --local --type=merge -o yaml - -f testdata/minimal.yaml - -p '{"spec":null}' | - kubectl apply --dry-run=server -n $TEST_USER_NAMESPACE -f - + exec: kubectl apply -n $TEST_USER_NAMESPACE -f manifests/failures/missing-spec.yaml assert: exit-code: 1 err: contains: "spec: Required value" - name: reject-missing-replicas - shell: sh - exec: >- - kubectl patch --local --type=merge -o yaml - -f testdata/minimal.yaml - -p '{"spec":{"replicas":null}}' | - kubectl apply --dry-run=server -n $TEST_USER_NAMESPACE -f - + exec: kubectl apply -n $TEST_USER_NAMESPACE -f manifests/failures/missing-replicas.yaml assert: exit-code: 1 err: contains: "spec.replicas: Required value" - name: reject-empty-replicas - shell: sh - exec: >- - kubectl patch --local --type=merge -o yaml - -f testdata/minimal.yaml - -p '{"spec":{"replicas":[]}}' | - kubectl apply --dry-run=server -n $TEST_USER_NAMESPACE -f - + exec: kubectl apply -n $TEST_USER_NAMESPACE -f manifests/failures/empty-replicas.yaml assert: exit-code: 1 err: contains: spec.replicas in body should have at least 1 items - name: reject-duplicate-replica-regions - shell: sh - exec: >- - kubectl patch --local --type=merge -o yaml - -f testdata/minimal.yaml - -p '{"spec":{"replicas":[{"region":"aws-us-west-2"},{"region":"aws-us-west-2"}]}}' | - kubectl apply --dry-run=server -n $TEST_USER_NAMESPACE -f - + exec: kubectl apply -n $TEST_USER_NAMESPACE -f manifests/failures/duplicate-replica-regions.yaml assert: exit-code: 1 err: contains: "spec.replicas[1]: Duplicate value" - name: reject-missing-retention-days - shell: sh - exec: >- - kubectl patch --local --type=merge -o yaml - -f testdata/minimal.yaml - -p '{"spec":{"retentionDays":null}}' | - kubectl apply --dry-run=server -n $TEST_USER_NAMESPACE -f - + exec: kubectl apply -n $TEST_USER_NAMESPACE -f manifests/failures/missing-retention-days.yaml assert: exit-code: 1 err: contains: "spec.retentionDays: Required value" - name: reject-auth-without-enabled - shell: sh - exec: >- - kubectl patch --local --type=merge -o yaml - -f testdata/minimal.yaml - -p '{"spec":{"apiKeyAuth":{"enabled":null}}}' | - kubectl apply --dry-run=server -n $TEST_USER_NAMESPACE -f - + exec: kubectl apply -n $TEST_USER_NAMESPACE -f manifests/failures/auth-without-enabled.yaml assert: exit-code: 1 err: @@ -129,140 +91,117 @@ tests: # Enums, sets and one-of rules - name: reject-unknown-search-attribute-type - shell: sh - exec: >- - kubectl patch --local --type=merge -o yaml - -f testdata/minimal.yaml - -p '{"spec":{"searchAttributes":{"CustomerId":"keyword"}}}' | - kubectl apply --dry-run=server -n $TEST_USER_NAMESPACE -f - + exec: kubectl apply -n $TEST_USER_NAMESPACE -f manifests/failures/unknown-search-attribute-type.yaml assert: exit-code: 1 err: contains: 'spec.searchAttributes.CustomerId: Unsupported value: "keyword"' - name: reject-duplicate-connectivity-rule-ids - shell: sh - exec: >- - kubectl patch --local --type=merge -o yaml - -f testdata/minimal.yaml - -p '{"spec":{"connectivityRuleIDs":["rule-1","rule-1"]}}' | - kubectl apply --dry-run=server -n $TEST_USER_NAMESPACE -f - + exec: kubectl apply -n $TEST_USER_NAMESPACE -f manifests/failures/duplicate-connectivity-rule-ids.yaml assert: exit-code: 1 err: contains: "spec.connectivityRuleIDs[1]: Duplicate value" - name: reject-both-capacity-modes - shell: sh - exec: >- - kubectl patch --local --type=merge -o yaml - -f testdata/minimal.yaml - -p '{"spec":{"capacity":{"onDemand":{},"provisioned":{"value":2}}}}' | - kubectl apply --dry-run=server -n $TEST_USER_NAMESPACE -f - + exec: kubectl apply -n $TEST_USER_NAMESPACE -f manifests/failures/both-capacity-modes.yaml assert: exit-code: 1 err: contains: exactly one of onDemand or provisioned must be set - name: reject-no-capacity-mode - shell: sh - exec: >- - kubectl patch --local --type=merge -o yaml - -f testdata/minimal.yaml - -p '{"spec":{"capacity":{}}}' | - kubectl apply --dry-run=server -n $TEST_USER_NAMESPACE -f - + exec: kubectl apply -n $TEST_USER_NAMESPACE -f manifests/failures/no-capacity-mode.yaml assert: exit-code: 1 err: contains: exactly one of onDemand or provisioned must be set - name: reject-unknown-encryption-validation-mode - shell: sh - exec: >- - kubectl patch --local --type=merge -o yaml - -f testdata/minimal.yaml - -p '{"spec":{"encryptionValidation":{"mode":"Strict"}}}' | - kubectl apply --dry-run=server -n $TEST_USER_NAMESPACE -f - + exec: kubectl apply -n $TEST_USER_NAMESPACE -f manifests/failures/unknown-encryption-validation-mode.yaml assert: exit-code: 1 err: contains: 'spec.encryptionValidation.mode: Unsupported value: "Strict"' - # Updates: projectID is immutable, and search attributes cannot be removed or - # have their type changed. - - name: create-namespace-with-project-and-search-attribute - shell: sh - exec: >- - kubectl patch --local --type=merge -o yaml - -f testdata/minimal.yaml - -p '{"metadata":{"name":"immutable-fields"},"spec":{"projectID":"project-1","searchAttributes":{"CustomerId":"Keyword"}}}' | - kubectl apply -n $TEST_USER_NAMESPACE -f - - require: - out: - contains: namespace.cloud.temporal.io/immutable-fields created - - - name: accept-update-of-mutable-fields - exec: >- - kubectl patch namespaces.cloud.temporal.io/immutable-fields - --dry-run=server -n $TEST_USER_NAMESPACE --type=merge - -p '{"spec":{"retentionDays":7,"replicas":[{"region":"aws-us-west-2"},{"region":"aws-us-east-1"}],"searchAttributes":{"OrderId":"Int"}}}' - - - name: reject-adding-project - exec: >- - kubectl patch namespaces.cloud.temporal.io/payments-prod - --dry-run=server -n $TEST_USER_NAMESPACE --type=merge - -p '{"spec":{"projectID":"project-1"}}' - assert: - exit-code: 1 - err: - contains: projectID cannot be added, changed or removed after creation + # Updates + # + # Each update case replaces a stored Namespace with a manifest from + # manifests/updates/ that changes exactly one thing. + # + - name: create-immutable-fields + kube.apply: manifests/immutable-fields.yaml - name: reject-changing-project - exec: >- - kubectl patch namespaces.cloud.temporal.io/immutable-fields - --dry-run=server -n $TEST_USER_NAMESPACE --type=merge - -p '{"spec":{"projectID":"project-2"}}' + exec: kubectl replace -n $TEST_USER_NAMESPACE -f manifests/updates/change-project.yaml assert: exit-code: 1 err: contains: projectID cannot be added, changed or removed after creation - name: reject-removing-project - exec: >- - kubectl patch namespaces.cloud.temporal.io/immutable-fields - --dry-run=server -n $TEST_USER_NAMESPACE --type=merge - -p '{"spec":{"projectID":null}}' + exec: kubectl replace -n $TEST_USER_NAMESPACE -f manifests/updates/remove-project.yaml assert: exit-code: 1 err: contains: projectID cannot be added, changed or removed after creation - name: reject-removing-search-attribute - exec: >- - kubectl patch namespaces.cloud.temporal.io/immutable-fields - --dry-run=server -n $TEST_USER_NAMESPACE --type=merge - -p '{"spec":{"searchAttributes":{"CustomerId":null}}}' + exec: kubectl replace -n $TEST_USER_NAMESPACE -f manifests/updates/remove-search-attribute.yaml assert: exit-code: 1 err: contains: searchAttributes cannot be removed or have their type changed - name: reject-changing-search-attribute-type - exec: >- - kubectl patch namespaces.cloud.temporal.io/immutable-fields - --dry-run=server -n $TEST_USER_NAMESPACE --type=merge - -p '{"spec":{"searchAttributes":{"CustomerId":"Text"}}}' + exec: kubectl replace -n $TEST_USER_NAMESPACE -f manifests/updates/change-search-attribute-type.yaml assert: exit-code: 1 err: contains: searchAttributes cannot be removed or have their type changed + - name: create-namespace-without-project + kube.apply: manifests/minimal.yaml + + - name: reject-adding-project + exec: kubectl replace -n $TEST_USER_NAMESPACE -f manifests/updates/add-project.yaml + assert: + exit-code: 1 + err: + contains: projectID cannot be added, changed or removed after creation + + - name: create-mutable-fields + kube.apply: manifests/mutable-fields.yaml + + - name: accept-update-of-mutable-fields + exec: kubectl replace -n $TEST_USER_NAMESPACE -f manifests/updates/mutable-fields.yaml + + - name: check-mutable-fields-updated + kube.get: namespaces.cloud.temporal.io/mutable-fields + assert: + matches: + spec: + retentionDays: 7 + replicas: + - region: aws-us-west-2 + - region: aws-us-east-1 + searchAttributes: + CustomerId: Keyword + OrderId: Int + - name: delete-example kube.delete: namespaces.cloud.temporal.io/payments-prod - - name: delete-namespace-with-project-and-search-attribute + - name: delete-immutable-fields kube.delete: namespaces.cloud.temporal.io/immutable-fields + - name: delete-namespace-without-project + kube.delete: namespaces.cloud.temporal.io/minimal + + - name: delete-mutable-fields + kube.delete: namespaces.cloud.temporal.io/mutable-fields + - name: ensure-namespaces-deleted kube.get: namespaces.cloud.temporal.io assert: From 624b7679b1152215f3c69bcbf08df44cd032ce12 Mon Sep 17 00:00:00 2001 From: Tomba Leishangthem <10569680+tomba7@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:12:26 -0700 Subject: [PATCH 3/4] refactor(crd): rename Namespace ProjectID field to Project --- README.md | 2 +- api/cloud/v1alpha1/namespace.go | 6 +++--- .../crd/bases/cloud.temporal.io_namespaces.yaml | 12 ++++++------ examples/namespace.yaml | 2 +- test/e2e/namespace/manifests/immutable-fields.yaml | 2 +- test/e2e/namespace/manifests/mutable-fields.yaml | 2 +- .../e2e/namespace/manifests/updates/add-project.yaml | 2 +- .../namespace/manifests/updates/change-project.yaml | 2 +- .../updates/change-search-attribute-type.yaml | 2 +- .../namespace/manifests/updates/mutable-fields.yaml | 2 +- .../manifests/updates/remove-search-attribute.yaml | 2 +- test/e2e/namespace/validation.yaml | 6 +++--- 12 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index c54fa01..2005da8 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ The API server checks that: * `replicas` and `retentionDays` are set, and `replicas` is not empty. * Enum values are valid, replica regions are unique, and exactly one capacity mode is set. -* Updates do not change `projectID`, and do not remove search attributes or +* Updates do not change `project`, and do not remove search attributes or change their type. Temporal Cloud reports all other errors, such as invalid tags or a retention diff --git a/api/cloud/v1alpha1/namespace.go b/api/cloud/v1alpha1/namespace.go index 5566e9b..f8419d0 100644 --- a/api/cloud/v1alpha1/namespace.go +++ b/api/cloud/v1alpha1/namespace.go @@ -27,15 +27,15 @@ type Namespace struct { // // Ref: https://github.com/temporalio/cloud-api/blob/main/temporal/api/cloud/namespace/v1/message.proto // +k8s:openapi-gen=true -// +kubebuilder:validation:XValidation:rule="has(self.projectID) == has(oldSelf.projectID) && (!has(self.projectID) || self.projectID == oldSelf.projectID)",message="projectID cannot be added, changed or removed after creation",fieldPath=".projectID" +// +kubebuilder:validation:XValidation:rule="has(self.project) == has(oldSelf.project) && (!has(self.project) || self.project == oldSelf.project)",message="project cannot be added, changed or removed after creation",fieldPath=".project" // +kubebuilder:validation:XValidation:rule="!has(oldSelf.searchAttributes) || (has(self.searchAttributes) && oldSelf.searchAttributes.all(k, k in self.searchAttributes && self.searchAttributes[k] == oldSelf.searchAttributes[k]))",message="searchAttributes cannot be removed or have their type changed",fieldPath=".searchAttributes" type NamespaceSpec struct { - // ProjectID is the ID of the Temporal Cloud project that the Namespace + // Project is the identifier of the Temporal Cloud project that the Namespace // belongs to. // Maps to Temporal Cloud proto `Namespace.project_id`. // +optional // +kubebuilder:validation:MinLength=1 - ProjectID string `json:"projectID,omitempty"` + Project string `json:"project,omitempty"` // Tags are key-value pairs to attach to the Namespace. // Maps to Temporal Cloud proto `Namespace.tags`. // +optional diff --git a/deploy/kustomize/crd/bases/cloud.temporal.io_namespaces.yaml b/deploy/kustomize/crd/bases/cloud.temporal.io_namespaces.yaml index b523a61..800e3ff 100644 --- a/deploy/kustomize/crd/bases/cloud.temporal.io_namespaces.yaml +++ b/deploy/kustomize/crd/bases/cloud.temporal.io_namespaces.yaml @@ -270,9 +270,9 @@ spec: required: - enabled type: object - projectID: + project: description: |- - ProjectID is the ID of the Temporal Cloud project that the Namespace + Project is the identifier of the Temporal Cloud project that the Namespace belongs to. Maps to Temporal Cloud proto `Namespace.project_id`. minLength: 1 @@ -335,10 +335,10 @@ spec: - retentionDays type: object x-kubernetes-validations: - - fieldPath: .projectID - message: projectID cannot be added, changed or removed after creation - rule: has(self.projectID) == has(oldSelf.projectID) && (!has(self.projectID) - || self.projectID == oldSelf.projectID) + - fieldPath: .project + message: project cannot be added, changed or removed after creation + rule: has(self.project) == has(oldSelf.project) && (!has(self.project) + || self.project == oldSelf.project) - fieldPath: .searchAttributes message: searchAttributes cannot be removed or have their type changed rule: '!has(oldSelf.searchAttributes) || (has(self.searchAttributes) diff --git a/examples/namespace.yaml b/examples/namespace.yaml index 03cd09d..ee69b63 100644 --- a/examples/namespace.yaml +++ b/examples/namespace.yaml @@ -13,7 +13,7 @@ metadata: spec: # The project to create the Namespace in. If not set, the account's default # project is used. Cannot be changed after creation. - # projectID: + # project: tags: environment: production team: payments diff --git a/test/e2e/namespace/manifests/immutable-fields.yaml b/test/e2e/namespace/manifests/immutable-fields.yaml index 9050505..199f480 100644 --- a/test/e2e/namespace/manifests/immutable-fields.yaml +++ b/test/e2e/namespace/manifests/immutable-fields.yaml @@ -3,7 +3,7 @@ kind: Namespace metadata: name: immutable-fields spec: - projectID: project-1 + project: project-1 replicas: - region: aws-us-west-2 retentionDays: 30 diff --git a/test/e2e/namespace/manifests/mutable-fields.yaml b/test/e2e/namespace/manifests/mutable-fields.yaml index d291881..63483ce 100644 --- a/test/e2e/namespace/manifests/mutable-fields.yaml +++ b/test/e2e/namespace/manifests/mutable-fields.yaml @@ -3,7 +3,7 @@ kind: Namespace metadata: name: mutable-fields spec: - projectID: project-1 + project: project-1 replicas: - region: aws-us-west-2 retentionDays: 30 diff --git a/test/e2e/namespace/manifests/updates/add-project.yaml b/test/e2e/namespace/manifests/updates/add-project.yaml index 0cf1104..65e00d3 100644 --- a/test/e2e/namespace/manifests/updates/add-project.yaml +++ b/test/e2e/namespace/manifests/updates/add-project.yaml @@ -3,7 +3,7 @@ kind: Namespace metadata: name: minimal spec: - projectID: project-1 + project: project-1 replicas: - region: aws-us-west-2 retentionDays: 30 diff --git a/test/e2e/namespace/manifests/updates/change-project.yaml b/test/e2e/namespace/manifests/updates/change-project.yaml index 85d7b87..11d75b8 100644 --- a/test/e2e/namespace/manifests/updates/change-project.yaml +++ b/test/e2e/namespace/manifests/updates/change-project.yaml @@ -3,7 +3,7 @@ kind: Namespace metadata: name: immutable-fields spec: - projectID: project-2 + project: project-2 replicas: - region: aws-us-west-2 retentionDays: 30 diff --git a/test/e2e/namespace/manifests/updates/change-search-attribute-type.yaml b/test/e2e/namespace/manifests/updates/change-search-attribute-type.yaml index 6977a28..e239f8f 100644 --- a/test/e2e/namespace/manifests/updates/change-search-attribute-type.yaml +++ b/test/e2e/namespace/manifests/updates/change-search-attribute-type.yaml @@ -3,7 +3,7 @@ kind: Namespace metadata: name: immutable-fields spec: - projectID: project-1 + project: project-1 replicas: - region: aws-us-west-2 retentionDays: 30 diff --git a/test/e2e/namespace/manifests/updates/mutable-fields.yaml b/test/e2e/namespace/manifests/updates/mutable-fields.yaml index 86241b4..20c43c7 100644 --- a/test/e2e/namespace/manifests/updates/mutable-fields.yaml +++ b/test/e2e/namespace/manifests/updates/mutable-fields.yaml @@ -3,7 +3,7 @@ kind: Namespace metadata: name: mutable-fields spec: - projectID: project-1 + project: project-1 replicas: - region: aws-us-west-2 - region: aws-us-east-1 diff --git a/test/e2e/namespace/manifests/updates/remove-search-attribute.yaml b/test/e2e/namespace/manifests/updates/remove-search-attribute.yaml index a80341c..35f6058 100644 --- a/test/e2e/namespace/manifests/updates/remove-search-attribute.yaml +++ b/test/e2e/namespace/manifests/updates/remove-search-attribute.yaml @@ -3,7 +3,7 @@ kind: Namespace metadata: name: immutable-fields spec: - projectID: project-1 + project: project-1 replicas: - region: aws-us-west-2 retentionDays: 30 diff --git a/test/e2e/namespace/validation.yaml b/test/e2e/namespace/validation.yaml index 6e7bc95..34e95cc 100644 --- a/test/e2e/namespace/validation.yaml +++ b/test/e2e/namespace/validation.yaml @@ -138,14 +138,14 @@ tests: assert: exit-code: 1 err: - contains: projectID cannot be added, changed or removed after creation + contains: project cannot be added, changed or removed after creation - name: reject-removing-project exec: kubectl replace -n $TEST_USER_NAMESPACE -f manifests/updates/remove-project.yaml assert: exit-code: 1 err: - contains: projectID cannot be added, changed or removed after creation + contains: project cannot be added, changed or removed after creation - name: reject-removing-search-attribute exec: kubectl replace -n $TEST_USER_NAMESPACE -f manifests/updates/remove-search-attribute.yaml @@ -169,7 +169,7 @@ tests: assert: exit-code: 1 err: - contains: projectID cannot be added, changed or removed after creation + contains: project cannot be added, changed or removed after creation - name: create-mutable-fields kube.apply: manifests/mutable-fields.yaml From 7ab78a199caa7a94a8b8716be19d102f65b411c5 Mon Sep 17 00:00:00 2001 From: Tomba Leishangthem <10569680+tomba7@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:34:08 -0700 Subject: [PATCH 4/4] feat(crd): add shared Cloud resource types --- api/cloud/common/doc.go | 4 ++ api/cloud/common/lifecycle_spec.go | 10 +++ api/cloud/common/resource_status.go | 72 +++++++++++++++++++ api/cloud/common/zz_generated.deepcopy.go | 46 ++++++++++++ api/cloud/v1alpha1/namespace.go | 15 ++-- api/cloud/v1alpha1/zz_generated.deepcopy.go | 20 +----- .../bases/cloud.temporal.io_namespaces.yaml | 50 ++++++++++++- 7 files changed, 189 insertions(+), 28 deletions(-) create mode 100644 api/cloud/common/doc.go create mode 100644 api/cloud/common/lifecycle_spec.go create mode 100644 api/cloud/common/resource_status.go create mode 100644 api/cloud/common/zz_generated.deepcopy.go diff --git a/api/cloud/common/doc.go b/api/cloud/common/doc.go new file mode 100644 index 0000000..c62b1b6 --- /dev/null +++ b/api/cloud/common/doc.go @@ -0,0 +1,4 @@ +// Package common contains API types shared by the Temporal Cloud resource +// CRDs. +// +k8s:deepcopy-gen=package +package common diff --git a/api/cloud/common/lifecycle_spec.go b/api/cloud/common/lifecycle_spec.go new file mode 100644 index 0000000..fa4a588 --- /dev/null +++ b/api/cloud/common/lifecycle_spec.go @@ -0,0 +1,10 @@ +package common + +// LifecycleSpec configures lifecycle settings for a Temporal Cloud resource. +// Maps to Temporal Cloud proto `LifecycleSpec`. +type LifecycleSpec struct { + // EnableDeleteProtection prevents the resource from being deleted. + // Maps to Temporal Cloud proto `LifecycleSpec.enable_delete_protection`. + // +optional + EnableDeleteProtection bool `json:"enableDeleteProtection,omitempty"` +} diff --git a/api/cloud/common/resource_status.go b/api/cloud/common/resource_status.go new file mode 100644 index 0000000..f16855b --- /dev/null +++ b/api/cloud/common/resource_status.go @@ -0,0 +1,72 @@ +package common + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ResourceStatus describes the state of a Temporal Cloud resource as reported +// by Temporal Cloud. Not every resource type reports every field. Fields that +// a resource type does not report are left unset. +// +// Ref: https://github.com/temporalio/cloud-api/blob/main/temporal/api/cloud/resource/v1/message.proto +type ResourceStatus struct { + // ID is the identifier Temporal Cloud uses for the resource. Not set for + // resources that Temporal Cloud identifies by name. + // Maps to Temporal Cloud proto `id`, or `Namespace.namespace` for + // Namespaces. + // +optional + ID string `json:"id,omitempty"` + // ResourceVersion is the version of the resource in Temporal Cloud. It is + // sent with each update and delete request. + // Maps to Temporal Cloud proto `resource_version`. + // +optional + ResourceVersion string `json:"resourceVersion,omitempty"` + // State is the state of the resource in Temporal Cloud. + // Maps to Temporal Cloud proto `state`. + // +optional + State ResourceState `json:"state,omitempty"` + // AsyncOperationID is the ID of the most recent asynchronous operation + // started on the resource. + // Maps to Temporal Cloud proto `async_operation_id`. + // +optional + AsyncOperationID string `json:"asyncOperationID,omitempty"` + // CreatedTime is when the resource was created in Temporal Cloud. + // Maps to Temporal Cloud proto `created_time`. + // +optional + CreatedTime *metav1.Time `json:"createdTime,omitempty"` + // LastModifiedTime is when the resource was last modified in Temporal + // Cloud. + // Maps to Temporal Cloud proto `last_modified_time`. + // +optional + LastModifiedTime *metav1.Time `json:"lastModifiedTime,omitempty"` +} + +// ResourceState is the state of a resource in Temporal Cloud. +// Maps to Temporal Cloud proto `ResourceState`. +// +kubebuilder:validation:Enum=Activating;ActivationFailed;Active;Updating;UpdateFailed;Deleting;DeleteFailed;Deleted;Suspended;Expired +type ResourceState string + +const ( + // ResourceStateActivating means the resource is being activated. + ResourceStateActivating ResourceState = "Activating" + // ResourceStateActivationFailed means the resource failed to activate. + ResourceStateActivationFailed ResourceState = "ActivationFailed" + // ResourceStateActive means the resource is active and ready to use. + ResourceStateActive ResourceState = "Active" + // ResourceStateUpdating means the resource is being updated. + ResourceStateUpdating ResourceState = "Updating" + // ResourceStateUpdateFailed means the resource failed to update. + ResourceStateUpdateFailed ResourceState = "UpdateFailed" + // ResourceStateDeleting means the resource is being deleted. + ResourceStateDeleting ResourceState = "Deleting" + // ResourceStateDeleteFailed means the resource failed to delete. + ResourceStateDeleteFailed ResourceState = "DeleteFailed" + // ResourceStateDeleted means the resource has been deleted. + ResourceStateDeleted ResourceState = "Deleted" + // ResourceStateSuspended means the resource is suspended and not + // available for use. + ResourceStateSuspended ResourceState = "Suspended" + // ResourceStateExpired means the resource has expired and is no longer + // available for use. + ResourceStateExpired ResourceState = "Expired" +) diff --git a/api/cloud/common/zz_generated.deepcopy.go b/api/cloud/common/zz_generated.deepcopy.go new file mode 100644 index 0000000..5aa49b1 --- /dev/null +++ b/api/cloud/common/zz_generated.deepcopy.go @@ -0,0 +1,46 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package common + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LifecycleSpec) DeepCopyInto(out *LifecycleSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LifecycleSpec. +func (in *LifecycleSpec) DeepCopy() *LifecycleSpec { + if in == nil { + return nil + } + out := new(LifecycleSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceStatus) DeepCopyInto(out *ResourceStatus) { + *out = *in + if in.CreatedTime != nil { + in, out := &in.CreatedTime, &out.CreatedTime + *out = (*in).DeepCopy() + } + if in.LastModifiedTime != nil { + in, out := &in.LastModifiedTime, &out.LastModifiedTime + *out = (*in).DeepCopy() + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceStatus. +func (in *ResourceStatus) DeepCopy() *ResourceStatus { + if in == nil { + return nil + } + out := new(ResourceStatus) + in.DeepCopyInto(out) + return out +} diff --git a/api/cloud/v1alpha1/namespace.go b/api/cloud/v1alpha1/namespace.go index f8419d0..a2ffe19 100644 --- a/api/cloud/v1alpha1/namespace.go +++ b/api/cloud/v1alpha1/namespace.go @@ -4,6 +4,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" tmprlcorev1 "github.com/temporalio/kube-temporal/api/core/v1" + + cloudcommon "github.com/temporalio/resource-operator/api/cloud/common" ) // Namespace represents an instance of a Temporal Cloud Namespace. @@ -72,7 +74,7 @@ type NamespaceSpec struct { // Lifecycle configures lifecycle settings. // Maps to Temporal Cloud proto `NamespaceSpec.lifecycle`. // +optional - Lifecycle *LifecycleSpec `json:"lifecycle,omitempty"` + Lifecycle *cloudcommon.LifecycleSpec `json:"lifecycle,omitempty"` // HighAvailability configures failover behavior. // Maps to Temporal Cloud proto `NamespaceSpec.high_availability`. // +optional @@ -210,14 +212,6 @@ type CodecServerErrorMessage struct { Link string `json:"link,omitempty"` } -// LifecycleSpec configures lifecycle settings for a Namespace. -type LifecycleSpec struct { - // EnableDeleteProtection prevents the Namespace from being deleted. - // Maps to Temporal Cloud proto `LifecycleSpec.enable_delete_protection`. - // +optional - EnableDeleteProtection bool `json:"enableDeleteProtection,omitempty"` -} - // HighAvailabilitySpec configures failover behavior for a Namespace. type HighAvailabilitySpec struct { // DisableManagedFailover turns off failover managed by Temporal Cloud. @@ -308,7 +302,8 @@ const ( // NamespaceStatus describes the current status of a Temporal Cloud namespace. // +k8s:openapi-gen=true type NamespaceStatus struct { - tmprlcorev1.StatusBase `json:",inline"` + tmprlcorev1.StatusBase `json:",inline"` + cloudcommon.ResourceStatus `json:",inline"` } // NamespaceList is a list of Namespace resources diff --git a/api/cloud/v1alpha1/zz_generated.deepcopy.go b/api/cloud/v1alpha1/zz_generated.deepcopy.go index 55a00b4..eb1a6fe 100644 --- a/api/cloud/v1alpha1/zz_generated.deepcopy.go +++ b/api/cloud/v1alpha1/zz_generated.deepcopy.go @@ -6,6 +6,7 @@ package v1alpha1 import ( + common "github.com/temporalio/resource-operator/api/cloud/common" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -178,22 +179,6 @@ func (in *HighAvailabilitySpec) DeepCopy() *HighAvailabilitySpec { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LifecycleSpec) DeepCopyInto(out *LifecycleSpec) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LifecycleSpec. -func (in *LifecycleSpec) DeepCopy() *LifecycleSpec { - if in == nil { - return nil - } - out := new(LifecycleSpec) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MTLSAuthSpec) DeepCopyInto(out *MTLSAuthSpec) { *out = *in @@ -315,7 +300,7 @@ func (in *NamespaceSpec) DeepCopyInto(out *NamespaceSpec) { } if in.Lifecycle != nil { in, out := &in.Lifecycle, &out.Lifecycle - *out = new(LifecycleSpec) + *out = new(common.LifecycleSpec) **out = **in } if in.HighAvailability != nil { @@ -360,6 +345,7 @@ func (in *NamespaceSpec) DeepCopy() *NamespaceSpec { func (in *NamespaceStatus) DeepCopyInto(out *NamespaceStatus) { *out = *in in.StatusBase.DeepCopyInto(&out.StatusBase) + in.ResourceStatus.DeepCopyInto(&out.ResourceStatus) return } diff --git a/deploy/kustomize/crd/bases/cloud.temporal.io_namespaces.yaml b/deploy/kustomize/crd/bases/cloud.temporal.io_namespaces.yaml index 800e3ff..018ded9 100644 --- a/deploy/kustomize/crd/bases/cloud.temporal.io_namespaces.yaml +++ b/deploy/kustomize/crd/bases/cloud.temporal.io_namespaces.yaml @@ -217,7 +217,7 @@ spec: properties: enableDeleteProtection: description: |- - EnableDeleteProtection prevents the Namespace from being deleted. + EnableDeleteProtection prevents the resource from being deleted. Maps to Temporal Cloud proto `LifecycleSpec.enable_delete_protection`. type: boolean type: object @@ -347,6 +347,12 @@ spec: status: description: Status describes the current status of a Namespace. properties: + asyncOperationID: + description: |- + AsyncOperationID is the ID of the most recent asynchronous operation + started on the resource. + Maps to Temporal Cloud proto `async_operation_id`. + type: string conditions: description: Conditions is the collection of Conditions associated with the resource. @@ -408,6 +414,48 @@ spec: x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map + createdTime: + description: |- + CreatedTime is when the resource was created in Temporal Cloud. + Maps to Temporal Cloud proto `created_time`. + format: date-time + type: string + id: + description: |- + ID is the identifier Temporal Cloud uses for the resource. Not set for + resources that Temporal Cloud identifies by name. + Maps to Temporal Cloud proto `id`, or `Namespace.namespace` for + Namespaces. + type: string + lastModifiedTime: + description: |- + LastModifiedTime is when the resource was last modified in Temporal + Cloud. + Maps to Temporal Cloud proto `last_modified_time`. + format: date-time + type: string + resourceVersion: + description: |- + ResourceVersion is the version of the resource in Temporal Cloud. It is + sent with each update and delete request. + Maps to Temporal Cloud proto `resource_version`. + type: string + state: + description: |- + State is the state of the resource in Temporal Cloud. + Maps to Temporal Cloud proto `state`. + enum: + - Activating + - ActivationFailed + - Active + - Updating + - UpdateFailed + - Deleting + - DeleteFailed + - Deleted + - Suspended + - Expired + type: string type: object required: - spec