Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions api/cloud/common/resource_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,15 @@ type ResourceStatus struct {
// Namespaces.
// +optional
ID string `json:"id,omitempty"`
// ResourceVersion is the version of the resource in Temporal Cloud. It is
// Version 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"`
Version string `json:"version,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
Expand All @@ -40,6 +35,17 @@ type ResourceStatus struct {
// Maps to Temporal Cloud proto `last_modified_time`.
// +optional
LastModifiedTime *metav1.Time `json:"lastModifiedTime,omitempty"`
// asyncOperationID is an optional field that contains the ID of the last
// async operation on this resource that was NOT in a terminal state
// (FAILED, COMPLETED, FULFILLED,REJECTED) when this reconcile loop
// executed.
//
// The reconciler removes this field when it determines the operation has
// reached a terminal state.
//
// Maps to Temporal Cloud proto `async_operation_id`.
// +optional
AsyncOperationID string `json:"asyncOperationID,omitempty"`
}

// ResourceState is the state of a resource in Temporal Cloud.
Expand Down
54 changes: 39 additions & 15 deletions api/cloud/v1alpha1/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,23 +45,33 @@ func (n Namespace) FromProto(in *tcloudnamespacev1.Namespace) *Namespace {
TypeMeta: n.TypeMeta,
ObjectMeta: n.ObjectMeta,
}
out.Spec.Project = n.Spec.Project
out.Spec.Tags = maps.Clone(n.Spec.Tags)

if in == nil {
return &out
}

var spec NamespaceSpec
out.Spec = spec.FromProto(in.GetSpec())
out.Spec.Project = in.GetProjectId()

// We use Status.Cloud.References.Project to save the project identifier
// returned from Temporal Cloud. We *never* mutate Spec.Project once it has
// been set.
out.Spec.Project = n.Spec.Project

// But we *do* update the Spec.Tags to contain the collection of tags
// returned from Temporal Cloud.
out.Spec.Tags = maps.Clone(in.GetTags())

out.Status.ID = in.GetNamespace()
out.Status.ResourceVersion = in.GetResourceVersion()
out.Status.State.FromProto(in.GetState())
out.Status.AsyncOperationID = in.GetAsyncOperationId()
out.Status.CreatedTime = transform.TimeFromProto(in.GetCreatedTime())
out.Status.LastModifiedTime = transform.TimeFromProto(in.GetLastModifiedTime())
out.Status.Resource.ID = in.GetNamespace()
out.Status.Resource.Version = in.GetResourceVersion()
out.Status.Resource.State.FromProto(in.GetState())
out.Status.Resource.AsyncOperationID = in.GetAsyncOperationId()
out.Status.Resource.CreatedTime = transform.TimeFromProto(in.GetCreatedTime())
out.Status.Resource.LastModifiedTime = transform.TimeFromProto(in.GetLastModifiedTime())

out.Status.References.Project = in.GetProjectId()
return &out
}

Expand All @@ -77,7 +87,7 @@ func (n Namespace) ToProto() *tcloudnamespacev1.Namespace {
spec.Name = n.Name
return &tcloudnamespacev1.Namespace{
Spec: spec,
ProjectId: n.Spec.Project,
ProjectId: n.Status.References.Project,
Tags: maps.Clone(n.Spec.Tags),
}
}
Expand All @@ -89,12 +99,10 @@ func (n Namespace) ToProto() *tcloudnamespacev1.Namespace {
// +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 {
// Project is the identifier of the Temporal Cloud project that the Namespace
// belongs to.
// Maps to Temporal Cloud proto `Namespace.project_id`.
// Project contains *either* a Temporal Cloud Project identifier string
// *or* a ProjectReference that points to a cloud.temporal.io/Project.
// +optional
// +kubebuilder:validation:MinLength=1
Project string `json:"project,omitempty"`
Project *ProjectReferenceOrID `json:"project,omitempty"`
// Tags are key-value pairs to attach to the Namespace.
// Maps to Temporal Cloud proto `Namespace.tags`.
// +optional
Expand Down Expand Up @@ -982,8 +990,24 @@ func (m EncryptionValidationMode) ToProto() tcloudnamespacev1.EncryptionValidati
// no corresponding NamespaceStatus struct in the Temporal Cloud API.
// +k8s:openapi-gen=true
type NamespaceStatus struct {
tmprlcorev1.StatusBase `json:",inline"`
cloudcommon.ResourceStatus `json:",inline"`
tmprlcorev1.StatusBase `json:",inline"`
// Resource contains common information about the Temporal Cloud resource
// that this CR represents.
Resource cloudcommon.ResourceStatus `json:"resource"`
// References is an optional field containing the Temporal Cloud resource
// IDs for any related resources.
References NamespaceReferences `json:"references,omitempty"`
}

// NamespaceReferences is an optional field containing the
// Temporal Cloud resource IDs for any related resources.
type NamespaceReferences struct {
// Project is the Temporal Cloud resource identifier for the Project
// containing this Temporal Namespace.
Project string `json:"project,omitempty"`
// ConnectivityRules is the set of Temporal Cloud resource identifiers for
// any Namespace ConnectivityRules associated with this Temporal Namespace
ConnectivityRules []string `json:"connectivityRules,omitempty"`
}

// NamespaceList is a list of Namespace resources
Expand Down
115 changes: 50 additions & 65 deletions api/cloud/v1alpha1/namespace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,25 @@ import (
"github.com/temporalio/cloud-operator/api/cloud/v1alpha1"
)

func newNamespace() v1alpha1.Namespace {
ns := v1alpha1.Namespace{}
ns.Namespace = "my-ns"
ns.Name = "my-name"
ns.Spec.Project = &v1alpha1.ProjectReferenceOrID{
ID: "proj-1",
}
ns.Spec.Tags = map[string]string{"a": "1"}
return ns
}

// fullSpec and fullProtoNamespace describe the same namespace with every
// field set. Pairs of booleans are given different values so that a swapped
// field mapping is caught even if both directions swap it.
func fullSpec() v1alpha1.NamespaceSpec {
return v1alpha1.NamespaceSpec{
Project: "proj-1",
Project: &v1alpha1.ProjectReferenceOrID{
ID: "proj-1",
},
Tags: map[string]string{"team": "core", "env": "prod"},
Replicas: []v1alpha1.ReplicaSpec{{Region: "aws-us-east-1"}, {Region: "aws-us-west-2"}},
RetentionDays: 30,
Expand Down Expand Up @@ -142,19 +155,25 @@ func TestNamespaceFromProto_Spec(t *testing.T) {
exp v1alpha1.NamespaceSpec
}{
{
name: "nil proto yields empty spec",
in: nil,
exp: v1alpha1.NamespaceSpec{},
name: "nil proto keeps project and tags fields",
exp: v1alpha1.NamespaceSpec{
Project: &v1alpha1.ProjectReferenceOrID{
ID: "proj-1",
},
Tags: map[string]string{"a": "1"},
},
},
{
name: "nil proto spec keeps outer fields",
name: "nil proto spec keeps project field but overrides tags field",
in: &tcloudnamespacev1.Namespace{
ProjectId: "proj-1",
Tags: map[string]string{"a": "1"},
ProjectId: "proj-other",
Tags: map[string]string{"team": "core", "env": "prod"},
},
exp: v1alpha1.NamespaceSpec{
Project: "proj-1",
Tags: map[string]string{"a": "1"},
Project: &v1alpha1.ProjectReferenceOrID{
ID: "proj-1",
},
Tags: map[string]string{"team": "core", "env": "prod"},
},
},
{
Expand All @@ -165,21 +184,35 @@ func TestNamespaceFromProto_Spec(t *testing.T) {
Replicas: []*tcloudnamespacev1.ReplicaSpec{{Region: "aws-us-east-1"}},
RetentionDays: 7,
},
Tags: map[string]string{"a": "1"},
},
exp: v1alpha1.NamespaceSpec{
Project: &v1alpha1.ProjectReferenceOrID{
ID: "proj-1",
},
Tags: map[string]string{"a": "1"},
Replicas: []v1alpha1.ReplicaSpec{{Region: "aws-us-east-1"}},
RetentionDays: 7,
},
},
{
name: "every field",
in: fullProtoNamespace(),
exp: fullSpec(),
exp: func() v1alpha1.NamespaceSpec {
ns := fullSpec()
// Project is never mutated.
ns.Project = &v1alpha1.ProjectReferenceOrID{
ID: "proj-1",
}
// But tags can be mutated from proto.
ns.Tags = map[string]string{"team": "core", "env": "prod"}
return ns
}(),
},
}
for _, tc := range cases {
t.Run(tc.name, func(tt *testing.T) {
var ns v1alpha1.Namespace
ns := newNamespace()

got := ns.FromProto(tc.in)

Expand Down Expand Up @@ -212,7 +245,7 @@ func TestNamespaceFromProto_Status(t *testing.T) {
},
exp: cloudcommon.ResourceStatus{
ID: "my-ns.acct1",
ResourceVersion: "rv-1",
Version: "rv-1",
State: cloudcommon.ResourceStateActive,
AsyncOperationID: "op-1",
CreatedTime: &metav1.Time{Time: created},
Expand All @@ -228,7 +261,7 @@ func TestNamespaceFromProto_Status(t *testing.T) {

got := ns.FromProto(tc.in)

require.Equal(tc.exp, got.Status.ResourceStatus)
require.Equal(tc.exp, got.Status.Resource)
// The qualified ID goes to status, never to metadata.name.
require.Equal("my-ns", got.Name)
})
Expand Down Expand Up @@ -256,7 +289,11 @@ func TestNamespaceToProto(t *testing.T) {
// ToProto sets only the fields that are sent to Temporal Cloud, so the
// qualified ID and status fields of the full proto are left unset.
full := fullProtoNamespace()

// The namespace and project ID are actually set from the Namespace CR's
// Metadata and Status, respectively.
full.Namespace = ""
full.ProjectId = ""

cases := []struct {
name string
Expand Down Expand Up @@ -291,58 +328,6 @@ func TestNamespaceToProto(t *testing.T) {
}
}

func TestNamespace_RoundTrip(t *testing.T) {
cases := []struct {
name string
spec v1alpha1.NamespaceSpec
}{
{
name: "minimal",
spec: v1alpha1.NamespaceSpec{
Replicas: []v1alpha1.ReplicaSpec{{Region: "aws-us-east-1"}},
RetentionDays: 7,
},
},
{
name: "every field",
spec: fullSpec(),
},
{
name: "on-demand capacity",
spec: v1alpha1.NamespaceSpec{
Replicas: []v1alpha1.ReplicaSpec{{Region: "aws-us-east-1"}},
RetentionDays: 7,
Capacity: &v1alpha1.CapacitySpec{OnDemand: &v1alpha1.OnDemandCapacity{}},
},
},
{
name: "explicitly empty blocks stay non-nil",
spec: v1alpha1.NamespaceSpec{
Replicas: []v1alpha1.ReplicaSpec{{Region: "aws-us-east-1"}},
RetentionDays: 7,
MTLSAuth: &v1alpha1.MTLSAuthSpec{},
APIKeyAuth: &v1alpha1.APIKeyAuthSpec{},
CodecServer: &v1alpha1.CodecServerSpec{CustomErrorMessage: &v1alpha1.CodecServerCustomErrorMessage{}},
Lifecycle: &cloudcommon.LifecycleSpec{},
HighAvailability: &v1alpha1.HighAvailabilitySpec{},
Fairness: &v1alpha1.FairnessSpec{},
EncryptionValidation: &v1alpha1.EncryptionValidationSpec{Mode: v1alpha1.EncryptionValidationModeDisabled},
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(tt *testing.T) {
orig := namespaceWithSpec(tc.spec)
tp := orig.ToProto()

var ns v1alpha1.Namespace
fp := ns.FromProto(tp)

require.Equal(tt, tc.spec, fp.Spec)
})
}
}

func TestCapacityFromProto(t *testing.T) {
cases := []struct {
name string
Expand Down
25 changes: 25 additions & 0 deletions api/cloud/v1alpha1/project_reference.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package v1alpha1

// ProjectReferenceOrID contains *either* a Temporal Cloud Project identifier string
// *or* a ProjectReference that points to a cloud.temporal.io/Project.
//
// +kubebuilder:validation:XValidation:rule="has(self.ref) != has(self.id)",message="exactly one of ref or id must be set"
type ProjectReferenceOrID struct {
// Ref is an optional field that points at a cloud.temporal.io/Project.
// +optional
Ref *ProjectReference `json:"ref,omitempty"`
// ID is the Temporal Cloud Project resource identifier
// +optional
ID string `json:"id,omitempty"`
}

// ProjectReference points at a cloud.temporal.io/Project.
type ProjectReference struct {
// Namespace contains the optional Kubernetes Namespace in which to find
// the Project. If empty, the Kubernetes Namespace containing the referring
// object is used.
Namespace string `json:"namespace,omitempty"`
// Name is the name of the Project.
// +required
Name string `json:"name"`
}
Loading
Loading