Skip to content
Open
Show file tree
Hide file tree
Changes from 22 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
70 changes: 70 additions & 0 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ 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"
)
Expand All @@ -40,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 != "" {
Expand Down Expand Up @@ -112,3 +116,69 @@ 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()
}
}
}
103 changes: 103 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 types.Object `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,20 @@ func (r *namespaceResource) Schema(ctx context.Context, _ resource.SchemaRequest
listvalidator.SizeAtLeast(1),
},
},
"capacity": schema.SingleNestedAttribute{
Optional: true,
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{
Expand Down Expand Up @@ -376,6 +401,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 +609,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 +872,31 @@ 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
} else {
state.Capacity = types.ObjectNull(capacityAttrs)
}

state.ConnectivityRuleIds = connectivityRuleIdsState
state.Endpoints = endpointsState
state.Regions = planRegionsUnordered
Expand Down Expand Up @@ -893,6 +964,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
Loading
Loading