diff --git a/api/cloud/common/resource_status.go b/api/cloud/common/resource_status.go index 7a4637f..a981833 100644 --- a/api/cloud/common/resource_status.go +++ b/api/cloud/common/resource_status.go @@ -11,12 +11,9 @@ import ( // // 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. + // Cloud describes the resource as reported by Temporal Cloud. // +optional - ID string `json:"id,omitempty"` + Cloud CloudStatus `json:"cloud,omitzero"` // 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`. @@ -42,6 +39,17 @@ type ResourceStatus struct { LastModifiedTime *metav1.Time `json:"lastModifiedTime,omitempty"` } +// CloudStatus describes the resource as reported by Temporal Cloud. +type CloudStatus 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"` + // TODO: Add identifier fields for referenced CRs +} + // 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 diff --git a/api/cloud/common/zz_generated.deepcopy.go b/api/cloud/common/zz_generated.deepcopy.go index 5aa49b1..04a4a4a 100644 --- a/api/cloud/common/zz_generated.deepcopy.go +++ b/api/cloud/common/zz_generated.deepcopy.go @@ -5,6 +5,22 @@ package common +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudStatus) DeepCopyInto(out *CloudStatus) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudStatus. +func (in *CloudStatus) DeepCopy() *CloudStatus { + if in == nil { + return nil + } + out := new(CloudStatus) + 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 @@ -24,6 +40,7 @@ func (in *LifecycleSpec) DeepCopy() *LifecycleSpec { // 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 + out.Cloud = in.Cloud if in.CreatedTime != nil { in, out := &in.CreatedTime, &out.CreatedTime *out = (*in).DeepCopy() diff --git a/api/cloud/v1alpha1/namespace.go b/api/cloud/v1alpha1/namespace.go index 38b6580..4221ba6 100644 --- a/api/cloud/v1alpha1/namespace.go +++ b/api/cloud/v1alpha1/namespace.go @@ -17,7 +17,7 @@ import ( // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:subresource:status -// +kubebuilder:printcolumn:name="ID",type=string,JSONPath=`.status.id` +// +kubebuilder:printcolumn:name="ID",type=string,JSONPath=`.status.cloud.id` // +kubebuilder:printcolumn:name="State",type=string,JSONPath=`.status.state` // +kubebuilder:printcolumn:name="Region",type=string,JSONPath=`.spec.replicas[0].region` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` @@ -43,7 +43,7 @@ func (n *Namespace) FromProto(in *tcloudnamespacev1.Namespace) { n.Spec.Project = in.GetProjectId() n.Spec.Tags = maps.Clone(in.GetTags()) - n.Status.ID = in.GetNamespace() + n.Status.Cloud.ID = in.GetNamespace() n.Status.ResourceVersion = in.GetResourceVersion() n.Status.State.FromProto(in.GetState()) n.Status.AsyncOperationID = in.GetAsyncOperationId() diff --git a/api/cloud/v1alpha1/namespace_test.go b/api/cloud/v1alpha1/namespace_test.go index 5163ab0..2f2bbc5 100644 --- a/api/cloud/v1alpha1/namespace_test.go +++ b/api/cloud/v1alpha1/namespace_test.go @@ -210,7 +210,7 @@ func TestNamespaceFromProto_Status(t *testing.T) { LastModifiedTime: timestamppb.New(modified), }, exp: cloudcommon.ResourceStatus{ - ID: "my-ns.acct1", + Cloud: cloudcommon.CloudStatus{ID: "my-ns.acct1"}, ResourceVersion: "rv-1", State: cloudcommon.ResourceStateActive, AsyncOperationID: "op-1", diff --git a/controller/namespace/delete.go b/controller/namespace/delete.go index db95def..fa2554d 100644 --- a/controller/namespace/delete.go +++ b/controller/namespace/delete.go @@ -46,7 +46,7 @@ func (r *Reconciler) ResourceDelete( latest := original.DeepCopy() cr := latest.CR().(*tcloudv1alpha1.Namespace) - if cr.Status.ID == "" { + if cr.Status.Cloud.ID == "" { // The user deleted the CR before the operator created a Namespace in // Temporal Cloud, so there's nothing to delete. return latest, nil @@ -70,7 +70,7 @@ func (r *Reconciler) ResourceDelete( if policy == tcloudcommon.DeletionPolicyRetain { log.Info( "retaining Temporal Cloud Namespace because of the retain deletion policy", - slog.String("namespace_id", cr.Status.ID), + slog.String("namespace_id", cr.Status.Cloud.ID), slog.String("deletion_policy", string(policy)), ) return latest, nil @@ -91,7 +91,7 @@ func (r *Reconciler) ResourceDelete( // A previous delete failed. Don't send another one automatically. err = ctrlerrors.NewTerminalError(fmt.Errorf( "deleting Temporal Cloud namespace %s previously failed (async operation %s); not retrying", - cr.Status.ID, cr.Status.AsyncOperationID, + cr.Status.Cloud.ID, cr.Status.AsyncOperationID, )) return latest, err case cr.Status.State == tcloudcommon.ResourceStateDeleting && @@ -105,7 +105,7 @@ func (r *Reconciler) ResourceDelete( err = requeue.NeededAfter( fmt.Errorf( "waiting for tcloud namespace %s to finish %s before deleting", - cr.Status.ID, cr.Status.State, + cr.Status.Cloud.ID, cr.Status.State, ), requeue.DefaultRequeueAfterDuration, ) @@ -120,7 +120,7 @@ func (r *Reconciler) ResourceDelete( "tcloud namespace %s has deletion protection enabled; set "+ "spec.lifecycle.enableDeleteProtection to false to delete it, "+ "or set the %s annotation to %q to keep it", - cr.Status.ID, tcloudcommon.AnnotationKeyDeletionPolicy, + cr.Status.Cloud.ID, tcloudcommon.AnnotationKeyDeletionPolicy, tcloudcommon.DeletionPolicyRetain, ) condition.SetStalled( @@ -133,7 +133,7 @@ func (r *Reconciler) ResourceDelete( // Read the namespace so deletion protection in Temporal Cloud can be made // to match the CR, and so the delete uses the current resource version. getResp, err := tcc.GetNamespace(ctx, &tcloudservicev1.GetNamespaceRequest{ - Namespace: cr.Status.ID, + Namespace: cr.Status.Cloud.ID, }) if status.Code(err) == codes.NotFound { // Already gone. @@ -141,7 +141,7 @@ func (r *Reconciler) ResourceDelete( return latest, nil } if err != nil { - err = fmt.Errorf("failed getting tcloud namespace %s: %w", cr.Status.ID, err) + err = fmt.Errorf("failed getting tcloud namespace %s: %w", cr.Status.Cloud.ID, err) return latest, err } tns := getResp.GetNamespace() @@ -154,7 +154,7 @@ func (r *Reconciler) ResourceDelete( } resp, err := tcc.DeleteNamespace(ctx, &tcloudservicev1.DeleteNamespaceRequest{ - Namespace: cr.Status.ID, + Namespace: cr.Status.Cloud.ID, ResourceVersion: tns.GetResourceVersion(), }) if status.Code(err) == codes.NotFound { @@ -165,7 +165,7 @@ func (r *Reconciler) ResourceDelete( if err != nil { // The gRPC error includes Temporal Cloud's message, for example that // deletion protection is enabled. - err = fmt.Errorf("failed deleting tcloud namespace %s: %w", cr.Status.ID, err) + err = fmt.Errorf("failed deleting tcloud namespace %s: %w", cr.Status.Cloud.ID, err) condition.SetStalled( latest, metav1.ConditionTrue, err.Error(), tcloudcommon.ReasonResourceDeleteFailed, ) @@ -177,7 +177,7 @@ func (r *Reconciler) ResourceDelete( err = requeue.NeededAfter( fmt.Errorf( "deleting tcloud namespace %s (async operation %s)", - cr.Status.ID, cr.Status.AsyncOperationID, + cr.Status.Cloud.ID, cr.Status.AsyncOperationID, ), requeue.DefaultRequeueAfterDuration, ) @@ -242,7 +242,7 @@ func checkDelete( operationv1.AsyncOperation_STATE_CANCELLED: msg := fmt.Sprintf( "deleting tcloud namespace %s failed: async operation %s is %s: %s", - cr.Status.ID, op.GetId(), op.GetState(), op.GetFailureReason(), + cr.Status.Cloud.ID, op.GetId(), op.GetState(), op.GetFailureReason(), ) condition.SetStalled( latest, metav1.ConditionTrue, msg, tcloudcommon.ReasonResourceDeleteFailed, diff --git a/controller/namespace/delete_test.go b/controller/namespace/delete_test.go index 81c6a0f..e564f39 100644 --- a/controller/namespace/delete_test.go +++ b/controller/namespace/delete_test.go @@ -40,20 +40,20 @@ const ( var ( statusActive = tcloudcommon.ResourceStatus{ - ID: deleteTestID, + Cloud: tcloudcommon.CloudStatus{ID: deleteTestID}, ResourceVersion: deleteTestRV, State: tcloudcommon.ResourceStateActive, } statusDeleting = tcloudcommon.ResourceStatus{ - ID: deleteTestID, + Cloud: tcloudcommon.CloudStatus{ID: deleteTestID}, ResourceVersion: deleteTestRV, State: tcloudcommon.ResourceStateDeleting, AsyncOperationID: deleteTestAsyncOpID, } statusDeleteFailed = tcloudcommon.ResourceStatus{ - ID: deleteTestID, + Cloud: tcloudcommon.CloudStatus{ID: deleteTestID}, ResourceVersion: deleteTestRV, State: tcloudcommon.ResourceStateDeleteFailed, AsyncOperationID: deleteTestAsyncOpID, diff --git a/controller/namespace/reconcile.go b/controller/namespace/reconcile.go index b5f7101..cd1d8ee 100644 --- a/controller/namespace/reconcile.go +++ b/controller/namespace/reconcile.go @@ -116,7 +116,7 @@ func (r *Reconciler) ResourceSetLatest( cr := latest.(*res).cr var tns *tcloudnamespacev1.Namespace - if cr.Status.ID == "" { + if cr.Status.Cloud.ID == "" { // The qualified namespace name is only known once a reconcile after // the create has saved it to status, and status can be lost (e.g. the // CR was recreated). Look the namespace up by name so that a duplicate @@ -128,7 +128,7 @@ func (r *Reconciler) ResourceSetLatest( } else { var resp *tcloudservicev1.GetNamespaceResponse resp, err = tcc.GetNamespace(ctx, &tcloudservicev1.GetNamespaceRequest{ - Namespace: cr.Status.ID, + Namespace: cr.Status.Cloud.ID, }) if status.Code(err) == codes.NotFound { // The namespace was deleted outside of the operator. Treat it @@ -213,7 +213,7 @@ func syncNamespace( desired *tcloudv1alpha1.Namespace, latest *tcloudv1alpha1.Namespace, ) error { - if latest.Status.ID == "" { + if latest.Status.Cloud.ID == "" { tns := desired.ToProto() resp, err := tcc.CreateNamespace(ctx, &tcloudservicev1.CreateNamespaceRequest{ Spec: tns.GetSpec(), @@ -232,7 +232,7 @@ func syncNamespace( // Only an Active namespace accepts new operations. Every other state // either resolves on its own, so we wait, or needs someone to step in, so // we stop. Unknown states also wait rather than fall through to an update. - id := latest.Status.ID + id := latest.Status.Cloud.ID switch state := latest.Status.State; state { case cloudcommon.ResourceStateActive: case cloudcommon.ResourceStateActivating, cloudcommon.ResourceStateUpdating: diff --git a/controller/namespace/reconcile_test.go b/controller/namespace/reconcile_test.go index 7abe685..bb72272 100644 --- a/controller/namespace/reconcile_test.go +++ b/controller/namespace/reconcile_test.go @@ -96,7 +96,7 @@ func TestReconciler_ResourceSetLatest(t *testing.T) { project := "tmprl-project" projectOther := "tmprl-project-other" - // tns has never been synced, so it has no Status.ID and + // tns has never been synced, so it has no Status.Cloud.ID and // ResourceSetLatest looks the namespace up by name. tns := &tcloudv1alpha1.Namespace{} tns.Name = tnsName @@ -105,9 +105,9 @@ func TestReconciler_ResourceSetLatest(t *testing.T) { tns.Spec.RetentionDays = 7 // tnsSynced has been synced before, so ResourceSetLatest gets the - // namespace by its Status.ID. + // namespace by its Status.Cloud.ID. tnsSynced := tns.DeepCopy() - tnsSynced.Status.ID = tnsID + tnsSynced.Status.Cloud.ID = tnsID tnsSynced.Status.ResourceVersion = "rv-0" // cloudNS is the namespace as Temporal Cloud reports it. Its project @@ -146,7 +146,7 @@ func TestReconciler_ResourceSetLatest(t *testing.T) { RetentionDays: 30, } ns.Status.ResourceStatus = cloudcommon.ResourceStatus{ - ID: tnsID, + Cloud: cloudcommon.CloudStatus{ID: tnsID}, ResourceVersion: "rv-1", State: cloudcommon.ResourceStateActive, AsyncOperationID: "op-1", diff --git a/deploy/kustomize/crd/bases/cloud.temporal.io_namespaces.yaml b/deploy/kustomize/crd/bases/cloud.temporal.io_namespaces.yaml index e4cc6a4..5bc2930 100644 --- a/deploy/kustomize/crd/bases/cloud.temporal.io_namespaces.yaml +++ b/deploy/kustomize/crd/bases/cloud.temporal.io_namespaces.yaml @@ -15,7 +15,7 @@ spec: scope: Namespaced versions: - additionalPrinterColumns: - - jsonPath: .status.id + - jsonPath: .status.cloud.id name: ID type: string - jsonPath: .status.state @@ -366,6 +366,18 @@ spec: started on the resource. Maps to Temporal Cloud proto `async_operation_id`. type: string + cloud: + description: Cloud describes the resource as reported by Temporal + Cloud. + properties: + 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 + type: object conditions: description: Conditions is the collection of Conditions associated with the resource. @@ -433,13 +445,6 @@ spec: 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 diff --git a/docs/crd-conventions.md b/docs/crd-conventions.md index 92615c1..13fd6cc 100644 --- a/docs/crd-conventions.md +++ b/docs/crd-conventions.md @@ -47,7 +47,7 @@ For the fields a CR manages, its `spec` is the source of truth. When the control ## Example Namespace CR -This manifest illustrates the proposed CR shape. The `status` field names `cloudID` and `cloudResourceVersion` are placeholders for the Cloud reference fields, whose names are not yet decided. All values are illustrative. +This manifest illustrates the proposed CR shape. The Cloud reference fields are grouped under `status.cloud`. All values are illustrative. ```yaml apiVersion: cloud.temporal.io/v1alpha1 @@ -70,8 +70,9 @@ spec: onDemand: {} status: # Cloud reference - cloudID: payments-prod.a1b2c - cloudResourceVersion: "4" + cloud: + id: payments-prod.a1b2c + resourceVersion: "4" # Progress observedGeneration: 1 conditions: