Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
230dc16
Adding Namespace Capacity Terraform Resource
pauloh-temporal Sep 24, 2025
bc58deb
fix
pauloh-temporal Sep 24, 2025
987e35f
optional
pauloh-temporal Sep 24, 2025
89042e0
go mod tidy + tests
pauloh-temporal Sep 24, 2025
66305eb
fix
pauloh-temporal Sep 24, 2025
4f7e44c
acceptance test
pauloh-temporal Sep 24, 2025
b5168f9
true
pauloh-temporal Sep 24, 2025
1ea78bc
testfix
pauloh-temporal Sep 24, 2025
a8e8694
Acceptance tests
pauloh-temporal Sep 25, 2025
d9f4be2
fix
pauloh-temporal Sep 25, 2025
4ff79ee
Merge branch 'main' into namespace-capacity
pauloh-temporal Sep 25, 2025
63335b3
errs
pauloh-temporal Sep 25, 2025
cb1e2c2
fix
pauloh-temporal Sep 25, 2025
e3c6dcb
fix
pauloh-temporal Sep 25, 2025
0fb5751
parma
pauloh-temporal Sep 25, 2025
233a352
retention days
pauloh-temporal Sep 25, 2025
adc4fa0
fix
pauloh-temporal Sep 25, 2025
59dbc1b
test change
pauloh-temporal Sep 25, 2025
0ce7f35
value of 1
pauloh-temporal Sep 26, 2025
ed59310
value of one
pauloh-temporal Sep 26, 2025
b75d21a
bump to 4
pauloh-temporal Sep 26, 2025
93f41aa
remove
pauloh-temporal Oct 6, 2025
5890606
change
pauloh-temporal Oct 6, 2025
782c1a8
change
pauloh-temporal Oct 6, 2025
3ef7489
acctest
pauloh-temporal Oct 7, 2025
759cfed
test
pauloh-temporal Oct 7, 2025
568b6cc
fix
pauloh-temporal Oct 7, 2025
1eba4bf
float
pauloh-temporal Oct 7, 2025
d7785d4
retry
pauloh-temporal Oct 7, 2025
efdcb7c
testfix
pauloh-temporal Oct 8, 2025
ee5227b
Update internal/provider/namespace_resource_test.go
pauloh-temporal Oct 8, 2025
61cc19d
gofmt
pauloh-temporal Oct 8, 2025
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
10 changes: 10 additions & 0 deletions docs/resources/namespace.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.

<a id="nestedatt--capacity"></a>
### 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'.


<a id="nestedatt--certificate_filters"></a>
### Nested Schema for `certificate_filters`

Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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.53.0
go.temporal.io/cloud-sdk v0.5.0
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these would need to be reconsumed after the proper API bump in Cloud SDK

