From 230dc1668bafc8e75c2e5518d39b68434d15a4e4 Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Tue, 23 Sep 2025 20:33:36 -0400 Subject: [PATCH 01/31] Adding Namespace Capacity Terraform Resource --- go.mod | 2 +- go.sum | 2 + internal/client/client.go | 53 ++++++++++++++++ internal/provider/namespace_resource.go | 82 +++++++++++++++++++++++++ 4 files changed, 138 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 1251d674..4760d058 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/hashicorp/terraform-plugin-testing v1.13.3 github.com/jpillora/maplock v0.0.0-20160420012925-5c725ac6e22a go.temporal.io/api v1.52.0 - go.temporal.io/cloud-sdk v0.5.0 + go.temporal.io/cloud-sdk v0.5.1-0.20250924000029-4237f0d769ff go.temporal.io/sdk v1.35.0 google.golang.org/grpc v1.75.0 google.golang.org/protobuf v1.36.8 diff --git a/go.sum b/go.sum index 7a5646bf..cb5ef021 100644 --- a/go.sum +++ b/go.sum @@ -240,6 +240,8 @@ go.temporal.io/api v1.52.0 h1:Tn69z2nhQeXtofa1/j/MbwPHnFRM9+13xqYmFl/KFjM= go.temporal.io/api v1.52.0/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= go.temporal.io/cloud-sdk v0.5.0 h1:6PdA6D8I/PiFLLpYwinre7ffPTct49zhapMAN5rJjmw= go.temporal.io/cloud-sdk v0.5.0/go.mod h1:AueDDyuayosk+zalfrnuftRqnRQTHwD0HYwNgEQc0YE= +go.temporal.io/cloud-sdk v0.5.1-0.20250924000029-4237f0d769ff h1:EjXYHBzRlnDlxw+QoUvKd7EbwZywkgjRg1wCC03JABQ= +go.temporal.io/cloud-sdk v0.5.1-0.20250924000029-4237f0d769ff/go.mod h1:AueDDyuayosk+zalfrnuftRqnRQTHwD0HYwNgEQc0YE= go.temporal.io/sdk v1.35.0 h1:lRNAQ5As9rLgYa7HBvnmKyzxLcdElTuoFJ0FXM/AsLQ= go.temporal.io/sdk v1.35.0/go.mod h1:1q5MuLc2MEJ4lneZTHJzpVebW2oZnyxoIOWX3oFVebw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/internal/client/client.go b/internal/client/client.go index b17090e3..8cbcdea3 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -31,6 +31,7 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" cloudservicev1 "go.temporal.io/cloud-sdk/api/cloudservice/v1" + "go.temporal.io/cloud-sdk/api/namespace/v1" operationv1 "go.temporal.io/cloud-sdk/api/operation/v1" "go.temporal.io/cloud-sdk/cloudclient" ) @@ -112,3 +113,55 @@ func AwaitAsyncOperation(ctx context.Context, cloudclient *Client, op *operation } } } + +func AwaitNamespaceCapacityOperation(ctx context.Context, cloudclient *Client, n *namespace.Namespace) error { + if n == nil { + return fmt.Errorf("namespace is required") + } + ns := n + + getResp, err := cloudclient.CloudService().GetNamespace(ctx, &cloudservicev1.GetNamespaceRequest{ + Namespace: ns.GetNamespace(), + }) + if err != nil { + return fmt.Errorf("failed to get namespace: %w", err) + } + ctx = tflog.SetField(ctx, "namespace_id", ns.GetNamespace()) + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + for { + select { + case <-ticker.C: + getResp, err = cloudclient.CloudService().GetNamespace(ctx, &cloudservicev1.GetNamespaceRequest{ + Namespace: ns.GetNamespace(), + }) + ns = getResp.GetNamespace() + if ns.GetCapacity().GetLatestRequest() == nil { + return nil + } + state := ns.GetCapacity().GetLatestRequest().GetState() + switch state { + case namespace.Capacity_Request_STATE_CAPACITY_REQUEST_UNSPECIFIED: + fallthrough + case namespace.Capacity_Request_STATE_CAPACITY_REQUEST_IN_PROGRESS: + tflog.Debug(ctx, "retrying in 1 second", map[string]any{ + "state": state, + }) + continue + case namespace.Capacity_Request_STATE_CAPACITY_REQUEST_FAILED: + tflog.Debug(ctx, "request failed") + return errors.New("capacity request failed") + case namespace.Capacity_Request_STATE_CAPACITY_REQUEST_COMPLETED: + tflog.Debug(ctx, "request completed") + return nil + default: + tflog.Warn(ctx, "unknown state, continuing", map[string]any{ + "state": state, + }) + continue + } + case <-ctx.Done(): + return ctx.Err() + } + } +} diff --git a/internal/provider/namespace_resource.go b/internal/provider/namespace_resource.go index b51799e4..3e8e53f9 100644 --- a/internal/provider/namespace_resource.go +++ b/internal/provider/namespace_resource.go @@ -83,6 +83,7 @@ type ( NamespaceLifecycle internaltypes.ZeroObjectValue `tfsdk:"namespace_lifecycle"` ConnectivityRuleIds internaltypes.UnorderedStringListValue `tfsdk:"connectivity_rule_ids"` Timeouts timeouts.Value `tfsdk:"timeouts"` + Capacity types.Object `tfsdk:"capacity"` } lifecycleModel struct { @@ -107,6 +108,11 @@ type ( GrpcAddress types.String `tfsdk:"grpc_address"` MtlsGrpcAddress types.String `tfsdk:"mtls_grpc_address"` } + + capacityModel struct { + Mode types.String `tfsdk:"mode"` + Value types.Float64 `tfsdk:"value"` + } ) var ( @@ -136,6 +142,11 @@ var ( "grpc_address": types.StringType, "mtls_grpc_address": types.StringType, } + + capacityAttrs = map[string]attr.Type{ + "mode": types.StringType, + "value": types.Float64Type, + } ) func NewNamespaceResource() resource.Resource { @@ -304,6 +315,19 @@ func (r *namespaceResource) Schema(ctx context.Context, _ resource.SchemaRequest listvalidator.SizeAtLeast(1), }, }, + "capacity": schema.SingleNestedAttribute{ + Description: "The capacity configuration for the namespace.", + Attributes: map[string]schema.Attribute{ + "mode": schema.StringAttribute{ + Description: "The mode of the capacity configuration. Must be one of 'provisioned' or 'on_demand'.", + Optional: true, + }, + "value": schema.Float64Attribute{ + Description: "The value of the capacity configuration. Must be set when mode is 'provisioned'.", + Optional: true, + }, + }, + }, }, Blocks: map[string]schema.Block{ "timeouts": timeouts.Block(ctx, timeouts.Opts{ @@ -600,6 +624,11 @@ func (r *namespaceResource) Update(ctx context.Context, req resource.UpdateReque return } + if err := client.AwaitNamespaceCapacityOperation(ctx, r.client, ns.Namespace); err != nil { + resp.Diagnostics.AddError("Failed to update namespace capacity", err.Error()) + return + } + resp.Diagnostics.Append(updateModelFromSpec(ctx, &plan, ns.Namespace)...) if resp.Diagnostics.HasError() { return @@ -824,8 +853,29 @@ func updateModelFromSpec( connectivityRuleIdsState = internaltypes.UnorderedStringListValue{ ListValue: planConnectivityRuleIds, } + } + capacitySpec := ns.GetSpec().GetCapacitySpec() + var capacityMode types.String + var capacityValue types.Float64 + if capacitySpec != nil { + if capacitySpec.GetOnDemand() != nil { + capacityMode = types.StringValue("on_demand") + } else if capacitySpec.GetProvisioned() != nil { + capacityMode = types.StringValue("provisioned") + capacityValue = types.Float64Value(capacitySpec.GetProvisioned().GetValue()) + } + capacity, objectDiags := types.ObjectValueFrom(ctx, capacityAttrs, &capacityModel{ + Mode: capacityMode, + Value: capacityValue, + }) + diags.Append(objectDiags...) + if diags.HasError() { + return diags + } + state.Capacity = capacity } + state.ConnectivityRuleIds = connectivityRuleIdsState state.Endpoints = endpointsState state.Regions = planRegionsUnordered @@ -893,6 +943,38 @@ func getLifecycleFromModel(ctx context.Context, model *namespaceResourceModel) ( }, diags } +func getCapacityFromModel(ctx context.Context, model *namespaceResourceModel) (*namespacev1.CapacitySpec, diag.Diagnostics) { + var diags diag.Diagnostics + var capacity capacityModel + diags.Append(model.Capacity.As(ctx, &capacity, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return nil, diags + } + switch capacity.Mode.ValueString() { + case "provisioned": + if capacity.Value.IsNull() || capacity.Value.ValueFloat64() <= 0 { + diags.Append(diag.NewErrorDiagnostic("Invalid capacity value", "Capacity value must be set when mode is 'provisioned'")) + return nil, diags + } + return &namespacev1.CapacitySpec{ + Spec: &namespacev1.CapacitySpec_Provisioned_{ + Provisioned: &namespacev1.CapacitySpec_Provisioned{ + Value: capacity.Value.ValueFloat64(), + }, + }, + }, diags + case "on_demand": + return &namespacev1.CapacitySpec{ + Spec: &namespacev1.CapacitySpec_OnDemand_{ + OnDemand: &namespacev1.CapacitySpec_OnDemand{}, + }, + }, diags + default: + diags.Append(diag.NewErrorDiagnostic("Invalid capacity mode", "Invalid capacity mode: "+capacity.Mode.ValueString())) + return nil, diags + } +} + func stringOrNull(s string) types.String { if s == "" { return types.StringNull() From bc58debc3c9080509094bfc8d44cc3485066f90c Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Tue, 23 Sep 2025 20:38:29 -0400 Subject: [PATCH 02/31] fix --- internal/client/client.go | 11 ++++------- internal/provider/namespace_resource.go | 10 ++++++++++ internal/provider/namespace_resource_test.go | 5 +++++ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/internal/client/client.go b/internal/client/client.go index 8cbcdea3..ee5a2d19 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -120,21 +120,18 @@ func AwaitNamespaceCapacityOperation(ctx context.Context, cloudclient *Client, n } ns := n - getResp, err := cloudclient.CloudService().GetNamespace(ctx, &cloudservicev1.GetNamespaceRequest{ - Namespace: ns.GetNamespace(), - }) - if err != nil { - return fmt.Errorf("failed to get namespace: %w", err) - } ctx = tflog.SetField(ctx, "namespace_id", ns.GetNamespace()) ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case <-ticker.C: - getResp, err = cloudclient.CloudService().GetNamespace(ctx, &cloudservicev1.GetNamespaceRequest{ + getResp, err := cloudclient.CloudService().GetNamespace(ctx, &cloudservicev1.GetNamespaceRequest{ Namespace: ns.GetNamespace(), }) + if err != nil { + return fmt.Errorf("failed to get namespace: %w", err) + } ns = getResp.GetNamespace() if ns.GetCapacity().GetLatestRequest() == nil { return nil diff --git a/internal/provider/namespace_resource.go b/internal/provider/namespace_resource.go index 3e8e53f9..59cd9245 100644 --- a/internal/provider/namespace_resource.go +++ b/internal/provider/namespace_resource.go @@ -595,6 +595,16 @@ func (r *namespaceResource) Update(ctx context.Context, req resource.UpdateReque spec.MtlsAuth = mtls } + if !plan.Capacity.IsNull() { + var d diag.Diagnostics + capacitySpec, d := getCapacityFromModel(ctx, &plan) + resp.Diagnostics.Append(d...) + if resp.Diagnostics.HasError() { + return + } + spec.CapacitySpec = capacitySpec + } + if !areRegionsEqual(currentNs.GetNamespace().GetSpec().GetRegions(), spec.Regions) { resp.Diagnostics.AddError("Namespace regions cannot be changed", "Changing the regions of a namespace is not supported currently via terraform.") return diff --git a/internal/provider/namespace_resource_test.go b/internal/provider/namespace_resource_test.go index 4eb4960d..ea821473 100644 --- a/internal/provider/namespace_resource_test.go +++ b/internal/provider/namespace_resource_test.go @@ -13,10 +13,15 @@ import ( "text/template" fwresource "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" cloudservicev1 "go.temporal.io/cloud-sdk/api/cloudservice/v1" + namespacev1 "go.temporal.io/cloud-sdk/api/namespace/v1" "github.com/temporalio/terraform-provider-temporalcloud/internal/client" ) From 987e35f57cddc00425f72d403e86752732ed4ab0 Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Tue, 23 Sep 2025 20:39:00 -0400 Subject: [PATCH 03/31] optional --- internal/provider/namespace_resource.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/provider/namespace_resource.go b/internal/provider/namespace_resource.go index 59cd9245..5c6da54b 100644 --- a/internal/provider/namespace_resource.go +++ b/internal/provider/namespace_resource.go @@ -316,6 +316,7 @@ func (r *namespaceResource) Schema(ctx context.Context, _ resource.SchemaRequest }, }, "capacity": schema.SingleNestedAttribute{ + Optional: true, Description: "The capacity configuration for the namespace.", Attributes: map[string]schema.Attribute{ "mode": schema.StringAttribute{ From 89042e05f6f099042ced97ed4b109587b387f811 Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Wed, 24 Sep 2025 06:32:09 -0400 Subject: [PATCH 04/31] go mod tidy + tests --- go.mod | 3 + go.sum | 2 - internal/provider/namespace_resource_test.go | 246 +++++++++++++++++++ 3 files changed, 249 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 4760d058..1311a8d9 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-testing v1.13.3 github.com/jpillora/maplock v0.0.0-20160420012925-5c725ac6e22a + github.com/stretchr/testify v1.10.0 go.temporal.io/api v1.52.0 go.temporal.io/cloud-sdk v0.5.1-0.20250924000029-4237f0d769ff go.temporal.io/sdk v1.35.0 @@ -34,6 +35,7 @@ require ( github.com/bgentry/speakeasy v0.1.0 // indirect github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.18.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -71,6 +73,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/oklog/run v1.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/posener/complete v1.2.3 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/spf13/cast v1.5.0 // indirect diff --git a/go.sum b/go.sum index cb5ef021..e27e1ae8 100644 --- a/go.sum +++ b/go.sum @@ -238,8 +238,6 @@ go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mx go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.temporal.io/api v1.52.0 h1:Tn69z2nhQeXtofa1/j/MbwPHnFRM9+13xqYmFl/KFjM= go.temporal.io/api v1.52.0/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= -go.temporal.io/cloud-sdk v0.5.0 h1:6PdA6D8I/PiFLLpYwinre7ffPTct49zhapMAN5rJjmw= -go.temporal.io/cloud-sdk v0.5.0/go.mod h1:AueDDyuayosk+zalfrnuftRqnRQTHwD0HYwNgEQc0YE= go.temporal.io/cloud-sdk v0.5.1-0.20250924000029-4237f0d769ff h1:EjXYHBzRlnDlxw+QoUvKd7EbwZywkgjRg1wCC03JABQ= go.temporal.io/cloud-sdk v0.5.1-0.20250924000029-4237f0d769ff/go.mod h1:AueDDyuayosk+zalfrnuftRqnRQTHwD0HYwNgEQc0YE= go.temporal.io/sdk v1.35.0 h1:lRNAQ5As9rLgYa7HBvnmKyzxLcdElTuoFJ0FXM/AsLQ= diff --git a/internal/provider/namespace_resource_test.go b/internal/provider/namespace_resource_test.go index ea821473..7a9aa432 100644 --- a/internal/provider/namespace_resource_test.go +++ b/internal/provider/namespace_resource_test.go @@ -796,3 +796,249 @@ PEM }, }) } + +func TestGetCapacityFromModel(t *testing.T) { + ctx := context.Background() + + tests := []struct { + name string + capacityModel capacityModel + expectedSpec *namespacev1.CapacitySpec + expectError bool + errorMessage string + }{ + { + name: "provisioned capacity with valid value", + capacityModel: capacityModel{ + Mode: types.StringValue("provisioned"), + Value: types.Float64Value(100.0), + }, + expectedSpec: &namespacev1.CapacitySpec{ + Spec: &namespacev1.CapacitySpec_Provisioned_{ + Provisioned: &namespacev1.CapacitySpec_Provisioned{ + Value: 100.0, + }, + }, + }, + expectError: false, + }, + { + name: "on_demand capacity", + capacityModel: capacityModel{ + Mode: types.StringValue("on_demand"), + Value: types.Float64Null(), + }, + expectedSpec: &namespacev1.CapacitySpec{ + Spec: &namespacev1.CapacitySpec_OnDemand_{ + OnDemand: &namespacev1.CapacitySpec_OnDemand{}, + }, + }, + expectError: false, + }, + { + name: "provisioned capacity with zero value should fail", + capacityModel: capacityModel{ + Mode: types.StringValue("provisioned"), + Value: types.Float64Value(0.0), + }, + expectedSpec: nil, + expectError: true, + errorMessage: "Invalid capacity value", + }, + { + name: "provisioned capacity with negative value should fail", + capacityModel: capacityModel{ + Mode: types.StringValue("provisioned"), + Value: types.Float64Value(-10.0), + }, + expectedSpec: nil, + expectError: true, + errorMessage: "Invalid capacity value", + }, + { + name: "provisioned capacity with null value should fail", + capacityModel: capacityModel{ + Mode: types.StringValue("provisioned"), + Value: types.Float64Null(), + }, + expectedSpec: nil, + expectError: true, + errorMessage: "Invalid capacity value", + }, + { + name: "invalid capacity mode should fail", + capacityModel: capacityModel{ + Mode: types.StringValue("invalid_mode"), + Value: types.Float64Value(50.0), + }, + expectedSpec: nil, + expectError: true, + errorMessage: "Invalid capacity mode", + }, + { + name: "empty capacity mode should fail", + capacityModel: capacityModel{ + Mode: types.StringValue(""), + Value: types.Float64Value(50.0), + }, + expectedSpec: nil, + expectError: true, + errorMessage: "Invalid capacity mode", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a capacity object from the model + capacity, diags := types.ObjectValueFrom(ctx, capacityAttrs, &tt.capacityModel) + require.False(t, diags.HasError(), "Failed to create capacity object: %v", diags.Errors()) + + // Create a namespace model with the capacity + model := &namespaceResourceModel{ + Capacity: capacity, + } + + // Call the function under test + spec, diags := getCapacityFromModel(ctx, model) + + if tt.expectError { + assert.True(t, diags.HasError(), "Expected error but got none") + if tt.errorMessage != "" { + found := false + for _, diag := range diags.Errors() { + if strings.Contains(diag.Summary(), tt.errorMessage) { + found = true + break + } + } + assert.True(t, found, "Expected error message '%s' not found in diagnostics: %v", tt.errorMessage, diags.Errors()) + } + assert.Nil(t, spec, "Expected nil spec on error") + } else { + assert.False(t, diags.HasError(), "Unexpected error: %v", diags.Errors()) + assert.NotNil(t, spec, "Expected non-nil spec") + + // Compare the specs + if tt.expectedSpec.GetProvisioned() != nil { + require.NotNil(t, spec.GetProvisioned(), "Expected provisioned capacity spec") + assert.Equal(t, tt.expectedSpec.GetProvisioned().GetValue(), spec.GetProvisioned().GetValue()) + } + + if tt.expectedSpec.GetOnDemand() != nil { + require.NotNil(t, spec.GetOnDemand(), "Expected on-demand capacity spec") + } + } + }) + } +} + +func TestCapacityModelValidation(t *testing.T) { + ctx := context.Background() + + t.Run("capacity model can be created from object value", func(t *testing.T) { + testCapacity := capacityModel{ + Mode: types.StringValue("provisioned"), + Value: types.Float64Value(50.0), + } + + capacity, diags := types.ObjectValueFrom(ctx, capacityAttrs, &testCapacity) + require.False(t, diags.HasError(), "Failed to create capacity object: %v", diags.Errors()) + + var retrievedCapacity capacityModel + diags = capacity.As(ctx, &retrievedCapacity, basetypes.ObjectAsOptions{}) + require.False(t, diags.HasError(), "Failed to retrieve capacity model: %v", diags.Errors()) + + assert.Equal(t, "provisioned", retrievedCapacity.Mode.ValueString()) + assert.Equal(t, 50.0, retrievedCapacity.Value.ValueFloat64()) + }) + + t.Run("capacity model can handle null values", func(t *testing.T) { + testCapacity := capacityModel{ + Mode: types.StringValue("on_demand"), + Value: types.Float64Null(), + } + + capacity, diags := types.ObjectValueFrom(ctx, capacityAttrs, &testCapacity) + require.False(t, diags.HasError(), "Failed to create capacity object: %v", diags.Errors()) + + var retrievedCapacity capacityModel + diags = capacity.As(ctx, &retrievedCapacity, basetypes.ObjectAsOptions{}) + require.False(t, diags.HasError(), "Failed to retrieve capacity model: %v", diags.Errors()) + + assert.Equal(t, "on_demand", retrievedCapacity.Mode.ValueString()) + assert.True(t, retrievedCapacity.Value.IsNull()) + }) +} + +func TestCapacitySpecParsing(t *testing.T) { + tests := []struct { + name string + capacitySpec *namespacev1.CapacitySpec + expectedMode string + expectedValue *float64 + expectNullValue bool + }{ + { + name: "provisioned capacity spec", + capacitySpec: &namespacev1.CapacitySpec{ + Spec: &namespacev1.CapacitySpec_Provisioned_{ + Provisioned: &namespacev1.CapacitySpec_Provisioned{ + Value: 75.5, + }, + }, + }, + expectedMode: "provisioned", + expectedValue: func() *float64 { v := 75.5; return &v }(), + }, + { + name: "on-demand capacity spec", + capacitySpec: &namespacev1.CapacitySpec{ + Spec: &namespacev1.CapacitySpec_OnDemand_{ + OnDemand: &namespacev1.CapacitySpec_OnDemand{}, + }, + }, + expectedMode: "on_demand", + expectNullValue: true, + }, + { + name: "nil capacity spec", + capacitySpec: nil, + expectedMode: "", + expectNullValue: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test the capacity parsing logic from updateModelFromSpec function + var capacityMode types.String + var capacityValue types.Float64 + + if tt.capacitySpec != nil { + if tt.capacitySpec.GetOnDemand() != nil { + capacityMode = types.StringValue("on_demand") + capacityValue = types.Float64Null() + } else if tt.capacitySpec.GetProvisioned() != nil { + capacityMode = types.StringValue("provisioned") + capacityValue = types.Float64Value(tt.capacitySpec.GetProvisioned().GetValue()) + } + } else { + capacityMode = types.StringNull() + capacityValue = types.Float64Null() + } + + if tt.expectedMode != "" { + assert.Equal(t, tt.expectedMode, capacityMode.ValueString()) + } else { + assert.True(t, capacityMode.IsNull()) + } + + if tt.expectNullValue { + assert.True(t, capacityValue.IsNull()) + } else { + require.NotNil(t, tt.expectedValue) + assert.Equal(t, *tt.expectedValue, capacityValue.ValueFloat64()) + } + }) + } +} From 66305eb43a9b49c6bf6ddf6b10cd345368284ef0 Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Wed, 24 Sep 2025 06:33:11 -0400 Subject: [PATCH 05/31] fix --- docs/resources/namespace.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/resources/namespace.md b/docs/resources/namespace.md index 1b9bb37e..e2a0fbd4 100644 --- a/docs/resources/namespace.md +++ b/docs/resources/namespace.md @@ -155,6 +155,7 @@ resource "temporalcloud_namespace" "terraform4" { - `accepted_client_ca` (String) The Base64-encoded CA cert in PEM format that clients use when authenticating with Temporal Cloud. This is a required field when a Namespace uses mTLS authentication. - `api_key_auth` (Boolean) If true, Temporal Cloud will enable API key authentication for this namespace. +- `capacity` (Attributes) The capacity configuration for the namespace. (see [below for nested schema](#nestedatt--capacity)) - `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)) - `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)) - `connectivity_rule_ids` (List of String) The IDs of the connectivity rules for this namespace. @@ -166,6 +167,15 @@ resource "temporalcloud_namespace" "terraform4" { - `endpoints` (Attributes) The endpoints for the namespace. (see [below for nested schema](#nestedatt--endpoints)) - `id` (String) The unique identifier of the namespace across all Temporal Cloud tenants. + +### Nested Schema for `capacity` + +Optional: + +- `mode` (String) The mode of the capacity configuration. Must be one of 'provisioned' or 'on_demand'. +- `value` (Number) The value of the capacity configuration. Must be set when mode is 'provisioned'. + + ### Nested Schema for `certificate_filters` From 4f7e44c7460c3772a5fad15708ad400b72c9fdad Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Wed, 24 Sep 2025 06:43:22 -0400 Subject: [PATCH 06/31] acceptance test --- internal/provider/namespace_resource_test.go | 292 ++++--------------- 1 file changed, 52 insertions(+), 240 deletions(-) diff --git a/internal/provider/namespace_resource_test.go b/internal/provider/namespace_resource_test.go index 7a9aa432..f6771d23 100644 --- a/internal/provider/namespace_resource_test.go +++ b/internal/provider/namespace_resource_test.go @@ -13,15 +13,10 @@ import ( "text/template" fwresource "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" cloudservicev1 "go.temporal.io/cloud-sdk/api/cloudservice/v1" - namespacev1 "go.temporal.io/cloud-sdk/api/namespace/v1" "github.com/temporalio/terraform-provider-temporalcloud/internal/client" ) @@ -797,248 +792,65 @@ PEM }) } -func TestGetCapacityFromModel(t *testing.T) { - ctx := context.Background() - - tests := []struct { - name string - capacityModel capacityModel - expectedSpec *namespacev1.CapacitySpec - expectError bool - errorMessage string - }{ - { - name: "provisioned capacity with valid value", - capacityModel: capacityModel{ - Mode: types.StringValue("provisioned"), - Value: types.Float64Value(100.0), - }, - expectedSpec: &namespacev1.CapacitySpec{ - Spec: &namespacev1.CapacitySpec_Provisioned_{ - Provisioned: &namespacev1.CapacitySpec_Provisioned{ - Value: 100.0, - }, - }, - }, - expectError: false, - }, - { - name: "on_demand capacity", - capacityModel: capacityModel{ - Mode: types.StringValue("on_demand"), - Value: types.Float64Null(), - }, - expectedSpec: &namespacev1.CapacitySpec{ - Spec: &namespacev1.CapacitySpec_OnDemand_{ - OnDemand: &namespacev1.CapacitySpec_OnDemand{}, - }, - }, - expectError: false, - }, - { - name: "provisioned capacity with zero value should fail", - capacityModel: capacityModel{ - Mode: types.StringValue("provisioned"), - Value: types.Float64Value(0.0), - }, - expectedSpec: nil, - expectError: true, - errorMessage: "Invalid capacity value", - }, - { - name: "provisioned capacity with negative value should fail", - capacityModel: capacityModel{ - Mode: types.StringValue("provisioned"), - Value: types.Float64Value(-10.0), - }, - expectedSpec: nil, - expectError: true, - errorMessage: "Invalid capacity value", - }, - { - name: "provisioned capacity with null value should fail", - capacityModel: capacityModel{ - Mode: types.StringValue("provisioned"), - Value: types.Float64Null(), - }, - expectedSpec: nil, - expectError: true, - errorMessage: "Invalid capacity value", - }, - { - name: "invalid capacity mode should fail", - capacityModel: capacityModel{ - Mode: types.StringValue("invalid_mode"), - Value: types.Float64Value(50.0), - }, - expectedSpec: nil, - expectError: true, - errorMessage: "Invalid capacity mode", - }, - { - name: "empty capacity mode should fail", - capacityModel: capacityModel{ - Mode: types.StringValue(""), - Value: types.Float64Value(50.0), - }, - expectedSpec: nil, - expectError: true, - errorMessage: "Invalid capacity mode", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a capacity object from the model - capacity, diags := types.ObjectValueFrom(ctx, capacityAttrs, &tt.capacityModel) - require.False(t, diags.HasError(), "Failed to create capacity object: %v", diags.Errors()) - - // Create a namespace model with the capacity - model := &namespaceResourceModel{ - Capacity: capacity, - } +func TestAccNamespaceWithCapacity(t *testing.T) { + name := fmt.Sprintf("%s-%s", "tf-capacity", randomString(10)) + config := func(name string, retention int, deleteProtection bool, mode string, value int) string { + return fmt.Sprintf(` +provider "temporalcloud" { - // Call the function under test - spec, diags := getCapacityFromModel(ctx, model) - - if tt.expectError { - assert.True(t, diags.HasError(), "Expected error but got none") - if tt.errorMessage != "" { - found := false - for _, diag := range diags.Errors() { - if strings.Contains(diag.Summary(), tt.errorMessage) { - found = true - break - } - } - assert.True(t, found, "Expected error message '%s' not found in diagnostics: %v", tt.errorMessage, diags.Errors()) - } - assert.Nil(t, spec, "Expected nil spec on error") - } else { - assert.False(t, diags.HasError(), "Unexpected error: %v", diags.Errors()) - assert.NotNil(t, spec, "Expected non-nil spec") - - // Compare the specs - if tt.expectedSpec.GetProvisioned() != nil { - require.NotNil(t, spec.GetProvisioned(), "Expected provisioned capacity spec") - assert.Equal(t, tt.expectedSpec.GetProvisioned().GetValue(), spec.GetProvisioned().GetValue()) - } - - if tt.expectedSpec.GetOnDemand() != nil { - require.NotNil(t, spec.GetOnDemand(), "Expected on-demand capacity spec") - } - } - }) - } } -func TestCapacityModelValidation(t *testing.T) { - ctx := context.Background() - - t.Run("capacity model can be created from object value", func(t *testing.T) { - testCapacity := capacityModel{ - Mode: types.StringValue("provisioned"), - Value: types.Float64Value(50.0), - } - - capacity, diags := types.ObjectValueFrom(ctx, capacityAttrs, &testCapacity) - require.False(t, diags.HasError(), "Failed to create capacity object: %v", diags.Errors()) - - var retrievedCapacity capacityModel - diags = capacity.As(ctx, &retrievedCapacity, basetypes.ObjectAsOptions{}) - require.False(t, diags.HasError(), "Failed to retrieve capacity model: %v", diags.Errors()) - - assert.Equal(t, "provisioned", retrievedCapacity.Mode.ValueString()) - assert.Equal(t, 50.0, retrievedCapacity.Value.ValueFloat64()) - }) - - t.Run("capacity model can handle null values", func(t *testing.T) { - testCapacity := capacityModel{ - Mode: types.StringValue("on_demand"), - Value: types.Float64Null(), - } - - capacity, diags := types.ObjectValueFrom(ctx, capacityAttrs, &testCapacity) - require.False(t, diags.HasError(), "Failed to create capacity object: %v", diags.Errors()) - - var retrievedCapacity capacityModel - diags = capacity.As(ctx, &retrievedCapacity, basetypes.ObjectAsOptions{}) - require.False(t, diags.HasError(), "Failed to retrieve capacity model: %v", diags.Errors()) +resource "temporalcloud_namespace" "terraform" { + name = "%s" + regions = ["aws-us-east-1"] + accepted_client_ca = base64encode(< Date: Wed, 24 Sep 2025 09:13:31 -0400 Subject: [PATCH 07/31] true --- internal/provider/namespace_resource.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/internal/provider/namespace_resource.go b/internal/provider/namespace_resource.go index 5c6da54b..53c3ae39 100644 --- a/internal/provider/namespace_resource.go +++ b/internal/provider/namespace_resource.go @@ -401,6 +401,16 @@ func (r *namespaceResource) Create(ctx context.Context, req resource.CreateReque ConnectivityRuleIds: connectivityRuleIds, } + if !plan.Capacity.IsNull() { + var d diag.Diagnostics + capacitySpec, d := getCapacityFromModel(ctx, &plan) + resp.Diagnostics.Append(d...) + if resp.Diagnostics.HasError() { + return + } + spec.CapacitySpec = capacitySpec + } + if !plan.ApiKeyAuth.ValueBool() && plan.AcceptedClientCA.IsNull() { resp.Diagnostics.AddError("Namespace not configured with authentication", "accepted_client_ca or api_key_auth must be set") return @@ -872,6 +882,7 @@ func updateModelFromSpec( if capacitySpec != nil { if capacitySpec.GetOnDemand() != nil { capacityMode = types.StringValue("on_demand") + capacityValue = types.Float64Value(0) } else if capacitySpec.GetProvisioned() != nil { capacityMode = types.StringValue("provisioned") capacityValue = types.Float64Value(capacitySpec.GetProvisioned().GetValue()) @@ -885,6 +896,8 @@ func updateModelFromSpec( return diags } state.Capacity = capacity + } else { + state.Capacity = types.ObjectNull(capacityAttrs) } state.ConnectivityRuleIds = connectivityRuleIdsState From 1ea78bcdf8b8ab7efca70ce6f0d4a12030c62fe4 Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Wed, 24 Sep 2025 09:24:50 -0400 Subject: [PATCH 08/31] testfix --- internal/provider/namespace_resource.go | 1 - internal/provider/namespace_resource_test.go | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/internal/provider/namespace_resource.go b/internal/provider/namespace_resource.go index 53c3ae39..d42594c7 100644 --- a/internal/provider/namespace_resource.go +++ b/internal/provider/namespace_resource.go @@ -882,7 +882,6 @@ func updateModelFromSpec( if capacitySpec != nil { if capacitySpec.GetOnDemand() != nil { capacityMode = types.StringValue("on_demand") - capacityValue = types.Float64Value(0) } else if capacitySpec.GetProvisioned() != nil { capacityMode = types.StringValue("provisioned") capacityValue = types.Float64Value(capacitySpec.GetProvisioned().GetValue()) diff --git a/internal/provider/namespace_resource_test.go b/internal/provider/namespace_resource_test.go index f6771d23..33f9e4b4 100644 --- a/internal/provider/namespace_resource_test.go +++ b/internal/provider/namespace_resource_test.go @@ -794,13 +794,13 @@ PEM func TestAccNamespaceWithCapacity(t *testing.T) { name := fmt.Sprintf("%s-%s", "tf-capacity", randomString(10)) - config := func(name string, retention int, deleteProtection bool, mode string, value int) string { + config := func(name string, retention int, deleteProtection bool, mode string, value string) string { return fmt.Sprintf(` provider "temporalcloud" { } -resource "temporalcloud_namespace" "terraform" { +resource "temporalcloud_namespace" "capacitytest" { name = "%s" regions = ["aws-us-east-1"] accepted_client_ca = base64encode(< Date: Thu, 25 Sep 2025 09:55:33 -0400 Subject: [PATCH 09/31] Acceptance tests --- internal/client/client.go | 1 + internal/provider/namespace_resource.go | 17 ++++--- internal/provider/namespace_resource_test.go | 51 ++++++++++++++------ 3 files changed, 48 insertions(+), 21 deletions(-) diff --git a/internal/client/client.go b/internal/client/client.go index ee5a2d19..f31eebb5 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -133,6 +133,7 @@ func AwaitNamespaceCapacityOperation(ctx context.Context, cloudclient *Client, n return fmt.Errorf("failed to get namespace: %w", err) } ns = getResp.GetNamespace() + n.Capacity = ns.GetCapacity() if ns.GetCapacity().GetLatestRequest() == nil { return nil } diff --git a/internal/provider/namespace_resource.go b/internal/provider/namespace_resource.go index d42594c7..381b5f71 100644 --- a/internal/provider/namespace_resource.go +++ b/internal/provider/namespace_resource.go @@ -402,13 +402,16 @@ func (r *namespaceResource) Create(ctx context.Context, req resource.CreateReque } if !plan.Capacity.IsNull() { - var d diag.Diagnostics - capacitySpec, d := getCapacityFromModel(ctx, &plan) - resp.Diagnostics.Append(d...) - if resp.Diagnostics.HasError() { - return - } - spec.CapacitySpec = capacitySpec + resp.Diagnostics.AddError("Capacity on namespace creation is not supported", "capacity should be null or not set when creating a namespace") + return + // This will be enabled when capacity on namespace creation is supported + // var d diag.Diagnostics + // capacitySpec, d := getCapacityFromModel(ctx, &plan) + // resp.Diagnostics.Append(d...) + // if resp.Diagnostics.HasError() { + // return + // } + // spec.CapacitySpec = capacitySpec } if !plan.ApiKeyAuth.ValueBool() && plan.AcceptedClientCA.IsNull() { diff --git a/internal/provider/namespace_resource_test.go b/internal/provider/namespace_resource_test.go index 33f9e4b4..4730b89f 100644 --- a/internal/provider/namespace_resource_test.go +++ b/internal/provider/namespace_resource_test.go @@ -794,8 +794,30 @@ PEM func TestAccNamespaceWithCapacity(t *testing.T) { name := fmt.Sprintf("%s-%s", "tf-capacity", randomString(10)) - config := func(name string, retention int, deleteProtection bool, mode string, value string) string { + config := func(name string, retention int, deleteProtection bool, variable string) string { return fmt.Sprintf(` +variable "provisioned" { + type = object({ + mode = string + value = number + }) + default = { + mode = "provisioned" + value = 2 + } +} + +variable "on_demand" { + type = object({ + mode = string + value = number + }) + default = { + mode = "on_demand" + value = 0 + } +} + provider "temporalcloud" { } @@ -823,11 +845,8 @@ PEM namespace_lifecycle = { enable_delete_protection = %t } - capacity = { - mode = "%s" - %s - } -}`, name, retention, deleteProtection, mode, value) + capacity = %s +}`, name, retention, deleteProtection, variable) } resource.ParallelTest(t, resource.TestCase{ @@ -835,20 +854,24 @@ PEM ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { - // New namespace with retention of 7 - Config: config(name, 14, true, "provisioned", `value = 16`), - }, - { - Config: config(name, 14, true, "on_demand", ""), + // New namespace with on demand capacity + Config: config(name, 14, false, "null"), }, + // cannot do provisioned capacity because the test environment doesn't have enough capacity + // { + // Config: config(name, 14, false, "var.on_demand"), // disable delete protection for deletion to succeed + // }, + // { + // Config: config(name, 14, true, "var.provisioned"), + // }, { ImportState: true, ImportStateVerify: true, ResourceName: "temporalcloud_namespace.terraform", }, - { - Config: config(name, 14, false, "on_demand", ""), // disable delete protection for deletion to succeed - }, + // { + // Config: config(name, 14, false, "var.on_demand"), // disable delete protection for deletion to succeed + // }, // Delete testing automatically occurs in TestCase }, }) From d9f4be26f3be5ae36c11002f3044ffb6c2f6fc1a Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Thu, 25 Sep 2025 10:01:13 -0400 Subject: [PATCH 10/31] fix --- internal/client/client.go | 33 +++++++++++++++++++------ internal/provider/namespace_resource.go | 2 +- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/internal/client/client.go b/internal/client/client.go index f31eebb5..bd068e25 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -28,6 +28,7 @@ import ( "fmt" "time" + "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform-plugin-log/tflog" cloudservicev1 "go.temporal.io/cloud-sdk/api/cloudservice/v1" @@ -41,6 +42,8 @@ type Client struct { *cloudclient.Client } +type AwaitPredicate func(ctx context.Context, cloudclient *Client, n *namespace.Namespace) (bool, error) + func NewConnectionWithAPIKey(addrStr string, allowInsecure bool, apiKey string, version string) (*Client, error) { userAgentProject := "terraform-provider-temporalcloud" if version != "" { @@ -114,9 +117,25 @@ func AwaitAsyncOperation(ctx context.Context, cloudclient *Client, op *operation } } -func AwaitNamespaceCapacityOperation(ctx context.Context, cloudclient *Client, n *namespace.Namespace) error { +func AwaitForFulfillment(ctx context.Context, cloudclient *Client, n *namespace.Namespace, predicate ...AwaitPredicate) (bool, error) { + if n == nil { + return false, fmt.Errorf("namespace is required") + } + var bValue bool = true + var errs error + for _, p := range predicate { + b, err := p(ctx, cloudclient, n) + if err != nil { + multierror.Append(errs, err) + } + bValue = bValue && b + } + return bValue, errs +} + +func AwaitNamespaceCapacityOperation(ctx context.Context, cloudclient *Client, n *namespace.Namespace) (bool, error) { if n == nil { - return fmt.Errorf("namespace is required") + return false, fmt.Errorf("namespace is required") } ns := n @@ -130,12 +149,12 @@ func AwaitNamespaceCapacityOperation(ctx context.Context, cloudclient *Client, n Namespace: ns.GetNamespace(), }) if err != nil { - return fmt.Errorf("failed to get namespace: %w", err) + return false, fmt.Errorf("failed to get namespace: %w", err) } ns = getResp.GetNamespace() n.Capacity = ns.GetCapacity() if ns.GetCapacity().GetLatestRequest() == nil { - return nil + return false, nil } state := ns.GetCapacity().GetLatestRequest().GetState() switch state { @@ -148,10 +167,10 @@ func AwaitNamespaceCapacityOperation(ctx context.Context, cloudclient *Client, n continue case namespace.Capacity_Request_STATE_CAPACITY_REQUEST_FAILED: tflog.Debug(ctx, "request failed") - return errors.New("capacity request failed") + return false, errors.New("capacity request failed") case namespace.Capacity_Request_STATE_CAPACITY_REQUEST_COMPLETED: tflog.Debug(ctx, "request completed") - return nil + return true, nil default: tflog.Warn(ctx, "unknown state, continuing", map[string]any{ "state": state, @@ -159,7 +178,7 @@ func AwaitNamespaceCapacityOperation(ctx context.Context, cloudclient *Client, n continue } case <-ctx.Done(): - return ctx.Err() + return false, ctx.Err() } } } diff --git a/internal/provider/namespace_resource.go b/internal/provider/namespace_resource.go index 381b5f71..9efadee8 100644 --- a/internal/provider/namespace_resource.go +++ b/internal/provider/namespace_resource.go @@ -648,7 +648,7 @@ func (r *namespaceResource) Update(ctx context.Context, req resource.UpdateReque return } - if err := client.AwaitNamespaceCapacityOperation(ctx, r.client, ns.Namespace); err != nil { + if _, err := client.AwaitForFulfillment(ctx, r.client, ns.Namespace, client.AwaitNamespaceCapacityOperation); err != nil { resp.Diagnostics.AddError("Failed to update namespace capacity", err.Error()) return } From 63335b3941852372906b0bfca721323d21cadead Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Thu, 25 Sep 2025 10:54:27 -0400 Subject: [PATCH 11/31] errs --- internal/client/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/client/client.go b/internal/client/client.go index bd068e25..df9029a2 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -126,7 +126,7 @@ func AwaitForFulfillment(ctx context.Context, cloudclient *Client, n *namespace. for _, p := range predicate { b, err := p(ctx, cloudclient, n) if err != nil { - multierror.Append(errs, err) + errs = multierror.Append(errs, err) } bValue = bValue && b } From cb1e2c28030d3ffc4d24815f7089f5781407a70c Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Thu, 25 Sep 2025 11:22:07 -0400 Subject: [PATCH 12/31] fix --- internal/provider/namespace_resource_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/provider/namespace_resource_test.go b/internal/provider/namespace_resource_test.go index 4730b89f..d1cba62f 100644 --- a/internal/provider/namespace_resource_test.go +++ b/internal/provider/namespace_resource_test.go @@ -867,7 +867,7 @@ PEM { ImportState: true, ImportStateVerify: true, - ResourceName: "temporalcloud_namespace.terraform", + ResourceName: "temporalcloud_namespace.capacitytest", }, // { // Config: config(name, 14, false, "var.on_demand"), // disable delete protection for deletion to succeed From e3c6dcb26c5fb6b171575eb546eb464ee13511c1 Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Thu, 25 Sep 2025 12:57:29 -0400 Subject: [PATCH 13/31] fix --- .vscode/launch.json | 29 ++++++++++++++++++++ internal/provider/namespace_resource_test.go | 15 ++++------ 2 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..e4224657 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,29 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${fileDirname}" + }, + { + "name": "Test Namespace Capacity", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${fileDirname}", + "args": ["-test.run", "TestAccNamespaceWithCapacity"], + "env": { + "TF_ACC": "1", + "TEMPORAL_CLOUD_ENDPOINT": "saas-api.tmprl.cloud:443", + "TEMPORAL_CLOUD_API_KEY": "eyJhbGciOiJFUzI1NiIsImtpZCI6InBwYWtIQSJ9.eyJhY2NvdW50X2lkIjoidGVtcG9yYWwtZGV2IiwiYXVkIjpbInRlbXBvcmFsLXRlc3QuaW8iXSwiZXhwIjoxNzU4ODg5MTMxLCJpc3MiOiJ0ZW1wb3JhbC10ZXN0LmlvIiwianRpIjoiUzRFUGpQdmVDY0c4ZnRGTXloY2x2WW1RU1JGU041SGoiLCJrZXlfaWQiOiJTNEVQalB2ZUNjRzhmdEZNeWhjbHZZbVFTUkZTTjVIaiIsInN1YiI6ImNhMmY3MGJmZjRmMDQ0YzY5YTExMzAxZDFkMmY4MjdhIn0.aruHWBDUklJEwSyr67l-nAqBZbd7JX3oK18tKJIPEN7EzCcl1Wpxj1tVms5PaQPuim5ndkZl0yfvWkTfYY9rTA" + } + } + ] +} \ No newline at end of file diff --git a/internal/provider/namespace_resource_test.go b/internal/provider/namespace_resource_test.go index d1cba62f..9a8daec8 100644 --- a/internal/provider/namespace_resource_test.go +++ b/internal/provider/namespace_resource_test.go @@ -794,7 +794,7 @@ PEM func TestAccNamespaceWithCapacity(t *testing.T) { name := fmt.Sprintf("%s-%s", "tf-capacity", randomString(10)) - config := func(name string, retention int, deleteProtection bool, variable string) string { + config := func(name string, variable string) string { return fmt.Sprintf(` variable "provisioned" { type = object({ @@ -840,11 +840,6 @@ ThGIAJ5f8VReP9T7155ri5sRCUTBdgFHVAIxAOrtnTo8uRjEs8HdUW0e9H7E2nyW -----END CERTIFICATE----- PEM ) - - retention_days = %d - namespace_lifecycle = { - enable_delete_protection = %t - } capacity = %s }`, name, retention, deleteProtection, variable) } @@ -855,14 +850,14 @@ PEM Steps: []resource.TestStep{ { // New namespace with on demand capacity - Config: config(name, 14, false, "null"), + Config: config(name, "null"), }, // cannot do provisioned capacity because the test environment doesn't have enough capacity // { - // Config: config(name, 14, false, "var.on_demand"), // disable delete protection for deletion to succeed + // Config: config(name, "var.on_demand"), // }, // { - // Config: config(name, 14, true, "var.provisioned"), + // Config: config(name, "var.provisioned"), // }, { ImportState: true, @@ -870,7 +865,7 @@ PEM ResourceName: "temporalcloud_namespace.capacitytest", }, // { - // Config: config(name, 14, false, "var.on_demand"), // disable delete protection for deletion to succeed + // Config: config(name, "var.on_demand"), // }, // Delete testing automatically occurs in TestCase }, From 0fb5751c0b75245ced1ce1535f6903a54763c0b2 Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Thu, 25 Sep 2025 12:57:40 -0400 Subject: [PATCH 14/31] parma --- internal/provider/namespace_resource_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/provider/namespace_resource_test.go b/internal/provider/namespace_resource_test.go index 9a8daec8..7bf45784 100644 --- a/internal/provider/namespace_resource_test.go +++ b/internal/provider/namespace_resource_test.go @@ -841,7 +841,7 @@ ThGIAJ5f8VReP9T7155ri5sRCUTBdgFHVAIxAOrtnTo8uRjEs8HdUW0e9H7E2nyW PEM ) capacity = %s -}`, name, retention, deleteProtection, variable) +}`, name, variable) } resource.ParallelTest(t, resource.TestCase{ From 233a3521d19a8d7e977690066def62dd292d7b96 Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Thu, 25 Sep 2025 16:59:30 -0400 Subject: [PATCH 15/31] retention days --- internal/provider/namespace_resource_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/provider/namespace_resource_test.go b/internal/provider/namespace_resource_test.go index 7bf45784..0cbf0575 100644 --- a/internal/provider/namespace_resource_test.go +++ b/internal/provider/namespace_resource_test.go @@ -840,6 +840,7 @@ ThGIAJ5f8VReP9T7155ri5sRCUTBdgFHVAIxAOrtnTo8uRjEs8HdUW0e9H7E2nyW -----END CERTIFICATE----- PEM ) + retention_days = 7 capacity = %s }`, name, variable) } From adc4fa010501bf22183ec19f7159538bf169ceb4 Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Thu, 25 Sep 2025 17:02:12 -0400 Subject: [PATCH 16/31] fix --- internal/provider/namespace_resource_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/internal/provider/namespace_resource_test.go b/internal/provider/namespace_resource_test.go index 0cbf0575..33255902 100644 --- a/internal/provider/namespace_resource_test.go +++ b/internal/provider/namespace_resource_test.go @@ -872,4 +872,18 @@ PEM }, }) + nameProvisioned := fmt.Sprintf("%s-%s", "tf-capacity-provisioned", randomString(10)) + + // provisioned capacity is not supported on namespace creation + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: config(nameProvisioned, "var.provisioned"), + ExpectError: regexp.MustCompile("Capacity on namespace creation is not supported"), + }, + }, + }) + } From 59dbc1b76f5bd4e943e4fb41bde9bc53605cc816 Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Thu, 25 Sep 2025 19:23:32 -0400 Subject: [PATCH 17/31] test change --- .vscode/launch.json | 29 -------- internal/provider/namespace_resource_test.go | 69 ++++++++++++++++---- 2 files changed, 56 insertions(+), 42 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index e4224657..00000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - - { - "name": "Launch Package", - "type": "go", - "request": "launch", - "mode": "auto", - "program": "${fileDirname}" - }, - { - "name": "Test Namespace Capacity", - "type": "go", - "request": "launch", - "mode": "auto", - "program": "${fileDirname}", - "args": ["-test.run", "TestAccNamespaceWithCapacity"], - "env": { - "TF_ACC": "1", - "TEMPORAL_CLOUD_ENDPOINT": "saas-api.tmprl.cloud:443", - "TEMPORAL_CLOUD_API_KEY": "eyJhbGciOiJFUzI1NiIsImtpZCI6InBwYWtIQSJ9.eyJhY2NvdW50X2lkIjoidGVtcG9yYWwtZGV2IiwiYXVkIjpbInRlbXBvcmFsLXRlc3QuaW8iXSwiZXhwIjoxNzU4ODg5MTMxLCJpc3MiOiJ0ZW1wb3JhbC10ZXN0LmlvIiwianRpIjoiUzRFUGpQdmVDY0c4ZnRGTXloY2x2WW1RU1JGU041SGoiLCJrZXlfaWQiOiJTNEVQalB2ZUNjRzhmdEZNeWhjbHZZbVFTUkZTTjVIaiIsInN1YiI6ImNhMmY3MGJmZjRmMDQ0YzY5YTExMzAxZDFkMmY4MjdhIn0.aruHWBDUklJEwSyr67l-nAqBZbd7JX3oK18tKJIPEN7EzCcl1Wpxj1tVms5PaQPuim5ndkZl0yfvWkTfYY9rTA" - } - } - ] -} \ No newline at end of file diff --git a/internal/provider/namespace_resource_test.go b/internal/provider/namespace_resource_test.go index 33255902..43516ed6 100644 --- a/internal/provider/namespace_resource_test.go +++ b/internal/provider/namespace_resource_test.go @@ -853,37 +853,80 @@ PEM // New namespace with on demand capacity Config: config(name, "null"), }, - // cannot do provisioned capacity because the test environment doesn't have enough capacity - // { - // Config: config(name, "var.on_demand"), - // }, - // { - // Config: config(name, "var.provisioned"), - // }, + { + Config: config(name, "var.provisioned"), + }, { ImportState: true, ImportStateVerify: true, ResourceName: "temporalcloud_namespace.capacitytest", }, - // { - // Config: config(name, "var.on_demand"), - // }, // Delete testing automatically occurs in TestCase }, }) +} - nameProvisioned := fmt.Sprintf("%s-%s", "tf-capacity-provisioned", randomString(10)) +func TestAccCreateNamespaceWithCapacity(t *testing.T) { + name := fmt.Sprintf("%s-%s", "tf-capacity-create", randomString(10)) + config := func(name string, variable string) string { + return fmt.Sprintf(` +variable "provisioned" { + type = object({ + mode = string + value = number + }) + default = { + mode = "provisioned" + value = 2 + } +} +variable "on_demand" { + type = object({ + mode = string + value = number + }) + default = { + mode = "on_demand" + value = 0 + } +} + +provider "temporalcloud" { + +} + +resource "temporalcloud_namespace" "capacitytest" { + name = "%s" + regions = ["aws-us-east-1"] + accepted_client_ca = base64encode(< Date: Thu, 25 Sep 2025 20:21:10 -0400 Subject: [PATCH 18/31] value of 1 --- internal/provider/namespace_resource_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/provider/namespace_resource_test.go b/internal/provider/namespace_resource_test.go index 43516ed6..4a912bcc 100644 --- a/internal/provider/namespace_resource_test.go +++ b/internal/provider/namespace_resource_test.go @@ -877,7 +877,7 @@ variable "provisioned" { }) default = { mode = "provisioned" - value = 2 + value = 1 } } From ed593104674cc29aedd11dc5208029651272a9c5 Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Thu, 25 Sep 2025 20:38:44 -0400 Subject: [PATCH 19/31] value of one --- internal/provider/namespace_resource_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/provider/namespace_resource_test.go b/internal/provider/namespace_resource_test.go index 4a912bcc..a9223f92 100644 --- a/internal/provider/namespace_resource_test.go +++ b/internal/provider/namespace_resource_test.go @@ -803,7 +803,7 @@ variable "provisioned" { }) default = { mode = "provisioned" - value = 2 + value = 1 } } From b75d21a543bb75ae1830ee58a8aedb572b3db1d6 Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Fri, 26 Sep 2025 16:55:10 -0400 Subject: [PATCH 20/31] bump to 4 --- internal/provider/namespace_resource_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/provider/namespace_resource_test.go b/internal/provider/namespace_resource_test.go index a9223f92..febb4597 100644 --- a/internal/provider/namespace_resource_test.go +++ b/internal/provider/namespace_resource_test.go @@ -803,7 +803,7 @@ variable "provisioned" { }) default = { mode = "provisioned" - value = 1 + value = 4 } } From 93f41aafde787aad334763daaa3dadf1878c095d Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Mon, 6 Oct 2025 12:35:40 -0400 Subject: [PATCH 21/31] remove --- internal/provider/namespace_resource.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/internal/provider/namespace_resource.go b/internal/provider/namespace_resource.go index 9efadee8..5957bd6e 100644 --- a/internal/provider/namespace_resource.go +++ b/internal/provider/namespace_resource.go @@ -648,11 +648,6 @@ func (r *namespaceResource) Update(ctx context.Context, req resource.UpdateReque return } - if _, err := client.AwaitForFulfillment(ctx, r.client, ns.Namespace, client.AwaitNamespaceCapacityOperation); err != nil { - resp.Diagnostics.AddError("Failed to update namespace capacity", err.Error()) - return - } - resp.Diagnostics.Append(updateModelFromSpec(ctx, &plan, ns.Namespace)...) if resp.Diagnostics.HasError() { return From 58906062cfe5004886856f19a0d44466cdb832f2 Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Mon, 6 Oct 2025 17:41:23 -0400 Subject: [PATCH 22/31] change --- internal/client/client.go | 70 ------------------------- internal/provider/namespace_resource.go | 7 +-- 2 files changed, 4 insertions(+), 73 deletions(-) diff --git a/internal/client/client.go b/internal/client/client.go index df9029a2..b17090e3 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -28,11 +28,9 @@ import ( "fmt" "time" - "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform-plugin-log/tflog" cloudservicev1 "go.temporal.io/cloud-sdk/api/cloudservice/v1" - "go.temporal.io/cloud-sdk/api/namespace/v1" operationv1 "go.temporal.io/cloud-sdk/api/operation/v1" "go.temporal.io/cloud-sdk/cloudclient" ) @@ -42,8 +40,6 @@ type Client struct { *cloudclient.Client } -type AwaitPredicate func(ctx context.Context, cloudclient *Client, n *namespace.Namespace) (bool, error) - func NewConnectionWithAPIKey(addrStr string, allowInsecure bool, apiKey string, version string) (*Client, error) { userAgentProject := "terraform-provider-temporalcloud" if version != "" { @@ -116,69 +112,3 @@ func AwaitAsyncOperation(ctx context.Context, cloudclient *Client, op *operation } } } - -func AwaitForFulfillment(ctx context.Context, cloudclient *Client, n *namespace.Namespace, predicate ...AwaitPredicate) (bool, error) { - if n == nil { - return false, fmt.Errorf("namespace is required") - } - var bValue bool = true - var errs error - for _, p := range predicate { - b, err := p(ctx, cloudclient, n) - if err != nil { - errs = multierror.Append(errs, err) - } - bValue = bValue && b - } - return bValue, errs -} - -func AwaitNamespaceCapacityOperation(ctx context.Context, cloudclient *Client, n *namespace.Namespace) (bool, error) { - if n == nil { - return false, fmt.Errorf("namespace is required") - } - ns := n - - ctx = tflog.SetField(ctx, "namespace_id", ns.GetNamespace()) - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() - for { - select { - case <-ticker.C: - getResp, err := cloudclient.CloudService().GetNamespace(ctx, &cloudservicev1.GetNamespaceRequest{ - Namespace: ns.GetNamespace(), - }) - if err != nil { - return false, fmt.Errorf("failed to get namespace: %w", err) - } - ns = getResp.GetNamespace() - n.Capacity = ns.GetCapacity() - if ns.GetCapacity().GetLatestRequest() == nil { - return false, nil - } - state := ns.GetCapacity().GetLatestRequest().GetState() - switch state { - case namespace.Capacity_Request_STATE_CAPACITY_REQUEST_UNSPECIFIED: - fallthrough - case namespace.Capacity_Request_STATE_CAPACITY_REQUEST_IN_PROGRESS: - tflog.Debug(ctx, "retrying in 1 second", map[string]any{ - "state": state, - }) - continue - case namespace.Capacity_Request_STATE_CAPACITY_REQUEST_FAILED: - tflog.Debug(ctx, "request failed") - return false, errors.New("capacity request failed") - case namespace.Capacity_Request_STATE_CAPACITY_REQUEST_COMPLETED: - tflog.Debug(ctx, "request completed") - return true, nil - default: - tflog.Warn(ctx, "unknown state, continuing", map[string]any{ - "state": state, - }) - continue - } - case <-ctx.Done(): - return false, ctx.Err() - } - } -} diff --git a/internal/provider/namespace_resource.go b/internal/provider/namespace_resource.go index 5957bd6e..a8aefc63 100644 --- a/internal/provider/namespace_resource.go +++ b/internal/provider/namespace_resource.go @@ -83,7 +83,7 @@ type ( NamespaceLifecycle internaltypes.ZeroObjectValue `tfsdk:"namespace_lifecycle"` ConnectivityRuleIds internaltypes.UnorderedStringListValue `tfsdk:"connectivity_rule_ids"` Timeouts timeouts.Value `tfsdk:"timeouts"` - Capacity types.Object `tfsdk:"capacity"` + Capacity internaltypes.ZeroObjectValue `tfsdk:"capacity"` } lifecycleModel struct { @@ -884,17 +884,18 @@ func updateModelFromSpec( capacityMode = types.StringValue("provisioned") capacityValue = types.Float64Value(capacitySpec.GetProvisioned().GetValue()) } - capacity, objectDiags := types.ObjectValueFrom(ctx, capacityAttrs, &capacityModel{ + cp, objectDiags := types.ObjectValueFrom(ctx, capacityAttrs, &capacityModel{ Mode: capacityMode, Value: capacityValue, }) + capacity := internaltypes.ZeroObjectValue{ObjectValue: cp} diags.Append(objectDiags...) if diags.HasError() { return diags } state.Capacity = capacity } else { - state.Capacity = types.ObjectNull(capacityAttrs) + state.Capacity = internaltypes.ZeroObjectValue{ObjectValue: types.ObjectNull(capacityAttrs)} } state.ConnectivityRuleIds = connectivityRuleIdsState From 782c1a8b89dab65ef00fc022a8a7bb2302a4949c Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Mon, 6 Oct 2025 17:43:22 -0400 Subject: [PATCH 23/31] change --- internal/provider/namespace_resource_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/provider/namespace_resource_test.go b/internal/provider/namespace_resource_test.go index febb4597..4d5b47db 100644 --- a/internal/provider/namespace_resource_test.go +++ b/internal/provider/namespace_resource_test.go @@ -11,6 +11,7 @@ import ( "strings" "testing" "text/template" + "time" fwresource "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -856,6 +857,12 @@ PEM { Config: config(name, "var.provisioned"), }, + { + PreConfig: func() { + time.Sleep(time.Minute * 1) // wait for previous update to finish + }, + Config: config(name, "var.on_demand"), + }, { ImportState: true, ImportStateVerify: true, From 3ef7489cbf5b06468e61526a25e2caf1e290a6d8 Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Tue, 7 Oct 2025 15:37:43 -0400 Subject: [PATCH 24/31] acctest --- internal/provider/namespace_resource.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/provider/namespace_resource.go b/internal/provider/namespace_resource.go index a8aefc63..b5f511c5 100644 --- a/internal/provider/namespace_resource.go +++ b/internal/provider/namespace_resource.go @@ -318,6 +318,11 @@ func (r *namespaceResource) Schema(ctx context.Context, _ resource.SchemaRequest "capacity": schema.SingleNestedAttribute{ Optional: true, Description: "The capacity configuration for the namespace.", + CustomType: internaltypes.ZeroObjectType{ + ObjectType: basetypes.ObjectType{ + AttrTypes: capacityAttrs, + }, + }, Attributes: map[string]schema.Attribute{ "mode": schema.StringAttribute{ Description: "The mode of the capacity configuration. Must be one of 'provisioned' or 'on_demand'.", From 759cfed17162018f2ef0de968c6780ce810a95bf Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Tue, 7 Oct 2025 16:17:19 -0400 Subject: [PATCH 25/31] test --- internal/provider/namespace_resource_test.go | 77 ++++++++++++++++++-- 1 file changed, 72 insertions(+), 5 deletions(-) diff --git a/internal/provider/namespace_resource_test.go b/internal/provider/namespace_resource_test.go index 4d5b47db..fd79d8b2 100644 --- a/internal/provider/namespace_resource_test.go +++ b/internal/provider/namespace_resource_test.go @@ -11,7 +11,6 @@ import ( "strings" "testing" "text/template" - "time" fwresource "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -793,7 +792,7 @@ PEM }) } -func TestAccNamespaceWithCapacity(t *testing.T) { +func TestAccNamespaceWithProvisionedCapacity(t *testing.T) { name := fmt.Sprintf("%s-%s", "tf-capacity", randomString(10)) config := func(name string, variable string) string { return fmt.Sprintf(` @@ -858,9 +857,77 @@ PEM Config: config(name, "var.provisioned"), }, { - PreConfig: func() { - time.Sleep(time.Minute * 1) // wait for previous update to finish - }, + ImportState: true, + ImportStateVerify: true, + ResourceName: "temporalcloud_namespace.capacitytest", + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccNamespaceWithOnDemandCapacity(t *testing.T) { + name := fmt.Sprintf("%s-%s", "tf-capacity", randomString(10)) + config := func(name string, variable string) string { + return fmt.Sprintf(` +variable "provisioned" { + type = object({ + mode = string + value = number + }) + default = { + mode = "provisioned" + value = 4 + } +} + +variable "on_demand" { + type = object({ + mode = string + value = number + }) + default = { + mode = "on_demand" + value = 0 + } +} + +provider "temporalcloud" { + +} + +resource "temporalcloud_namespace" "capacitytest" { + name = "%s" + regions = ["aws-us-east-1"] + accepted_client_ca = base64encode(< Date: Tue, 7 Oct 2025 16:39:02 -0400 Subject: [PATCH 26/31] fix --- internal/provider/namespace_resource_test.go | 200 ++++++------------- 1 file changed, 63 insertions(+), 137 deletions(-) diff --git a/internal/provider/namespace_resource_test.go b/internal/provider/namespace_resource_test.go index fd79d8b2..713c3eeb 100644 --- a/internal/provider/namespace_resource_test.go +++ b/internal/provider/namespace_resource_test.go @@ -11,10 +11,12 @@ import ( "strings" "testing" "text/template" + "time" fwresource "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" + "go.temporal.io/cloud-sdk/api/namespace/v1" cloudservicev1 "go.temporal.io/cloud-sdk/api/cloudservice/v1" @@ -792,7 +794,7 @@ PEM }) } -func TestAccNamespaceWithProvisionedCapacity(t *testing.T) { +func TestAccNamespaceWithCapacity(t *testing.T) { name := fmt.Sprintf("%s-%s", "tf-capacity", randomString(10)) config := func(name string, variable string) string { return fmt.Sprintf(` @@ -855,80 +857,69 @@ PEM }, { Config: config(name, "var.provisioned"), - }, - { - ImportState: true, - ImportStateVerify: true, - ResourceName: "temporalcloud_namespace.capacitytest", - }, - // Delete testing automatically occurs in TestCase - }, - }) -} - -func TestAccNamespaceWithOnDemandCapacity(t *testing.T) { - name := fmt.Sprintf("%s-%s", "tf-capacity", randomString(10)) - config := func(name string, variable string) string { - return fmt.Sprintf(` -variable "provisioned" { - type = object({ - mode = string - value = number - }) - default = { - mode = "provisioned" - value = 4 - } -} - -variable "on_demand" { - type = object({ - mode = string - value = number - }) - default = { - mode = "on_demand" - value = 0 - } -} - -provider "temporalcloud" { - -} - -resource "temporalcloud_namespace" "capacitytest" { - name = "%s" - regions = ["aws-us-east-1"] - accepted_client_ca = base64encode(< Date: Tue, 7 Oct 2025 16:46:09 -0400 Subject: [PATCH 27/31] float --- internal/provider/namespace_resource_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/provider/namespace_resource_test.go b/internal/provider/namespace_resource_test.go index 713c3eeb..d7475776 100644 --- a/internal/provider/namespace_resource_test.go +++ b/internal/provider/namespace_resource_test.go @@ -880,7 +880,7 @@ PEM } else { value := ns.GetNamespace().GetCapacity().GetProvisioned().GetCurrentValue() if value != 4.0 { - return fmt.Errorf("expected provisioned capacity of 4, got %d", value) + return fmt.Errorf("expected provisioned capacity of 4, got %f", value) } // success return nil From d7785d4f0a4c3d7c07b16eca3ade4b763150d876 Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Tue, 7 Oct 2025 19:51:40 -0400 Subject: [PATCH 28/31] retry --- internal/provider/namespace_resource.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/internal/provider/namespace_resource.go b/internal/provider/namespace_resource.go index b5f511c5..b4b7b7da 100644 --- a/internal/provider/namespace_resource.go +++ b/internal/provider/namespace_resource.go @@ -880,11 +880,19 @@ func updateModelFromSpec( } capacitySpec := ns.GetSpec().GetCapacitySpec() - var capacityMode types.String - var capacityValue types.Float64 if capacitySpec != nil { + var capacityMode types.String + var capacityValue types.Float64 if capacitySpec.GetOnDemand() != nil { capacityMode = types.StringValue("on_demand") + // For on_demand mode, preserve the value from the current state if it exists + if !state.Capacity.IsNull() { + var currentCapacity capacityModel + diags.Append(state.Capacity.As(ctx, ¤tCapacity, basetypes.ObjectAsOptions{})...) + if !diags.HasError() { + capacityValue = currentCapacity.Value + } + } } else if capacitySpec.GetProvisioned() != nil { capacityMode = types.StringValue("provisioned") capacityValue = types.Float64Value(capacitySpec.GetProvisioned().GetValue()) From efdcb7c3c4ef6d601f99022c3b9c79e215a220b2 Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Tue, 7 Oct 2025 21:12:33 -0400 Subject: [PATCH 29/31] testfix --- internal/provider/namespace_resource.go | 5 +++-- internal/provider/namespace_resource_test.go | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/internal/provider/namespace_resource.go b/internal/provider/namespace_resource.go index b4b7b7da..c9ca301d 100644 --- a/internal/provider/namespace_resource.go +++ b/internal/provider/namespace_resource.go @@ -885,11 +885,12 @@ func updateModelFromSpec( var capacityValue types.Float64 if capacitySpec.GetOnDemand() != nil { capacityMode = types.StringValue("on_demand") - // For on_demand mode, preserve the value from the current state if it exists + // For on_demand mode, set value to 0 if it's in the current state, otherwise leave it null if !state.Capacity.IsNull() { var currentCapacity capacityModel diags.Append(state.Capacity.As(ctx, ¤tCapacity, basetypes.ObjectAsOptions{})...) - if !diags.HasError() { + if !diags.HasError() && !currentCapacity.Value.IsNull() { + // Preserve the value from state if it exists capacityValue = currentCapacity.Value } } diff --git a/internal/provider/namespace_resource_test.go b/internal/provider/namespace_resource_test.go index d7475776..4c23b1e5 100644 --- a/internal/provider/namespace_resource_test.go +++ b/internal/provider/namespace_resource_test.go @@ -922,9 +922,10 @@ PEM }, }, { - ImportState: true, - ImportStateVerify: true, - ResourceName: "temporalcloud_namespace.capacitytest", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"capacity.value"}, + ResourceName: "temporalcloud_namespace.capacitytest", }, // Delete testing automatically occurs in TestCase }, From ee5227bab22044b51f612f93a27b55c85417018b Mon Sep 17 00:00:00 2001 From: pauloh-temporal Date: Wed, 8 Oct 2025 12:49:39 -0400 Subject: [PATCH 30/31] Update internal/provider/namespace_resource_test.go Co-authored-by: Abhinav Nekkanti <10552725+anekkanti@users.noreply.github.com> --- internal/provider/namespace_resource_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/provider/namespace_resource_test.go b/internal/provider/namespace_resource_test.go index 4c23b1e5..ff3b1822 100644 --- a/internal/provider/namespace_resource_test.go +++ b/internal/provider/namespace_resource_test.go @@ -921,6 +921,10 @@ PEM return fmt.Errorf("timed out waiting for capacity change") }, }, + { + // Revert namespace back to defaults + Config: config(name, "null"), + }, { ImportState: true, ImportStateVerify: true, From 61cc19dfde83ead4d9851a6284579768bbc3130d Mon Sep 17 00:00:00 2001 From: Paul W Oh Date: Wed, 8 Oct 2025 12:53:13 -0400 Subject: [PATCH 31/31] gofmt --- internal/provider/namespace_resource_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/provider/namespace_resource_test.go b/internal/provider/namespace_resource_test.go index ff3b1822..2465a522 100644 --- a/internal/provider/namespace_resource_test.go +++ b/internal/provider/namespace_resource_test.go @@ -922,7 +922,7 @@ PEM }, }, { - // Revert namespace back to defaults + // Revert namespace back to defaults Config: config(name, "null"), }, {