diff --git a/docs/resources/nexus_endpoint.md b/docs/resources/nexus_endpoint.md new file mode 100644 index 0000000..e1bea0c --- /dev/null +++ b/docs/resources/nexus_endpoint.md @@ -0,0 +1,126 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "temporalcloud_nexus_endpoint Resource - terraform-provider-temporalcloud" +subcategory: "" +description: |- + Provisions a Temporal Cloud Nexus endpoint. +--- + +# temporalcloud_nexus_endpoint (Resource) + +Provisions a Temporal Cloud Nexus endpoint. + +## Example Usage + +```terraform +terraform { + required_providers { + temporalcloud = { + source = "temporalio/temporalcloud" + } + } +} + +resource "temporalcloud_namespace" "target_namespace" { + name = "terraform-target-namespace" + regions = ["aws-us-west-2"] + api_key_auth = true + retention_days = 14 + timeouts { + create = "10m" + delete = "10m" + } +} + +resource "temporalcloud_namespace" "caller_namespace" { + name = "terraform-caller-namespace" + regions = ["aws-us-east-1"] + api_key_auth = true + retention_days = 14 + timeouts { + create = "10m" + delete = "10m" + } +} + +resource "temporalcloud_namespace" "caller_namespace_2" { + name = "terraform-caller-namespace-2" + regions = ["gcp-us-central1"] + api_key_auth = true + retention_days = 14 + timeouts { + create = "10m" + delete = "10m" + } +} + +resource "temporalcloud_nexus_endpoint" "nexus_endpoint" { + name = "terraform-nexus-endpoint" + description = <<-EOT + Service Name: + my-hello-service + Operation Names: + echo + say-hello + + Input / Output arguments are in the following repository: + https://github.com/temporalio/samples-go/blob/main/nexus/service/api.go + EOT + worker_target_spec = { + namespace_id = temporalcloud_namespace.target_namespace.id + task_queue = "terraform-task-queue" + } + allowed_caller_namespaces = [ + temporalcloud_namespace.caller_namespace.id, + temporalcloud_namespace.caller_namespace_2.id, + ] +} +``` + + +## Schema + +### Required + +- `allowed_caller_namespaces` (Set of String) Namespace(s) that are allowed to call this Endpoint. +- `name` (String) The name of the endpoint. Must be unique within an account and match `^[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9]$` +- `worker_target_spec` (Attributes) A target spec for routing nexus requests to a specific cloud namespace worker. (see [below for nested schema](#nestedatt--worker_target_spec)) + +### Optional + +- `description` (String, Sensitive) The description for the Nexus endpoint. +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `id` (String) The unique identifier of the Nexus endpoint. + + +### Nested Schema for `worker_target_spec` + +Required: + +- `namespace_id` (String) The target cloud namespace to route requests to. Namespace must be in same account as the endpoint. +- `task_queue` (String) The task queue on the cloud namespace to route requests to. + + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). +- `delete` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Setting a timeout for a Delete operation is only applicable if changes are saved into state before the destroy operation occurs. + +## Import + +Import is supported using the following syntax: + +```shell +# Nexus Endpoints can be imported to incorporate existing Nexus Endpoints into your Terraform pipeline. +# To import a Nexus Endpoint, you need +# - a resource configuration in your Terraform configuration file/module to accept the imported Nexus Endpoint. In the example below, the placeholder is "temporalcloud_nexus_endpoint" "nexus_endpoint" +# - the Nexus Endpoint's ID, which is found using the Temporal Cloud CLI tcld nexus endpoint list. In the example below, this is 405f7da4224a43d99c211904ed9b3819 + +terraform import temporalcloud_nexus_endpoint.nexus_endpoint 405f7da4224a43d99c211904ed9b3819 +``` diff --git a/examples/resources/temporalcloud_nexus_endpoint/import.sh b/examples/resources/temporalcloud_nexus_endpoint/import.sh new file mode 100644 index 0000000..1b2b73c --- /dev/null +++ b/examples/resources/temporalcloud_nexus_endpoint/import.sh @@ -0,0 +1,6 @@ +# Nexus Endpoints can be imported to incorporate existing Nexus Endpoints into your Terraform pipeline. +# To import a Nexus Endpoint, you need +# - a resource configuration in your Terraform configuration file/module to accept the imported Nexus Endpoint. In the example below, the placeholder is "temporalcloud_nexus_endpoint" "nexus_endpoint" +# - the Nexus Endpoint's ID, which is found using the Temporal Cloud CLI tcld nexus endpoint list. In the example below, this is 405f7da4224a43d99c211904ed9b3819 + +terraform import temporalcloud_nexus_endpoint.nexus_endpoint 405f7da4224a43d99c211904ed9b3819 \ No newline at end of file diff --git a/examples/resources/temporalcloud_nexus_endpoint/resource.tf b/examples/resources/temporalcloud_nexus_endpoint/resource.tf new file mode 100644 index 0000000..fcfb73a --- /dev/null +++ b/examples/resources/temporalcloud_nexus_endpoint/resource.tf @@ -0,0 +1,62 @@ +terraform { + required_providers { + temporalcloud = { + source = "temporalio/temporalcloud" + } + } +} + +resource "temporalcloud_namespace" "target_namespace" { + name = "terraform-target-namespace" + regions = ["aws-us-west-2"] + api_key_auth = true + retention_days = 14 + timeouts { + create = "10m" + delete = "10m" + } +} + +resource "temporalcloud_namespace" "caller_namespace" { + name = "terraform-caller-namespace" + regions = ["aws-us-east-1"] + api_key_auth = true + retention_days = 14 + timeouts { + create = "10m" + delete = "10m" + } +} + +resource "temporalcloud_namespace" "caller_namespace_2" { + name = "terraform-caller-namespace-2" + regions = ["gcp-us-central1"] + api_key_auth = true + retention_days = 14 + timeouts { + create = "10m" + delete = "10m" + } +} + +resource "temporalcloud_nexus_endpoint" "nexus_endpoint" { + name = "terraform-nexus-endpoint" + description = <<-EOT + Service Name: + my-hello-service + Operation Names: + echo + say-hello + + Input / Output arguments are in the following repository: + https://github.com/temporalio/samples-go/blob/main/nexus/service/api.go + EOT + worker_target_spec = { + namespace_id = temporalcloud_namespace.target_namespace.id + task_queue = "terraform-task-queue" + } + allowed_caller_namespaces = [ + temporalcloud_namespace.caller_namespace.id, + temporalcloud_namespace.caller_namespace_2.id, + ] +} diff --git a/internal/provider/nexus_endpoint_resource.go b/internal/provider/nexus_endpoint_resource.go new file mode 100644 index 0000000..0309ad3 --- /dev/null +++ b/internal/provider/nexus_endpoint_resource.go @@ -0,0 +1,403 @@ +package provider + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/temporalio/terraform-provider-temporalcloud/internal/client" + cloudservicev1 "go.temporal.io/api/cloud/cloudservice/v1" + nexusv1 "go.temporal.io/api/cloud/nexus/v1" +) + +type ( + nexusEndpointResource struct { + client *client.Client + } + + nexusEndpointResourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + WorkerTargetSpec types.Object `tfsdk:"worker_target_spec"` + AllowedCallerNamespaces types.Set `tfsdk:"allowed_caller_namespaces"` + + Timeouts timeouts.Value `tfsdk:"timeouts"` + } + + nexusEndpointWorkerTargetModel struct { + NamespaceID types.String `tfsdk:"namespace_id"` + TaskQueue types.String `tfsdk:"task_queue"` + } +) + +var ( + _ resource.Resource = (*nexusEndpointResource)(nil) + _ resource.ResourceWithConfigure = (*nexusEndpointResource)(nil) + _ resource.ResourceWithImportState = (*nexusEndpointResource)(nil) + + workerTargetSpecAttrs = map[string]attr.Type{ + "namespace_id": types.StringType, + "task_queue": types.StringType, + } +) + +func NewNexusEndpointResource() resource.Resource { + return &nexusEndpointResource{} +} + +func (r *nexusEndpointResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*client.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = client +} + +func (r *nexusEndpointResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_nexus_endpoint" +} + +func (r *nexusEndpointResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Provisions a Temporal Cloud Nexus endpoint.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The unique identifier of the Nexus endpoint.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + Description: "The name of the endpoint. Must be unique within an account and match `^[a-zA-Z][a-zA-Z0-9\\-]*[a-zA-Z0-9]$`", + Required: true, + }, + "description": schema.StringAttribute{ + Description: "The description for the Nexus endpoint.", + Optional: true, + Sensitive: true, + }, + "worker_target_spec": schema.SingleNestedAttribute{ + Description: "A target spec for routing nexus requests to a specific cloud namespace worker.", + Attributes: map[string]schema.Attribute{ + "namespace_id": schema.StringAttribute{ + Description: "The target cloud namespace to route requests to. Namespace must be in same account as the endpoint.", + Required: true, + }, + "task_queue": schema.StringAttribute{ + Description: "The task queue on the cloud namespace to route requests to.", + Required: true, + }, + }, + Required: true, + }, + "allowed_caller_namespaces": schema.SetAttribute{ + Description: "Namespace(s) that are allowed to call this Endpoint.", + ElementType: types.StringType, + Required: true, + }, + }, + Blocks: map[string]schema.Block{ + "timeouts": timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Delete: true, + }), + }, + } +} + +func (r *nexusEndpointResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan nexusEndpointResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + createTimeout, diags := plan.Timeouts.Create(ctx, defaultCreateTimeout) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx, cancel := context.WithTimeout(ctx, createTimeout) + defer cancel() + + description := "" + if !plan.Description.IsNull() { + description = plan.Description.ValueString() + } + + targetSpec, diags := getTargetSpecFromModel(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + policySpecs, diags := getPolicySpecsFromModel(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + svcResp, err := r.client.CloudService().CreateNexusEndpoint(ctx, &cloudservicev1.CreateNexusEndpointRequest{ + Spec: &nexusv1.EndpointSpec{ + Name: plan.Name.ValueString(), + Description: description, + TargetSpec: targetSpec, + PolicySpecs: policySpecs, + }, + }) + + if err != nil { + resp.Diagnostics.AddError("Failed to create Nexus endpoint", err.Error()) + return + } + if err := client.AwaitAsyncOperation(ctx, r.client, svcResp.AsyncOperation); err != nil { + resp.Diagnostics.AddError("Failed to create Nexus endpoint", err.Error()) + return + } + + nexusEndpoint, err := r.client.CloudService().GetNexusEndpoint(ctx, &cloudservicev1.GetNexusEndpointRequest{ + EndpointId: svcResp.GetEndpointId(), + }) + if err != nil { + resp.Diagnostics.AddError("Failed to get Nexus endpoint after creation", err.Error()) + return + } + + resp.Diagnostics.Append(updateNexusEndpointModelFromSpec(ctx, &plan, nexusEndpoint.Endpoint)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *nexusEndpointResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state nexusEndpointResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + nexusEndpoint, err := r.client.CloudService().GetNexusEndpoint(ctx, &cloudservicev1.GetNexusEndpointRequest{ + EndpointId: state.ID.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError("Failed to get Nexus endpoint", err.Error()) + return + } + + resp.Diagnostics.Append(updateNexusEndpointModelFromSpec(ctx, &state, nexusEndpoint.Endpoint)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +func (r *nexusEndpointResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan nexusEndpointResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + nexusEndpoint, err := r.client.CloudService().GetNexusEndpoint(ctx, &cloudservicev1.GetNexusEndpointRequest{ + EndpointId: plan.ID.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError("Failed to get current Nexus endpoint status", err.Error()) + return + } + + description := "" + if !plan.Description.IsNull() { + description = plan.Description.ValueString() + } + + targetSpec, diags := getTargetSpecFromModel(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + policySpecs, diags := getPolicySpecsFromModel(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + svcResp, err := r.client.CloudService().UpdateNexusEndpoint(ctx, &cloudservicev1.UpdateNexusEndpointRequest{ + EndpointId: plan.ID.ValueString(), + Spec: &nexusv1.EndpointSpec{ + Name: plan.Name.ValueString(), + Description: description, + TargetSpec: targetSpec, + PolicySpecs: policySpecs, + }, + ResourceVersion: nexusEndpoint.GetEndpoint().GetResourceVersion(), + }) + if err != nil { + resp.Diagnostics.AddError("Failed to update Nexus endpoint", err.Error()) + return + } + + if err := client.AwaitAsyncOperation(ctx, r.client, svcResp.GetAsyncOperation()); err != nil { + resp.Diagnostics.AddError("Failed to update Nexus endpoint", err.Error()) + return + } + + nexusEndpoint, err = r.client.CloudService().GetNexusEndpoint(ctx, &cloudservicev1.GetNexusEndpointRequest{ + EndpointId: plan.ID.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError("Failed to get Nexus endpoint after update", err.Error()) + return + } + + resp.Diagnostics.Append(updateNexusEndpointModelFromSpec(ctx, &plan, nexusEndpoint.Endpoint)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *nexusEndpointResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state nexusEndpointResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + deleteTimeout, diags := state.Timeouts.Delete(ctx, defaultDeleteTimeout) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + nexusEndpoint, err := r.client.CloudService().GetNexusEndpoint(ctx, &cloudservicev1.GetNexusEndpointRequest{ + EndpointId: state.ID.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError("Failed to get current Nexus endpoint status", err.Error()) + return + } + + ctx, cancel := context.WithTimeout(ctx, deleteTimeout) + defer cancel() + + svcResp, err := r.client.CloudService().DeleteNexusEndpoint(ctx, &cloudservicev1.DeleteNexusEndpointRequest{ + EndpointId: state.ID.ValueString(), + ResourceVersion: nexusEndpoint.GetEndpoint().GetResourceVersion(), + }) + if err != nil { + resp.Diagnostics.AddError("Failed to delete Nexus endpoint", err.Error()) + return + } + + if err := client.AwaitAsyncOperation(ctx, r.client, svcResp.AsyncOperation); err != nil { + resp.Diagnostics.AddError("Failed to delete Nexus endpoint", err.Error()) + } +} + +func (r *nexusEndpointResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func updateNexusEndpointModelFromSpec(ctx context.Context, model *nexusEndpointResourceModel, nexusEndpoint *nexusv1.Endpoint) diag.Diagnostics { + var diags diag.Diagnostics + + model.ID = types.StringValue(nexusEndpoint.GetId()) + + model.Name = types.StringValue(nexusEndpoint.GetSpec().GetName()) + + if nexusEndpoint.GetSpec().GetDescription() != "" { + model.Description = types.StringValue(nexusEndpoint.GetSpec().GetDescription()) + } + + nexusEndpointTargetSpec := nexusEndpoint.GetSpec().GetTargetSpec() + if workerSpec := nexusEndpointTargetSpec.GetWorkerTargetSpec(); workerSpec != nil { + workerTargetSpec := &nexusEndpointWorkerTargetModel{ + NamespaceID: types.StringValue(workerSpec.GetNamespaceId()), + TaskQueue: types.StringValue(workerSpec.GetTaskQueue()), + } + model.WorkerTargetSpec, diags = types.ObjectValueFrom(ctx, workerTargetSpecAttrs, workerTargetSpec) + if diags.HasError() { + return diags + } + } + + allowedNamespaces := make([]types.String, 0) + nexusEndpointPolicySpecs := nexusEndpoint.GetSpec().GetPolicySpecs() + for _, policySpec := range nexusEndpointPolicySpecs { + if policySpec.GetAllowedCloudNamespacePolicySpec() != nil { + allowedNamespaces = append(allowedNamespaces, types.StringValue(policySpec.GetAllowedCloudNamespacePolicySpec().GetNamespaceId())) + } + } + model.AllowedCallerNamespaces, diags = types.SetValueFrom(ctx, types.StringType, allowedNamespaces) + if diags.HasError() { + return diags + } + + return diags +} + +func getTargetSpecFromModel(ctx context.Context, model *nexusEndpointResourceModel) (*nexusv1.EndpointTargetSpec, diag.Diagnostics) { + var diags diag.Diagnostics + + var workerTargetSpecModel nexusEndpointWorkerTargetModel + diags.Append(model.WorkerTargetSpec.As(ctx, &workerTargetSpecModel, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return nil, diags + } + workerTargetSpec := &nexusv1.WorkerTargetSpec{ + NamespaceId: workerTargetSpecModel.NamespaceID.ValueString(), + TaskQueue: workerTargetSpecModel.TaskQueue.ValueString(), + } + + return &nexusv1.EndpointTargetSpec{ + Variant: &nexusv1.EndpointTargetSpec_WorkerTargetSpec{ + WorkerTargetSpec: workerTargetSpec, + }, + }, diags +} + +func getPolicySpecsFromModel(_ context.Context, model *nexusEndpointResourceModel) ([]*nexusv1.EndpointPolicySpec, diag.Diagnostics) { + policySpecs := make([]*nexusv1.EndpointPolicySpec, 0, len(model.AllowedCallerNamespaces.Elements())) + for _, namespace := range model.AllowedCallerNamespaces.Elements() { + ns, ok := namespace.(types.String) + if !ok { + return nil, diag.Diagnostics{diag.NewErrorDiagnostic("Invalid namespace type", "Expected types.String")} + } + policySpecs = append(policySpecs, &nexusv1.EndpointPolicySpec{ + Variant: &nexusv1.EndpointPolicySpec_AllowedCloudNamespacePolicySpec{ + AllowedCloudNamespacePolicySpec: &nexusv1.AllowedCloudNamespacePolicySpec{ + NamespaceId: ns.ValueString(), + }, + }, + }) + } + + return policySpecs, nil +} diff --git a/internal/provider/nexus_endpoint_resource_test.go b/internal/provider/nexus_endpoint_resource_test.go new file mode 100644 index 0000000..2606bf8 --- /dev/null +++ b/internal/provider/nexus_endpoint_resource_test.go @@ -0,0 +1,108 @@ +package provider + +import ( + "fmt" + "strconv" + "strings" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccNexusEndpointResource(t *testing.T) { + timeSuffix := time.Now().Format("060102150405") + endpointName := fmt.Sprintf("tf-nexus-endpoint-%s-%s", timeSuffix, randomStringWithLength(3)) + description := "test description" + targetNamespaceName := fmt.Sprintf("tf-nexus-target-%s-%s", timeSuffix, randomStringWithLength(4)) + taskQueue := "task-queue-1" + callerNamespaceName := fmt.Sprintf("tf-nexus-caller-%s-%s", timeSuffix, randomStringWithLength(4)) + callerNamespace2Name := fmt.Sprintf("tf-nexus-caller2-%s-%s", timeSuffix, randomStringWithLength(3)) + + updatedDescription := "updated description" + updatedTaskQueue := "task-queue-2" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccNexusEndpointResourceConfig(endpointName, description, targetNamespaceName, taskQueue, []string{callerNamespaceName, callerNamespace2Name}), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("temporalcloud_nexus_endpoint.test", "name", endpointName), + resource.TestCheckResourceAttr("temporalcloud_nexus_endpoint.test", "description", description), + resource.TestCheckResourceAttrSet("temporalcloud_nexus_endpoint.test", "worker_target_spec.namespace_id"), + // resource.TestCheckResourceAttr("temporalcloud_nexus_endpoint.test", "worker_target_spec.namespace_id", targetNamespaceName + "." + accountID), + resource.TestCheckResourceAttr("temporalcloud_nexus_endpoint.test", "worker_target_spec.task_queue", taskQueue), + resource.TestCheckResourceAttr("temporalcloud_nexus_endpoint.test", "allowed_caller_namespaces.#", "2"), + resource.TestCheckResourceAttrSet("temporalcloud_nexus_endpoint.test", "id"), + ), + }, + // ImportState testing + { + ResourceName: "temporalcloud_nexus_endpoint.test", + ImportState: true, + ImportStateVerify: true, + }, + // Update and Read testing + { + Config: testAccNexusEndpointResourceConfig(endpointName, updatedDescription, targetNamespaceName, updatedTaskQueue, []string{callerNamespaceName}), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("temporalcloud_nexus_endpoint.test", "description", updatedDescription), + resource.TestCheckResourceAttr("temporalcloud_nexus_endpoint.test", "worker_target_spec.task_queue", updatedTaskQueue), + resource.TestCheckResourceAttr("temporalcloud_nexus_endpoint.test", "allowed_caller_namespaces.#", "1"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func testAccNamespaceResourceConfig(resourceName, name, region string, retentionDays int) string { + return fmt.Sprintf(` +resource "temporalcloud_namespace" %[1]q { + name = %[2]q + regions = [%[3]q] + api_key_auth = true + retention_days = %[4]d + timeouts { + create = "15m" + delete = "15m" + } +} +`, resourceName, name, region, retentionDays) +} + +func testAccNexusEndpointResourceConfig(name, description, targetNamespaceName, taskQueue string, allowedNamespaces []string) string { + region := "aws-us-west-2" + retentionDays := 1 + allowedNamespaceIDs := []string{} + namespacesConfig := testAccNamespaceResourceConfig("target_namespace", targetNamespaceName, region, retentionDays) + for i, allowedNamespace := range allowedNamespaces { + namespacesConfig += testAccNamespaceResourceConfig("allowed_namespace_"+strconv.Itoa(i), allowedNamespace, region, retentionDays) + allowedNamespaceIDs = append(allowedNamespaceIDs, "temporalcloud_namespace.allowed_namespace_"+strconv.Itoa(i)+".id") + } + allowedNamespaceIDsStr := fmt.Sprintf("[%s]", strings.Join(allowedNamespaceIDs, ", ")) + + return fmt.Sprintf(` +%[1]s + +resource "temporalcloud_nexus_endpoint" "test" { + name = %[2]q + description = %[3]q + + worker_target_spec = { + namespace_id = temporalcloud_namespace.target_namespace.id + task_queue = %[4]q + } + + allowed_caller_namespaces = %[5]s + + timeouts { + create = "4m" + delete = "4m" + } +} +`, namespacesConfig, name, description, taskQueue, allowedNamespaceIDsStr) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 69f75b5..619d2e8 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -140,6 +140,7 @@ func (p *TerraformCloudProvider) Resources(ctx context.Context) []func() resourc NewUserResource, NewServiceAccountResource, NewApiKeyResource, + NewNexusEndpointResource, } } diff --git a/internal/provider/utils_test.go b/internal/provider/utils_test.go new file mode 100644 index 0000000..f1cef06 --- /dev/null +++ b/internal/provider/utils_test.go @@ -0,0 +1,16 @@ +package provider + +import ( + "math/rand" + "time" +) + +func randomStringWithLength(length int) string { + r := rand.New(rand.NewSource(time.Now().UTC().UnixNano())) + const charset = "abcdefghijklmnopqrstuvwxyz" + b := make([]byte, length) + for i := range b { + b[i] = charset[r.Intn(len(charset))] + } + return string(b) +}