go.temporal.io/cloud-sdk v0.5.1-0.20250924000029-4237f0d769ff
go.temporal.io/sdk v1.36.0
google.golang.org/grpc v1.75.1
google.golang.org/protobuf v1.36.9
Expand All @@ -34,6 +34,7 @@ require (
github.com/bgentry/speakeasy v0.1.0 // indirect
github.com/bmatcuk/doublestar/v4 v4.9.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
Expand Down Expand Up @@ -71,6 +72,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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,8 @@ 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.53.0 h1:6vAFpXaC584AIELa6pONV56MTpkm4Ha7gPWL2acNAjo=
go.temporal.io/api v1.53.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.36.0 h1:WO9zetpybBNK7xsQth4Z+3Zzw1zSaM9MOUGrnnUjZMo=
go.temporal.io/sdk v1.36.0/go.mod h1:8BxGRF0LcQlfQrLLGkgVajbsKUp/PY7280XTdcKc18Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
Expand Down
118 changes: 118 additions & 0 deletions internal/provider/namespace_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ type (
NamespaceLifecycle internaltypes.ZeroObjectValue `tfsdk:"namespace_lifecycle"`
ConnectivityRuleIds internaltypes.UnorderedStringListValue `tfsdk:"connectivity_rule_ids"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
Capacity internaltypes.ZeroObjectValue `tfsdk:"capacity"`
}

lifecycleModel struct {
Expand All @@ -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 (
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -304,6 +315,25 @@ func (r *namespaceResource) Schema(ctx context.Context, _ resource.SchemaRequest
listvalidator.SizeAtLeast(1),
},
},
"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'.",
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{
Expand Down Expand Up @@ -376,6 +406,19 @@ func (r *namespaceResource) Create(ctx context.Context, req resource.CreateReque
ConnectivityRuleIds: connectivityRuleIds,
}

if !plan.Capacity.IsNull() {
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() {
resp.Diagnostics.AddError("Namespace not configured with authentication", "accepted_client_ca or api_key_auth must be set")
return
Expand Down Expand Up @@ -571,6 +614,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
Expand Down Expand Up @@ -824,8 +877,41 @@ func updateModelFromSpec(
connectivityRuleIdsState = internaltypes.UnorderedStringListValue{
ListValue: planConnectivityRuleIds,
}
}

capacitySpec := ns.GetSpec().GetCapacitySpec()
if capacitySpec != nil {
var capacityMode types.String
var capacityValue types.Float64
if capacitySpec.GetOnDemand() != nil {
capacityMode = types.StringValue("on_demand")
// 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, &currentCapacity, basetypes.ObjectAsOptions{})...)
if !diags.HasError() && !currentCapacity.Value.IsNull() {
// Preserve the value from state if it exists
capacityValue = currentCapacity.Value
}
}
} else if capacitySpec.GetProvisioned() != nil {
capacityMode = types.StringValue("provisioned")
capacityValue = types.Float64Value(capacitySpec.GetProvisioned().GetValue())
}
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 = internaltypes.ZeroObjectValue{ObjectValue: types.ObjectNull(capacityAttrs)}
}

state.ConnectivityRuleIds = connectivityRuleIdsState
state.Endpoints = endpointsState
state.Regions = planRegionsUnordered
Expand Down Expand Up @@ -893,6 +979,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()
Expand Down
144 changes: 144 additions & 0 deletions internal/provider/namespace_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -791,3 +793,145 @@ PEM
},
})
}

func TestAccNamespaceWithCapacity(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(<<PEM
-----BEGIN CERTIFICATE-----
MIIByDCCAU2gAwIBAgIRAuOeFDeADUx5O53PRIsIPZIwCgYIKoZIzj0EAwMwEjEQ
MA4GA1UEChMHdGVzdGluZzAeFw0yNTA4MjAxNDAwMzNaFw0yNjA4MjAxNDAxMzNa
MBIxEDAOBgNVBAoTB3Rlc3RpbmcwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATRWwv2
nVfToOR59QuRHk5jAVhu991AQWXwLFSzHzjmZ8XIkiVzh3EhPwybsnm+uV6XN/xe
1+KJ/0NyiVL91KFwS0y5xLKqdvy/mOv0eSUy/blJpLR66diTqPDMlYntuBmjZzBl
MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTNvOjx
e/IC/jxLZvXGQT4fmj0eMTAjBgNVHREEHDAaghhjbGllbnQucm9vdC50ZXN0aW5n
LjJ5cU4wCgYIKoZIzj0EAwMDaQAwZgIxALwxPDblJQ9R65G9/M7Tyx1H/7EUTeo9
ThGIAJ5f8VReP9T7155ri5sRCUTBdgFHVAIxAOrtnTo8uRjEs8HdUW0e9H7E2nyW
5hWHcfGvGFFkZn3TkJIX3kdJslSDmxOXhn7D/w==
-----END CERTIFICATE-----
PEM
)
retention_days = 7
capacity = %s
}`, name, variable)
}

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
// New namespace with on demand capacity
Config: config(name, "null"),
},
{
Config: config(name, "var.provisioned"),
Check: func(state *terraform.State) error {
id := state.RootModule().Resources["temporalcloud_namespace.capacitytest"].Primary.Attributes["id"]
conn := newConnection(t)
for i := 0; i < 60; i++ {
ns, err := conn.GetNamespace(context.Background(), &cloudservicev1.GetNamespaceRequest{
Namespace: id,
})
time.Sleep(1 * time.Second)
if err != nil {
return fmt.Errorf("failed to get namespace: %v", err)
}
if ns.GetNamespace().GetCapacity().GetLatestRequest().GetState() == namespace.Capacity_Request_STATE_CAPACITY_REQUEST_IN_PROGRESS {
continue
}
if ns.GetNamespace().GetCapacity().GetLatestRequest().GetState() == namespace.Capacity_Request_STATE_CAPACITY_REQUEST_FAILED {
return fmt.Errorf("capacity request failed: %v", ns.GetNamespace().GetCapacity().GetLatestRequest())
}
if ns.GetNamespace().GetCapacity().GetLatestRequest().GetState() == namespace.Capacity_Request_STATE_CAPACITY_REQUEST_COMPLETED {
if ns.GetNamespace().GetCapacity().GetProvisioned() == nil {
return fmt.Errorf("expected provisioned capacity, got nil")
} else {
value := ns.GetNamespace().GetCapacity().GetProvisioned().GetCurrentValue()
if value != 4.0 {
return fmt.Errorf("expected provisioned capacity of 4, got %f", value)
}
// success
return nil
}
}
}
return fmt.Errorf("timed out waiting for capacity change")
},
},
{
Config: config(name, "var.on_demand"),
Check: func(state *terraform.State) error {
id := state.RootModule().Resources["temporalcloud_namespace.capacitytest"].Primary.Attributes["id"]
conn := newConnection(t)
for i := 0; i < 60; i++ {
ns, err := conn.GetNamespace(context.Background(), &cloudservicev1.GetNamespaceRequest{
Namespace: id,
})
time.Sleep(1 * time.Second)
if err != nil {
return fmt.Errorf("failed to get namespace: %v", err)
}
if ns.GetNamespace().GetCapacity().GetLatestRequest().GetState() == namespace.Capacity_Request_STATE_CAPACITY_REQUEST_IN_PROGRESS {
continue
}
if ns.GetNamespace().GetCapacity().GetLatestRequest().GetState() == namespace.Capacity_Request_STATE_CAPACITY_REQUEST_FAILED {
return fmt.Errorf("capacity request failed: %v", ns.GetNamespace().GetCapacity().GetLatestRequest())
}
if ns.GetNamespace().GetCapacity().GetLatestRequest().GetState() == namespace.Capacity_Request_STATE_CAPACITY_REQUEST_COMPLETED {
if ns.GetNamespace().GetCapacity().GetOnDemand() == nil {
return fmt.Errorf("expected on_demand capacity, got nil")
} else {
// success
return nil
}
}
}
return fmt.Errorf("timed out waiting for capacity change")
},
},
{
// Revert namespace back to defaults
Config: config(name, "null"),
},
{
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"capacity.value"},
ResourceName: "temporalcloud_namespace.capacitytest",
},
// Delete testing automatically occurs in TestCase
},
})
}
Loading