Skip to content

Commit 00c5f9d

Browse files
authored
Add support for namespace lifecycle (#309)
1 parent 9bb2691 commit 00c5f9d

File tree

9 files changed

+625
-56
lines changed

9 files changed

+625
-56
lines changed

docs/data-sources/namespace.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ output "namespace" {
7171
- `endpoints` (Attributes) The endpoints for the namespace. (see [below for nested schema](#nestedatt--endpoints))
7272
- `limits` (Attributes) The limits set on the namespace currently. (see [below for nested schema](#nestedatt--limits))
7373
- `name` (String) The name of the namespace.
74+
- `namespace_lifecycle` (Attributes) The lifecycle settings for the namespace. (see [below for nested schema](#nestedatt--namespace_lifecycle))
7475
- `regions` (List of String) The list of regions that this namespace is available in. If more than one region is specified, this namespace is a Multi-region Namespace, which is currently unsupported by the Terraform provider.
7576
- `retention_days` (Number) The number of days to retain workflow history. Any changes to the retention period will be applied to all new running workflows.
7677
- `state` (String) The current state of the namespace.
@@ -132,3 +133,11 @@ Read-Only:
132133
Read-Only:
133134

134135
- `actions_per_second_limit` (Number) The number of actions per second (APS) that is currently allowed for the namespace. The namespace may be throttled if its APS exceeds the limit.
136+
137+
138+
<a id="nestedatt--namespace_lifecycle"></a>
139+
### Nested Schema for `namespace_lifecycle`
140+
141+
Read-Only:
142+
143+
- `enable_delete_protection` (Boolean) If true, delete protection is enabled for the namespace. This means that the namespace cannot be deleted until this is set to false.

docs/data-sources/namespaces.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Read-Only:
6262
- `id` (String) The unique identifier of the namespace across all Temporal Cloud tenants.
6363
- `limits` (Attributes) The limits set on the namespace currently. (see [below for nested schema](#nestedatt--namespaces--limits))
6464
- `name` (String) The name of the namespace.
65+
- `namespace_lifecycle` (Attributes) The lifecycle settings for the namespace. (see [below for nested schema](#nestedatt--namespaces--namespace_lifecycle))
6566
- `regions` (List of String) The list of regions that this namespace is available in. If more than one region is specified, this namespace is a Multi-region Namespace, which is currently unsupported by the Terraform provider.
6667
- `retention_days` (Number) The number of days to retain workflow history. Any changes to the retention period will be applied to all new running workflows.
6768
- `state` (String) The current state of the namespace.
@@ -123,3 +124,11 @@ Read-Only:
123124
Read-Only:
124125

125126
- `actions_per_second_limit` (Number) The number of actions per second (APS) that is currently allowed for the namespace. The namespace may be throttled if its APS exceeds the limit.
127+
128+
129+
<a id="nestedatt--namespaces--namespace_lifecycle"></a>
130+
### Nested Schema for `namespaces.namespace_lifecycle`
131+
132+
Read-Only:
133+
134+
- `enable_delete_protection` (Boolean) If true, delete protection is enabled for the namespace. This means that the namespace cannot be deleted until this is set to false.

docs/resources/namespace.md

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,9 @@ resource "temporalcloud_namespace" "terraform" {
4242
regions = ["aws-us-east-1"]
4343
accepted_client_ca = base64encode(file("${path.module}/ca.pem"))
4444
retention_days = 14
45-
46-
// Prevents Terraform from deleting namespace. Must remove this before destroying resource.
47-
lifecycle {
48-
prevent_destroy = true
45+
namespace_lifecycle = {
46+
// Prevents namespace from being deleted accidentally. Must be updated to false before destroying resource.
47+
enable_delete_protection = true
4948
}
5049
}
5150
@@ -111,10 +110,9 @@ resource "temporalcloud_namespace" "terraform2" {
111110
regions = ["aws-us-east-1"]
112111
accepted_client_ca = base64encode(tls_self_signed_cert.ca.cert_pem)
113112
retention_days = 14
114-
115-
// Prevents Terraform from deleting namespace. Must remove this before destroying resource.
116-
lifecycle {
117-
prevent_destroy = true
113+
namespace_lifecycle = {
114+
// Prevents namespace from being deleted accidentally. Must be updated to false before destroying resource.
115+
enable_delete_protection = true
118116
}
119117
}
120118
@@ -124,10 +122,9 @@ resource "temporalcloud_namespace" "terraform3" {
124122
regions = ["aws-us-east-1"]
125123
api_key_auth = true
126124
retention_days = 14
127-
128-
// Prevents Terraform from deleting namespace. Must remove this before destroying resource.
129-
lifecycle {
130-
prevent_destroy = true
125+
namespace_lifecycle = {
126+
// Prevents namespace from being deleted accidentally. Must be updated to false before destroying resource.
127+
enable_delete_protection = true
131128
}
132129
}
133130
@@ -161,6 +158,7 @@ resource "temporalcloud_namespace" "terraform4" {
161158
- `certificate_filters` (Attributes List) A list of filters to apply to client certificates when initiating a connection Temporal Cloud. If present, connections will only be allowed from client certificates whose distinguished name properties match at least one of the filters. Empty lists are not allowed, omit the attribute instead. (see [below for nested schema](#nestedatt--certificate_filters))
162159
- `codec_server` (Attributes) A codec server is used by the Temporal Cloud UI to decode payloads for all users interacting with this namespace, even if the workflow history itself is encrypted. (see [below for nested schema](#nestedatt--codec_server))
163160
- `connectivity_rule_ids` (List of String) The IDs of the connectivity rules for this namespace.
161+
- `namespace_lifecycle` (Attributes) The lifecycle configuration for the namespace. (see [below for nested schema](#nestedatt--namespace_lifecycle))
164162
- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts))
165163

166164
### Read-Only
@@ -192,6 +190,14 @@ Optional:
192190
- `pass_access_token` (Boolean) If true, Temporal Cloud will pass the access token to the codec server upon each request.
193191

194192

193+
<a id="nestedatt--namespace_lifecycle"></a>
194+
### Nested Schema for `namespace_lifecycle`
195+
196+
Optional:
197+
198+
- `enable_delete_protection` (Boolean) If true, the namespace cannot be deleted. This is a safeguard against accidental deletion. To delete a namespace with this option enabled, you must first set it to false.
199+
200+
195201
<a id="nestedblock--timeouts"></a>
196202
### Nested Schema for `timeouts`
197203

examples/resources/temporalcloud_namespace/resource.tf

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,9 @@ resource "temporalcloud_namespace" "terraform" {
2121
regions = ["aws-us-east-1"]
2222
accepted_client_ca = base64encode(file("${path.module}/ca.pem"))
2323
retention_days = 14
24-
25-
// Prevents Terraform from deleting namespace. Must remove this before destroying resource.
26-
lifecycle {
27-
prevent_destroy = true
24+
namespace_lifecycle = {
25+
// Prevents namespace from being deleted accidentally. Must be updated to false before destroying resource.
26+
enable_delete_protection = true
2827
}
2928
}
3029

@@ -90,10 +89,9 @@ resource "temporalcloud_namespace" "terraform2" {
9089
regions = ["aws-us-east-1"]
9190
accepted_client_ca = base64encode(tls_self_signed_cert.ca.cert_pem)
9291
retention_days = 14
93-
94-
// Prevents Terraform from deleting namespace. Must remove this before destroying resource.
95-
lifecycle {
96-
prevent_destroy = true
92+
namespace_lifecycle = {
93+
// Prevents namespace from being deleted accidentally. Must be updated to false before destroying resource.
94+
enable_delete_protection = true
9795
}
9896
}
9997

@@ -103,10 +101,9 @@ resource "temporalcloud_namespace" "terraform3" {
103101
regions = ["aws-us-east-1"]
104102
api_key_auth = true
105103
retention_days = 14
106-
107-
// Prevents Terraform from deleting namespace. Must remove this before destroying resource.
108-
lifecycle {
109-
prevent_destroy = true
104+
namespace_lifecycle = {
105+
// Prevents namespace from being deleted accidentally. Must be updated to false before destroying resource.
106+
enable_delete_protection = true
110107
}
111108
}
112109

internal/provider/namespace_resource.go

Lines changed: 88 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"github.com/hashicorp/terraform-plugin-log/tflog"
3535
"google.golang.org/grpc/codes"
3636
"google.golang.org/grpc/status"
37+
"google.golang.org/protobuf/proto"
3738

3839
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
3940
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
@@ -78,9 +79,13 @@ type (
7879
ApiKeyAuth types.Bool `tfsdk:"api_key_auth"`
7980
CodecServer types.Object `tfsdk:"codec_server"`
8081
Endpoints types.Object `tfsdk:"endpoints"`
82+
NamespaceLifecycle internaltypes.ZeroObjectValue `tfsdk:"namespace_lifecycle"`
8183
ConnectivityRuleIds internaltypes.UnorderedStringListValue `tfsdk:"connectivity_rule_ids"`
84+
Timeouts timeouts.Value `tfsdk:"timeouts"`
85+
}
8286

83-
Timeouts timeouts.Value `tfsdk:"timeouts"`
87+
lifecycleModel struct {
88+
EnableDeleteProtection types.Bool `tfsdk:"enable_delete_protection"`
8489
}
8590

8691
namespaceCertificateFilterModel struct {
@@ -121,6 +126,10 @@ var (
121126
"include_cross_origin_credentials": types.BoolType,
122127
}
123128

129+
lifecycleAttrs = map[string]attr.Type{
130+
"enable_delete_protection": types.BoolType,
131+
}
132+
124133
endpointsAttrs = map[string]attr.Type{
125134
"web_address": types.StringType,
126135
"grpc_address": types.StringType,
@@ -263,6 +272,23 @@ func (r *namespaceResource) Schema(ctx context.Context, _ resource.SchemaRequest
263272
},
264273
Computed: true,
265274
},
275+
"namespace_lifecycle": schema.SingleNestedAttribute{
276+
Description: "The lifecycle configuration for the namespace.",
277+
CustomType: internaltypes.ZeroObjectType{
278+
ObjectType: basetypes.ObjectType{
279+
AttrTypes: lifecycleAttrs,
280+
},
281+
},
282+
Attributes: map[string]schema.Attribute{
283+
"enable_delete_protection": schema.BoolAttribute{
284+
Description: "If true, the namespace cannot be deleted. This is a safeguard against accidental deletion. To delete a namespace with this option enabled, you must first set it to false.",
285+
Optional: true,
286+
Computed: true,
287+
Default: booldefault.StaticBool(false),
288+
},
289+
},
290+
Optional: true,
291+
},
266292
"connectivity_rule_ids": schema.ListAttribute{
267293
Description: "The IDs of the connectivity rules for this namespace.",
268294
Optional: true,
@@ -321,17 +347,28 @@ func (r *namespaceResource) Create(ctx context.Context, req resource.CreateReque
321347
}
322348
}
323349

350+
var lifecycle *namespacev1.LifecycleSpec
351+
if !plan.NamespaceLifecycle.IsNull() && !plan.NamespaceLifecycle.IsZero(ctx) {
352+
var d diag.Diagnostics
353+
lifecycle, d = getLifecycleFromModel(ctx, &plan)
354+
resp.Diagnostics.Append(d...)
355+
if resp.Diagnostics.HasError() {
356+
return
357+
}
358+
}
359+
324360
connectivityRuleIds, d := getConnectivityRuleIdsFromModel(ctx, &plan)
325361
resp.Diagnostics.Append(d...)
326362
if resp.Diagnostics.HasError() {
327363
return
328364
}
329365

330-
var spec = &namespacev1.NamespaceSpec{
366+
spec := &namespacev1.NamespaceSpec{
331367
Name: plan.Name.ValueString(),
332368
Regions: regions,
333369
RetentionDays: int32(plan.RetentionDays.ValueInt64()),
334370
CodecServer: codecServer,
371+
Lifecycle: lifecycle,
335372
ConnectivityRuleIds: connectivityRuleIds,
336373
}
337374

@@ -404,7 +441,7 @@ func (r *namespaceResource) Read(ctx context.Context, req resource.ReadRequest,
404441
if err != nil {
405442
switch status.Code(err) {
406443
case codes.NotFound:
407-
tflog.Warn(ctx, "Namespace Resource not found, removing from state", map[string]interface{}{
444+
tflog.Warn(ctx, "Namespace Resource not found, removing from state", map[string]any{
408445
"id": state.ID.ValueString(),
409446
})
410447

@@ -484,12 +521,23 @@ func (r *namespaceResource) Update(ctx context.Context, req resource.UpdateReque
484521
}
485522
}
486523

487-
var spec = &namespacev1.NamespaceSpec{
524+
var lifecycle *namespacev1.LifecycleSpec
525+
if !plan.NamespaceLifecycle.IsNull() && !plan.NamespaceLifecycle.IsZero(ctx) {
526+
var d diag.Diagnostics
527+
lifecycle, d = getLifecycleFromModel(ctx, &plan)
528+
resp.Diagnostics.Append(d...)
529+
if resp.Diagnostics.HasError() {
530+
return
531+
}
532+
}
533+
534+
spec := &namespacev1.NamespaceSpec{
488535
Name: plan.Name.ValueString(),
489536
Regions: regions,
490537
RetentionDays: int32(plan.RetentionDays.ValueInt64()),
491538
CodecServer: codecServer,
492539
SearchAttributes: currentNs.GetNamespace().GetSpec().GetSearchAttributes(),
540+
Lifecycle: lifecycle,
493541
ConnectivityRuleIds: connectivityRuleIds,
494542
}
495543

@@ -576,7 +624,7 @@ func (r *namespaceResource) Delete(ctx context.Context, req resource.DeleteReque
576624
if err != nil {
577625
switch status.Code(err) {
578626
case codes.NotFound:
579-
tflog.Warn(ctx, "Namespace Resource not found, removing from state", map[string]interface{}{
627+
tflog.Warn(ctx, "Namespace Resource not found, removing from state", map[string]any{
580628
"id": state.ID.ValueString(),
581629
})
582630

@@ -596,7 +644,7 @@ func (r *namespaceResource) Delete(ctx context.Context, req resource.DeleteReque
596644
if err != nil {
597645
switch status.Code(err) {
598646
case codes.NotFound:
599-
tflog.Warn(ctx, "Namespace Resource not found, removing from state", map[string]interface{}{
647+
tflog.Warn(ctx, "Namespace Resource not found, removing from state", map[string]any{
600648
"id": state.ID.ValueString(),
601649
})
602650

@@ -646,13 +694,17 @@ func getConnectivityRuleIdsFromModel(ctx context.Context, plan *namespaceResourc
646694
}
647695

648696
requestConnectivityRuleIds := make([]string, len(connectivityRuleIds))
649-
for i, connectivityRuleId := range connectivityRuleIds {
650-
requestConnectivityRuleIds[i] = connectivityRuleId.ValueString()
697+
for i, id := range connectivityRuleIds {
698+
requestConnectivityRuleIds[i] = id.ValueString()
651699
}
652700
return requestConnectivityRuleIds, diags
653701
}
654702

655-
func updateModelFromSpec(ctx context.Context, state *namespaceResourceModel, ns *namespacev1.Namespace) diag.Diagnostics {
703+
func updateModelFromSpec(
704+
ctx context.Context,
705+
state *namespaceResourceModel,
706+
ns *namespacev1.Namespace,
707+
) diag.Diagnostics {
656708
var diags diag.Diagnostics
657709

658710
state.ID = types.StringValue(ns.GetNamespace())
@@ -726,6 +778,21 @@ func updateModelFromSpec(ctx context.Context, state *namespaceResourceModel, ns
726778
}
727779
state.CodecServer = codecServerState
728780

781+
if lifecycleSpec := ns.GetSpec().GetLifecycle(); lifecycleSpec != nil && !proto.Equal(lifecycleSpec, &namespacev1.LifecycleSpec{}) {
782+
lifecycle := &lifecycleModel{
783+
EnableDeleteProtection: types.BoolValue(lifecycleSpec.GetEnableDeleteProtection()),
784+
}
785+
st, objectDiags := types.ObjectValueFrom(ctx, lifecycleAttrs, lifecycle)
786+
diags.Append(objectDiags...)
787+
if diags.HasError() {
788+
return diags
789+
}
790+
state.NamespaceLifecycle = internaltypes.ZeroObjectValue{ObjectValue: st}
791+
} else if !state.NamespaceLifecycle.IsZero(ctx) {
792+
// only update the lifecycle if its not already set to zero
793+
state.NamespaceLifecycle = internaltypes.ZeroObjectValue{ObjectValue: types.ObjectNull(lifecycleAttrs)}
794+
}
795+
729796
endpoints := &endpointsModel{
730797
GrpcAddress: stringOrNull(ns.GetEndpoints().GetGrpcAddress()),
731798
WebAddress: stringOrNull(ns.GetEndpoints().GetWebAddress()),
@@ -810,6 +877,18 @@ func getCodecServerFromModel(ctx context.Context, model *namespaceResourceModel)
810877
}, diags
811878
}
812879

880+
func getLifecycleFromModel(ctx context.Context, model *namespaceResourceModel) (*namespacev1.LifecycleSpec, diag.Diagnostics) {
881+
var diags diag.Diagnostics
882+
var lifecycle lifecycleModel
883+
diags.Append(model.NamespaceLifecycle.As(ctx, &lifecycle, basetypes.ObjectAsOptions{})...)
884+
if diags.HasError() {
885+
return nil, diags
886+
}
887+
return &namespacev1.LifecycleSpec{
888+
EnableDeleteProtection: lifecycle.EnableDeleteProtection.ValueBool(),
889+
}, diags
890+
}
891+
813892
func stringOrNull(s string) types.String {
814893
if s == "" {
815894
return types.StringNull()

internal/provider/namespace_resource_test.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func TestNamespaceSchema(t *testing.T) {
4545

4646
func TestAccBasicNamespace(t *testing.T) {
4747
name := fmt.Sprintf("%s-%s", "tf-basic-namespace", randomString(10))
48-
config := func(name string, retention int) string {
48+
config := func(name string, retention int, deleteProtection bool) string {
4949
return fmt.Sprintf(`
5050
provider "temporalcloud" {
5151
@@ -71,7 +71,10 @@ PEM
7171
)
7272
7373
retention_days = %d
74-
}`, name, retention)
74+
namespace_lifecycle = {
75+
enable_delete_protection = %t
76+
}
77+
}`, name, retention, deleteProtection)
7578
}
7679

7780
resource.ParallelTest(t, resource.TestCase{
@@ -80,16 +83,19 @@ PEM
8083
Steps: []resource.TestStep{
8184
{
8285
// New namespace with retention of 7
83-
Config: config(name, 7),
86+
Config: config(name, 7, true),
8487
},
8588
{
86-
Config: config(name, 14),
89+
Config: config(name, 14, true),
8790
},
8891
{
8992
ImportState: true,
9093
ImportStateVerify: true,
9194
ResourceName: "temporalcloud_namespace.terraform",
9295
},
96+
{
97+
Config: config(name, 14, false), // disable delete protection for deletion to succeed
98+
},
9399
// Delete testing automatically occurs in TestCase
94100
},
95101
})

0 commit comments

Comments
 (0)