diff --git a/docs/resources/account_audit_log_sink.md b/docs/resources/account_audit_log_sink.md new file mode 100644 index 0000000..20f64c3 --- /dev/null +++ b/docs/resources/account_audit_log_sink.md @@ -0,0 +1,95 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "temporalcloud_account_audit_log_sink Resource - terraform-provider-temporalcloud" +subcategory: "" +description: |- + Provisions an account audit log sink. +--- + +# temporalcloud_account_audit_log_sink (Resource) + +Provisions an account audit log sink. + +## Example Usage + +```terraform +terraform { + required_providers { + temporalcloud = { + source = "temporalio/temporalcloud" + } + } +} + +provider "temporalcloud" { + +} + +# Example with Kinesis +resource "temporalcloud_account_audit_log_sink" "kinesis_sink" { + sink_name = "my-kinesis-sink" + enabled = true + kinesis = { + role_name = "arn:aws:iam::123456789012:role/TemporalCloudKinesisRole" + destination_uri = "arn:aws:kinesis:us-east-1:123456789012:stream/my-audit-stream" + region = "us-east-1" + } +} + +# Example with PubSub +resource "temporalcloud_account_audit_log_sink" "pubsub_sink" { + sink_name = "my-pubsub-sink" + enabled = true + pubsub = { + service_account_id = "my-service-account-id" + topic_name = "temporal-audit-logs" + gcp_project_id = "my-gcp-project" + } +} +``` + + +## Schema + +### Required + +- `sink_name` (String) The unique name of the audit log sink, it can't be changed once set. + +### Optional + +- `enabled` (Boolean) A flag indicating whether the audit log sink is enabled or not. +- `kinesis` (Attributes) The Kinesis configuration details when destination_type is Kinesis. (see [below for nested schema](#nestedatt--kinesis)) +- `pubsub` (Attributes) The PubSub configuration details when destination_type is PubSub. (see [below for nested schema](#nestedatt--pubsub)) +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `id` (String) The unique identifier of the account audit log sink. + + +### Nested Schema for `kinesis` + +Required: + +- `destination_uri` (String) The destination URI of the Kinesis stream where Temporal will send data. +- `region` (String) The region of the Kinesis stream. +- `role_name` (String) The IAM role that Temporal Cloud assumes for writing records to the customer's Kinesis stream. + + + +### Nested Schema for `pubsub` + +Required: + +- `gcp_project_id` (String) The GCP project ID of the PubSub topic and service account. +- `service_account_id` (String) The customer service account ID that Temporal Cloud impersonates for writing records to the customer's PubSub topic. +- `topic_name` (String) The destination PubSub topic name for Temporal. + + + +### 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. diff --git a/examples/resources/temporalcloud_account_audit_log_sink/resource.tf b/examples/resources/temporalcloud_account_audit_log_sink/resource.tf new file mode 100644 index 0000000..c531e64 --- /dev/null +++ b/examples/resources/temporalcloud_account_audit_log_sink/resource.tf @@ -0,0 +1,33 @@ +terraform { + required_providers { + temporalcloud = { + source = "temporalio/temporalcloud" + } + } +} + +provider "temporalcloud" { + +} + +# Example with Kinesis +resource "temporalcloud_account_audit_log_sink" "kinesis_sink" { + sink_name = "my-kinesis-sink" + enabled = true + kinesis = { + role_name = "arn:aws:iam::123456789012:role/TemporalCloudKinesisRole" + destination_uri = "arn:aws:kinesis:us-east-1:123456789012:stream/my-audit-stream" + region = "us-east-1" + } +} + +# Example with PubSub +resource "temporalcloud_account_audit_log_sink" "pubsub_sink" { + sink_name = "my-pubsub-sink" + enabled = true + pubsub = { + service_account_id = "my-service-account-id" + topic_name = "temporal-audit-logs" + gcp_project_id = "my-gcp-project" + } +} diff --git a/go.mod b/go.mod index 689dc2c..1a897b6 100644 --- a/go.mod +++ b/go.mod @@ -95,3 +95,5 @@ require ( gopkg.in/yaml.v2 v2.3.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace go.temporal.io/cloud-sdk => github.com/temporalio/cloud-sdk-go v0.6.1-0.20251031194819-5117604c8a4f diff --git a/go.sum b/go.sum index 49a21ef..eca75c5 100644 --- a/go.sum +++ b/go.sum @@ -202,6 +202,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/temporalio/cloud-sdk-go v0.6.1-0.20251031194819-5117604c8a4f h1:i8w+OmC4ocK72/LdwKXnXawHC6CCMHrtzVNYI4+6tGk= +github.com/temporalio/cloud-sdk-go v0.6.1-0.20251031194819-5117604c8a4f/go.mod h1:AueDDyuayosk+zalfrnuftRqnRQTHwD0HYwNgEQc0YE= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= @@ -238,8 +240,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.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/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= diff --git a/internal/provider/account_audit_log_sink_datasource.go b/internal/provider/account_audit_log_sink_datasource.go new file mode 100644 index 0000000..a1dbd59 --- /dev/null +++ b/internal/provider/account_audit_log_sink_datasource.go @@ -0,0 +1,207 @@ +package provider + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/temporalio/terraform-provider-temporalcloud/internal/client" + "github.com/temporalio/terraform-provider-temporalcloud/internal/provider/enums" + internaltypes "github.com/temporalio/terraform-provider-temporalcloud/internal/types" + accountv1 "go.temporal.io/cloud-sdk/api/account/v1" + cloudservicev1 "go.temporal.io/cloud-sdk/api/cloudservice/v1" +) + +var ( + _ datasource.DataSource = &accountAuditLogSinkDataSource{} + _ datasource.DataSourceWithConfigure = &accountAuditLogSinkDataSource{} +) + +func NewAccountAuditLogSinkDataSource() datasource.DataSource { + return &accountAuditLogSinkDataSource{} +} + +type ( + accountAuditLogSinkDataSource struct { + client *client.Client + } + + accountAuditLogSinkDataModel struct { + ID types.String `tfsdk:"id"` + SinkName types.String `tfsdk:"sink_name"` + Enabled types.Bool `tfsdk:"enabled"` + Kinesis types.Object `tfsdk:"kinesis"` + PubSub types.Object `tfsdk:"pubsub"` + State types.String `tfsdk:"state"` + } +) + +func accountAuditLogSinkDataSourceSchema(idRequired bool) map[string]schema.Attribute { + idAttribute := schema.StringAttribute{ + Description: "The unique identifier of the account audit log sink.", + } + + switch idRequired { + case true: + idAttribute.Required = true + case false: + idAttribute.Computed = true + } + + return map[string]schema.Attribute{ + "id": idAttribute, + "sink_name": schema.StringAttribute{ + Description: "The unique name of the audit log sink.", + Required: true, + }, + "enabled": schema.BoolAttribute{ + Description: "A flag indicating whether the audit log sink is enabled or not.", + Computed: true, + }, + "kinesis": schema.SingleNestedAttribute{ + Description: "The Kinesis configuration details when destination_type is Kinesis.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "role_name": schema.StringAttribute{ + Description: "The IAM role that Temporal Cloud assumes for writing records to the customer's Kinesis stream.", + Computed: true, + }, + "destination_uri": schema.StringAttribute{ + Description: "The destination URI of the Kinesis stream where Temporal will send data.", + Computed: true, + }, + "region": schema.StringAttribute{ + Description: "The region of the Kinesis stream.", + Computed: true, + }, + }, + }, + "pubsub": schema.SingleNestedAttribute{ + Description: "The PubSub configuration details when destination_type is PubSub.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "service_account_id": schema.StringAttribute{ + Description: "The customer service account ID that Temporal Cloud impersonates for writing records to the customer's PubSub topic.", + Computed: true, + }, + "topic_name": schema.StringAttribute{ + Description: "The destination PubSub topic name for Temporal.", + Computed: true, + }, + "gcp_project_id": schema.StringAttribute{ + Description: "The GCP project ID of the PubSub topic and service account.", + Computed: true, + }, + }, + }, + "state": schema.StringAttribute{ + Description: "The current state of the audit log sink.", + Computed: true, + }, + } +} + +func (d *accountAuditLogSinkDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_account_audit_log_sink" +} + +func (d *accountAuditLogSinkDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.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 + } + + d.client = client +} + +func (d *accountAuditLogSinkDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Fetches details about an account audit log sink.", + Attributes: accountAuditLogSinkDataSourceSchema(false), + } +} + +func (d *accountAuditLogSinkDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var input accountAuditLogSinkDataModel + resp.Diagnostics.Append(req.Config.Get(ctx, &input)...) + if resp.Diagnostics.HasError() { + return + } + + if len(input.SinkName.ValueString()) == 0 { + resp.Diagnostics.AddError("invalid account audit log sink sink_name", "account audit log sink sink_name is required") + return + } + + auditLogSinkResp, err := d.client.CloudService().GetAccountAuditLogSink(ctx, &cloudservicev1.GetAccountAuditLogSinkRequest{ + Name: input.SinkName.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError("Failed to get account audit log sink", err.Error()) + return + } + + model, diags := accountAuditLogSinkToAccountAuditLogSinkDataModel(ctx, auditLogSinkResp.GetSink()) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) +} + +func accountAuditLogSinkToAccountAuditLogSinkDataModel(ctx context.Context, auditLogSink *accountv1.AuditLogSink) (*accountAuditLogSinkDataModel, diag.Diagnostics) { + var diags diag.Diagnostics + stateStr, err := enums.FromResourceState(auditLogSink.State) + if err != nil { + diags.AddError("Failed to convert resource state", err.Error()) + return nil, diags + } + + model := new(accountAuditLogSinkDataModel) + model.ID = types.StringValue(auditLogSink.GetName()) + model.SinkName = types.StringValue(auditLogSink.GetName()) + model.Enabled = types.BoolValue(auditLogSink.GetSpec().GetEnabled()) + model.State = types.StringValue(stateStr) + + kinesisObj := types.ObjectNull(internaltypes.KinesisSpecModelAttrTypes) + if auditLogSink.GetSpec().GetKinesisSink() != nil { + kinesisSpec := internaltypes.KinesisSpecModel{ + RoleName: types.StringValue(auditLogSink.GetSpec().GetKinesisSink().GetRoleName()), + DestinationUri: types.StringValue(auditLogSink.GetSpec().GetKinesisSink().GetDestinationUri()), + Region: types.StringValue(auditLogSink.GetSpec().GetKinesisSink().GetRegion()), + } + + kinesisObj, diags = types.ObjectValueFrom(ctx, internaltypes.KinesisSpecModelAttrTypes, kinesisSpec) + if diags.HasError() { + return nil, diags + } + } + + pubsubObj := types.ObjectNull(internaltypes.PubSubSpecModelAttrTypes) + if auditLogSink.GetSpec().GetPubSubSink() != nil { + pubsubSpec := internaltypes.PubSubSpecModel{ + ServiceAccountId: types.StringValue(auditLogSink.GetSpec().GetPubSubSink().GetServiceAccountId()), + TopicName: types.StringValue(auditLogSink.GetSpec().GetPubSubSink().GetTopicName()), + GcpProjectId: types.StringValue(auditLogSink.GetSpec().GetPubSubSink().GetGcpProjectId()), + } + + pubsubObj, diags = types.ObjectValueFrom(ctx, internaltypes.PubSubSpecModelAttrTypes, pubsubSpec) + if diags.HasError() { + return nil, diags + } + } + + model.Kinesis = kinesisObj + model.PubSub = pubsubObj + return model, diags +} diff --git a/internal/provider/account_audit_log_sink_datasource_test.go b/internal/provider/account_audit_log_sink_datasource_test.go new file mode 100644 index 0000000..acf34c0 --- /dev/null +++ b/internal/provider/account_audit_log_sink_datasource_test.go @@ -0,0 +1,282 @@ +package provider + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccDataSource_AccountAuditLogSink_Kinesis(t *testing.T) { + accountAuditLogSinkTestLocks.Lock("account") + defer func() { + _ = accountAuditLogSinkTestLocks.Unlock("account") + }() + + sinkRegion := "us-east-1" + sinkName := fmt.Sprintf("tf-test-sink-%s", randomString(8)) + + config := func(name, region string) string { + return fmt.Sprintf(` +provider "temporalcloud" { + +} + +resource "temporalcloud_account_audit_log_sink" "test" { + sink_name = %[1]q + enabled = true + kinesis = { + role_name = "test-role" + destination_uri = "test-uri" + region = %[2]q + } +} + +data "temporalcloud_account_audit_log_sink" "test" { + sink_name = temporalcloud_account_audit_log_sink.test.sink_name +} + +output "account_audit_log_sink" { + value = data.temporalcloud_account_audit_log_sink.test +} +`, name, region) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: config(sinkName, sinkRegion), + Check: func(s *terraform.State) error { + output, ok := s.RootModule().Outputs["account_audit_log_sink"] + if !ok { + return fmt.Errorf("missing expected output") + } + + outputValue, ok := output.Value.(map[string]interface{}) + if !ok { + return fmt.Errorf("expected value to be map") + } + + outputSinkName, ok := outputValue["sink_name"].(string) + if !ok { + return fmt.Errorf("expected sink_name to be a string") + } + if outputSinkName != sinkName { + return fmt.Errorf("expected sink_name to be %q, got: %q", sinkName, outputSinkName) + } + + outputID, ok := outputValue["id"].(string) + if !ok { + return fmt.Errorf("expected id to be a string") + } + if outputID != sinkName { + return fmt.Errorf("expected id to be %q, got: %q", sinkName, outputID) + } + + outputEnabled, ok := outputValue["enabled"].(bool) + if !ok { + return fmt.Errorf("expected enabled to be a bool") + } + if !outputEnabled { + return fmt.Errorf("expected enabled to be true, got: false") + } + + outputState, ok := outputValue["state"].(string) + if !ok { + return fmt.Errorf("expected state to be a string") + } + if outputState == "" { + return fmt.Errorf("expected state to not be empty") + } + + // Check Kinesis configuration + kinesisMap, ok := outputValue["kinesis"].(map[string]interface{}) + if !ok { + return fmt.Errorf("expected kinesis to be a map") + } + if kinesisMap == nil { + return fmt.Errorf("expected kinesis to not be null") + } + + roleName, ok := kinesisMap["role_name"].(string) + if !ok { + return fmt.Errorf("expected kinesis.role_name to be a string") + } + if roleName != "test-role" { + return fmt.Errorf("expected kinesis.role_name to be 'test-role', got: %q", roleName) + } + + destinationURI, ok := kinesisMap["destination_uri"].(string) + if !ok { + return fmt.Errorf("expected kinesis.destination_uri to be a string") + } + if destinationURI != "test-uri" { + return fmt.Errorf("expected kinesis.destination_uri to be 'test-uri', got: %q", destinationURI) + } + + region, ok := kinesisMap["region"].(string) + if !ok { + return fmt.Errorf("expected kinesis.region to be a string") + } + if region != sinkRegion { + return fmt.Errorf("expected kinesis.region to be %q, got: %q", sinkRegion, region) + } + + // For Kinesis sink, pubsub should be null/empty + if pubsub, exists := outputValue["pubsub"]; exists && pubsub != nil { + return fmt.Errorf("expected pubsub to be null for Kinesis sink") + } + + return nil + }, + }, + { + ResourceName: "temporalcloud_account_audit_log_sink.test", + ImportState: true, + ImportStateVerify: true, + Destroy: true, + }, + }, + }) +} + +func TestAccDataSource_AccountAuditLogSink_PubSub(t *testing.T) { + accountAuditLogSinkTestLocks.Lock("account") + defer func() { + _ = accountAuditLogSinkTestLocks.Unlock("account") + }() + + sinkName := fmt.Sprintf("tf-test-sink-%s", randomString(8)) + + config := func(name string) string { + return fmt.Sprintf(` +provider "temporalcloud" { + +} + +resource "temporalcloud_account_audit_log_sink" "test" { + sink_name = %[1]q + enabled = true + pubsub = { + service_account_id = "test-sa" + topic_name = "test-topic" + gcp_project_id = "test-project" + } +} + +data "temporalcloud_account_audit_log_sink" "test" { + sink_name = temporalcloud_account_audit_log_sink.test.sink_name +} + +output "account_audit_log_sink" { + value = data.temporalcloud_account_audit_log_sink.test +} +`, name) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: config(sinkName), + Check: func(s *terraform.State) error { + output, ok := s.RootModule().Outputs["account_audit_log_sink"] + if !ok { + return fmt.Errorf("missing expected output") + } + + outputValue, ok := output.Value.(map[string]interface{}) + if !ok { + return fmt.Errorf("expected value to be map") + } + + outputSinkName, ok := outputValue["sink_name"].(string) + if !ok { + return fmt.Errorf("expected sink_name to be a string") + } + if outputSinkName != sinkName { + return fmt.Errorf("expected sink_name to be %q, got: %q", sinkName, outputSinkName) + } + + outputID, ok := outputValue["id"].(string) + if !ok { + return fmt.Errorf("expected id to be a string") + } + if outputID != sinkName { + return fmt.Errorf("expected id to be %q, got: %q", sinkName, outputID) + } + + outputEnabled, ok := outputValue["enabled"].(bool) + if !ok { + return fmt.Errorf("expected enabled to be a bool") + } + if !outputEnabled { + return fmt.Errorf("expected enabled to be true, got: false") + } + + outputState, ok := outputValue["state"].(string) + if !ok { + return fmt.Errorf("expected state to be a string") + } + if outputState == "" { + return fmt.Errorf("expected state to not be empty") + } + + // Check PubSub configuration + pubsubMap, ok := outputValue["pubsub"].(map[string]interface{}) + if !ok { + return fmt.Errorf("expected pubsub to be a map") + } + if pubsubMap == nil { + return fmt.Errorf("expected pubsub to not be null") + } + + serviceAccountID, ok := pubsubMap["service_account_id"].(string) + if !ok { + return fmt.Errorf("expected pubsub.service_account_id to be a string") + } + if serviceAccountID != "test-sa" { + return fmt.Errorf("expected pubsub.service_account_id to be 'test-sa', got: %q", serviceAccountID) + } + + topicName, ok := pubsubMap["topic_name"].(string) + if !ok { + return fmt.Errorf("expected pubsub.topic_name to be a string") + } + if topicName != "test-topic" { + return fmt.Errorf("expected pubsub.topic_name to be 'test-topic', got: %q", topicName) + } + + gcpProjectID, ok := pubsubMap["gcp_project_id"].(string) + if !ok { + return fmt.Errorf("expected pubsub.gcp_project_id to be a string") + } + if gcpProjectID != "test-project" { + return fmt.Errorf("expected pubsub.gcp_project_id to be 'test-project', got: %q", gcpProjectID) + } + + // For PubSub sink, kinesis should be null/empty + if kinesis, exists := outputValue["kinesis"]; exists && kinesis != nil { + return fmt.Errorf("expected kinesis to be null for PubSub sink") + } + + return nil + }, + }, + { + ResourceName: "temporalcloud_account_audit_log_sink.test", + ImportState: true, + ImportStateVerify: true, + Destroy: true, + }, + }, + }) +} diff --git a/internal/provider/account_audit_log_sink_resource.go b/internal/provider/account_audit_log_sink_resource.go new file mode 100644 index 0000000..72f738b --- /dev/null +++ b/internal/provider/account_audit_log_sink_resource.go @@ -0,0 +1,478 @@ +// The MIT License +// +// Copyright (c) 2023 Temporal Technologies Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package provider + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/temporalio/terraform-provider-temporalcloud/internal/client" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + internaltypes "github.com/temporalio/terraform-provider-temporalcloud/internal/types" + accountv1 "go.temporal.io/cloud-sdk/api/account/v1" + cloudservicev1 "go.temporal.io/cloud-sdk/api/cloudservice/v1" + sinkv1 "go.temporal.io/cloud-sdk/api/sink/v1" +) + +type ( + accountAuditLogSinkResource struct { + client *client.Client + } + + accountAuditLogSinkResourceModel struct { + ID types.String `tfsdk:"id"` + SinkName types.String `tfsdk:"sink_name"` + Enabled types.Bool `tfsdk:"enabled"` + Kinesis types.Object `tfsdk:"kinesis"` + PubSub types.Object `tfsdk:"pubsub"` + Timeouts timeouts.Value `tfsdk:"timeouts"` + } +) + +var ( + _ resource.Resource = (*accountAuditLogSinkResource)(nil) + _ resource.ResourceWithConfigure = (*accountAuditLogSinkResource)(nil) + _ resource.ResourceWithImportState = (*accountAuditLogSinkResource)(nil) +) + +func NewAccountAuditLogSinkResource() resource.Resource { + return &accountAuditLogSinkResource{} +} + +func (r *accountAuditLogSinkResource) Configure(ctx 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 *accountAuditLogSinkResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_account_audit_log_sink" +} + +func (r *accountAuditLogSinkResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Provisions an account audit log sink.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The unique identifier of the account audit log sink.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "sink_name": schema.StringAttribute{ + Description: "The unique name of the audit log sink, it can't be changed once set.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "enabled": schema.BoolAttribute{ + Description: "A flag indicating whether the audit log sink is enabled or not.", + Computed: true, + Default: booldefault.StaticBool(true), + Optional: true, + }, + "kinesis": schema.SingleNestedAttribute{ + Description: "The Kinesis configuration details when destination_type is Kinesis.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "role_name": schema.StringAttribute{ + Description: "The IAM role that Temporal Cloud assumes for writing records to the customer's Kinesis stream.", + Required: true, + }, + "destination_uri": schema.StringAttribute{ + Description: "The destination URI of the Kinesis stream where Temporal will send data.", + Required: true, + }, + "region": schema.StringAttribute{ + Description: "The region of the Kinesis stream.", + Required: true, + }, + }, + Validators: []validator.Object{ + objectvalidator.ExactlyOneOf(path.Expressions{ + path.MatchRoot("pubsub"), + }...), + }, + }, + "pubsub": schema.SingleNestedAttribute{ + Description: "The PubSub configuration details when destination_type is PubSub.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "service_account_id": schema.StringAttribute{ + Description: "The customer service account ID that Temporal Cloud impersonates for writing records to the customer's PubSub topic.", + Required: true, + }, + "topic_name": schema.StringAttribute{ + Description: "The destination PubSub topic name for Temporal.", + Required: true, + }, + "gcp_project_id": schema.StringAttribute{ + Description: "The GCP project ID of the PubSub topic and service account.", + Required: true, + }, + }, + Validators: []validator.Object{ + objectvalidator.ExactlyOneOf(path.Expressions{ + path.MatchRoot("kinesis"), + }...), + }, + }, + }, + Blocks: map[string]schema.Block{ + "timeouts": timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Delete: true, + }), + }, + } +} + +func (r *accountAuditLogSinkResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan accountAuditLogSinkResourceModel + 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() + + sinkSpec, d := getAccountAuditLogSinkSpecFromModel(ctx, &plan) + resp.Diagnostics.Append(d...) + if resp.Diagnostics.HasError() || sinkSpec == nil { + return + } + + svcResp, err := r.client.CloudService().CreateAccountAuditLogSink(ctx, &cloudservicev1.CreateAccountAuditLogSinkRequest{ + Spec: sinkSpec, + AsyncOperationId: uuid.New().String(), + }) + + if err != nil { + resp.Diagnostics.AddError("Failed to create account audit log sink", err.Error()) + return + } + + if err := client.AwaitAsyncOperation(ctx, r.client, svcResp.AsyncOperation); err != nil { + resp.Diagnostics.AddError("Failed to get account audit log sink creation status", err.Error()) + return + } + + sink, err := r.client.CloudService().GetAccountAuditLogSink(ctx, &cloudservicev1.GetAccountAuditLogSinkRequest{ + Name: sinkSpec.GetName(), + }) + + if err != nil { + resp.Diagnostics.AddError("Failed to get account audit log sink", err.Error()) + return + } + + resp.Diagnostics.Append(updateAccountAuditLogSinkModelFromSpec(ctx, &plan, sink.GetSink())...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func updateAccountAuditLogSinkModelFromSpec(ctx context.Context, state *accountAuditLogSinkResourceModel, sink *accountv1.AuditLogSink) diag.Diagnostics { + var diags diag.Diagnostics + + kinesisObj := types.ObjectNull(internaltypes.KinesisSpecModelAttrTypes) + if sink.GetSpec().GetKinesisSink() != nil { + kinesisSpec := internaltypes.KinesisSpecModel{ + RoleName: types.StringValue(sink.GetSpec().GetKinesisSink().GetRoleName()), + DestinationUri: types.StringValue(sink.GetSpec().GetKinesisSink().GetDestinationUri()), + Region: types.StringValue(sink.GetSpec().GetKinesisSink().GetRegion()), + } + + kinesisObj, diags = types.ObjectValueFrom(ctx, internaltypes.KinesisSpecModelAttrTypes, kinesisSpec) + if diags.HasError() { + return diags + } + } + + pubsubObj := types.ObjectNull(internaltypes.PubSubSpecModelAttrTypes) + if sink.GetSpec().GetPubSubSink() != nil { + pubsubSpec := internaltypes.PubSubSpecModel{ + ServiceAccountId: types.StringValue(sink.GetSpec().GetPubSubSink().GetServiceAccountId()), + TopicName: types.StringValue(sink.GetSpec().GetPubSubSink().GetTopicName()), + GcpProjectId: types.StringValue(sink.GetSpec().GetPubSubSink().GetGcpProjectId()), + } + + pubsubObj, diags = types.ObjectValueFrom(ctx, internaltypes.PubSubSpecModelAttrTypes, pubsubSpec) + if diags.HasError() { + return diags + } + } + + state.SinkName = types.StringValue(sink.GetName()) + state.Enabled = types.BoolValue(sink.GetSpec().GetEnabled()) + state.Kinesis = kinesisObj + state.PubSub = pubsubObj + state.ID = types.StringValue(sink.GetName()) + + return diags +} + +func (r *accountAuditLogSinkResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var plan accountAuditLogSinkResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + deleteTimeout, diags := plan.Timeouts.Delete(ctx, defaultDeleteTimeout) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + sinkName := plan.ID.ValueString() + currentSink, err := r.client.CloudService().GetAccountAuditLogSink(ctx, &cloudservicev1.GetAccountAuditLogSinkRequest{ + Name: sinkName, + }) + + if err != nil { + switch status.Code(err) { + case codes.NotFound: + tflog.Warn(ctx, "Account Audit Log Sink Resource not found, removing from state", map[string]interface{}{ + "id": plan.ID.ValueString(), + }) + return + } + + resp.Diagnostics.AddError("Failed to get account audit log sink", err.Error()) + return + } + + ctx, cancel := context.WithTimeout(ctx, deleteTimeout) + defer cancel() + + svcResp, err := r.client.CloudService().DeleteAccountAuditLogSink(ctx, &cloudservicev1.DeleteAccountAuditLogSinkRequest{ + Name: sinkName, + ResourceVersion: currentSink.GetSink().GetResourceVersion(), + AsyncOperationId: uuid.New().String(), + }) + + if err != nil { + switch status.Code(err) { + case codes.NotFound: + tflog.Warn(ctx, "Account Audit Log Sink Resource not found, removing from state", map[string]interface{}{ + "id": plan.ID.ValueString(), + }) + return + } + + resp.Diagnostics.AddError("Failed to delete account audit log sink", err.Error()) + return + } + + if err := client.AwaitAsyncOperation(ctx, r.client, svcResp.AsyncOperation); err != nil { + resp.Diagnostics.AddError("Failed to get account audit log sink deletion status", err.Error()) + return + } +} + +func (r *accountAuditLogSinkResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func getAccountAuditLogSinkSpecFromModel(ctx context.Context, plan *accountAuditLogSinkResourceModel) (*accountv1.AuditLogSinkSpec, diag.Diagnostics) { + var diags diag.Diagnostics + + // Check that only one of Kinesis or PubSub is set + if !plan.Kinesis.IsNull() && !plan.PubSub.IsNull() { + diags.AddError("Invalid sink configuration", "Only one of Kinesis or PubSub can be configured") + return nil, diags + } + + if !plan.Kinesis.IsNull() { + var kinesisSpec internaltypes.KinesisSpecModel + diags.Append(plan.Kinesis.As(ctx, &kinesisSpec, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return nil, diags + } + + kinesisSinkSpec := &sinkv1.KinesisSpec{ + RoleName: kinesisSpec.RoleName.ValueString(), + DestinationUri: kinesisSpec.DestinationUri.ValueString(), + Region: kinesisSpec.Region.ValueString(), + } + + return &accountv1.AuditLogSinkSpec{ + Name: plan.SinkName.ValueString(), + Enabled: plan.Enabled.ValueBool(), + SinkType: &accountv1.AuditLogSinkSpec_KinesisSink{ + KinesisSink: kinesisSinkSpec, + }, + }, nil + } else if !plan.PubSub.IsNull() { + var pubsubSpec internaltypes.PubSubSpecModel + diags.Append(plan.PubSub.As(ctx, &pubsubSpec, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return nil, diags + } + + pubsubSinkSpec := &sinkv1.PubSubSpec{ + ServiceAccountId: pubsubSpec.ServiceAccountId.ValueString(), + TopicName: pubsubSpec.TopicName.ValueString(), + GcpProjectId: pubsubSpec.GcpProjectId.ValueString(), + } + + return &accountv1.AuditLogSinkSpec{ + Name: plan.SinkName.ValueString(), + Enabled: plan.Enabled.ValueBool(), + SinkType: &accountv1.AuditLogSinkSpec_PubSubSink{ + PubSubSink: pubsubSinkSpec, + }, + }, nil + } + + return nil, diags +} + +func (r *accountAuditLogSinkResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state accountAuditLogSinkResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + sinkName := state.ID.ValueString() + + sink, err := r.client.CloudService().GetAccountAuditLogSink(ctx, &cloudservicev1.GetAccountAuditLogSinkRequest{ + Name: sinkName, + }) + if err != nil { + switch status.Code(err) { + case codes.NotFound: + tflog.Warn(ctx, "Account Audit Log Sink Resource not found, removing from state", map[string]interface{}{ + "id": state.ID.ValueString(), + }) + resp.State.RemoveResource(ctx) + return + } + + resp.Diagnostics.AddError("Failed to get account audit log sink", err.Error()) + return + } + + resp.Diagnostics.Append(updateAccountAuditLogSinkModelFromSpec(ctx, &state, sink.GetSink())...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +func (r *accountAuditLogSinkResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan accountAuditLogSinkResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + sinkSpec, diags := getAccountAuditLogSinkSpecFromModel(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() || sinkSpec == nil { + return + } + + sinkName := plan.ID.ValueString() + + currentSink, err := r.client.CloudService().GetAccountAuditLogSink(ctx, &cloudservicev1.GetAccountAuditLogSinkRequest{ + Name: sinkName, + }) + + if err != nil { + resp.Diagnostics.AddError("Failed to get account audit log sink", err.Error()) + return + } + + svcResp, err := r.client.CloudService().UpdateAccountAuditLogSink(ctx, &cloudservicev1.UpdateAccountAuditLogSinkRequest{ + Spec: sinkSpec, + ResourceVersion: currentSink.GetSink().GetResourceVersion(), + AsyncOperationId: uuid.New().String(), + }) + if err != nil { + resp.Diagnostics.AddError("Failed to update account audit log sink", err.Error()) + return + } + + if err := client.AwaitAsyncOperation(ctx, r.client, svcResp.AsyncOperation); err != nil { + resp.Diagnostics.AddError("Failed to get account audit log sink update status", err.Error()) + return + } + + sink, err := r.client.CloudService().GetAccountAuditLogSink(ctx, &cloudservicev1.GetAccountAuditLogSinkRequest{ + Name: sinkName, + }) + if err != nil { + resp.Diagnostics.AddError("Failed to get account audit log sink", err.Error()) + return + } + + resp.Diagnostics.Append(updateAccountAuditLogSinkModelFromSpec(ctx, &plan, sink.GetSink())...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} diff --git a/internal/provider/account_audit_log_sink_resource_test.go b/internal/provider/account_audit_log_sink_resource_test.go new file mode 100644 index 0000000..e7ca0bf --- /dev/null +++ b/internal/provider/account_audit_log_sink_resource_test.go @@ -0,0 +1,198 @@ +package provider + +import ( + "context" + "fmt" + "testing" + + fwresource "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/jpillora/maplock" +) + +// accountAuditLogSinkTestLocks is a per-account mutex that protects against concurrent sink operations +// across all test files, since an account can only have one audit log sink at a time. +var accountAuditLogSinkTestLocks = maplock.New() + +func TestAccountAuditLogSinkResource_Schema(t *testing.T) { + t.Parallel() + + ctx := context.Background() + schemaRequest := fwresource.SchemaRequest{} + schemaResponse := &fwresource.SchemaResponse{} + + NewAccountAuditLogSinkResource().Schema(ctx, schemaRequest, schemaResponse) + + if schemaResponse.Diagnostics.HasError() { + t.Fatalf("Schema method diagnostics: %+v", schemaResponse.Diagnostics) + } + + diagnostics := schemaResponse.Schema.ValidateImplementation(ctx) + + if diagnostics.HasError() { + t.Fatalf("Schema validation diagnostics: %+v", diagnostics) + } +} + +func TestAccAccountAuditLogSink_Kinesis(t *testing.T) { + accountAuditLogSinkTestLocks.Lock("account") + defer func() { + _ = accountAuditLogSinkTestLocks.Unlock("account") + }() + + sinkRegion := "us-east-1" + sinkName := fmt.Sprintf("tf-test-sink-%s", randomString(8)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccAccountAuditLogSinkKinesisConfig(sinkName, sinkRegion), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("temporalcloud_account_audit_log_sink.test", "sink_name", sinkName), + resource.TestCheckResourceAttr("temporalcloud_account_audit_log_sink.test", "enabled", "true"), + resource.TestCheckResourceAttr("temporalcloud_account_audit_log_sink.test", "kinesis.role_name", "test-role"), + resource.TestCheckResourceAttr("temporalcloud_account_audit_log_sink.test", "kinesis.destination_uri", "test-uri"), + resource.TestCheckResourceAttr("temporalcloud_account_audit_log_sink.test", "kinesis.region", sinkRegion), + ), + }, + // ImportState testing + { + ResourceName: "temporalcloud_account_audit_log_sink.test", + ImportState: true, + ImportStateVerify: true, + }, + // Update testing + { + Config: testAccAccountAuditLogSinkKinesisConfigUpdate(sinkName, sinkRegion), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("temporalcloud_account_audit_log_sink.test", "enabled", "false"), + resource.TestCheckResourceAttr("temporalcloud_account_audit_log_sink.test", "kinesis.role_name", "test-updated-role"), + resource.TestCheckResourceAttr("temporalcloud_account_audit_log_sink.test", "kinesis.destination_uri", "test-updated-uri"), + resource.TestCheckResourceAttr("temporalcloud_account_audit_log_sink.test", "kinesis.region", sinkRegion), + ), + }, + // Delete testing + { + ResourceName: "temporalcloud_account_audit_log_sink.test", + ImportState: true, + ImportStateVerify: true, + Destroy: true, + }, + }, + }) +} + +func TestAccAccountAuditLogSink_PubSub(t *testing.T) { + accountAuditLogSinkTestLocks.Lock("account") + defer func() { + _ = accountAuditLogSinkTestLocks.Unlock("account") + }() + + sinkName := fmt.Sprintf("tf-test-sink-%s", randomString(8)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccAccountAuditLogSinkPubSubConfig(sinkName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("temporalcloud_account_audit_log_sink.test", "sink_name", sinkName), + resource.TestCheckResourceAttr("temporalcloud_account_audit_log_sink.test", "enabled", "true"), + resource.TestCheckResourceAttr("temporalcloud_account_audit_log_sink.test", "pubsub.service_account_id", "test-sa"), + resource.TestCheckResourceAttr("temporalcloud_account_audit_log_sink.test", "pubsub.topic_name", "test-topic"), + resource.TestCheckResourceAttr("temporalcloud_account_audit_log_sink.test", "pubsub.gcp_project_id", "test-project"), + ), + }, + // ImportState testing + { + ResourceName: "temporalcloud_account_audit_log_sink.test", + ImportState: true, + ImportStateVerify: true, + }, + // Update testing + { + Config: testAccAccountAuditLogSinkPubSubConfigUpdate(sinkName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("temporalcloud_account_audit_log_sink.test", "enabled", "false"), + resource.TestCheckResourceAttr("temporalcloud_account_audit_log_sink.test", "pubsub.service_account_id", "test-updated-sa"), + resource.TestCheckResourceAttr("temporalcloud_account_audit_log_sink.test", "pubsub.topic_name", "test-updated-topic"), + resource.TestCheckResourceAttr("temporalcloud_account_audit_log_sink.test", "pubsub.gcp_project_id", "test-updated-project"), + ), + }, + // Delete testing + { + ResourceName: "temporalcloud_account_audit_log_sink.test", + ImportState: true, + ImportStateVerify: true, + Destroy: true, + }, + }, + }) +} + +func testAccAccountAuditLogSinkKinesisConfig(sinkName, sinkRegion string) string { + return fmt.Sprintf(` +provider "temporalcloud" { +} + +resource "temporalcloud_account_audit_log_sink" "test" { + sink_name = %[1]q + enabled = true + kinesis = { + role_name = "test-role" + destination_uri = "test-uri" + region = %[2]q + } +} +`, sinkName, sinkRegion) +} + +func testAccAccountAuditLogSinkKinesisConfigUpdate(sinkName, sinkRegion string) string { + return fmt.Sprintf(` +resource "temporalcloud_account_audit_log_sink" "test" { + sink_name = %[1]q + enabled = false + kinesis = { + role_name = "test-updated-role" + destination_uri = "test-updated-uri" + region = %[2]q + } +} +`, sinkName, sinkRegion) +} + +func testAccAccountAuditLogSinkPubSubConfig(sinkName string) string { + return fmt.Sprintf(` +provider "temporalcloud" { +} + +resource "temporalcloud_account_audit_log_sink" "test" { + sink_name = %[1]q + enabled = true + pubsub = { + service_account_id = "test-sa" + topic_name = "test-topic" + gcp_project_id = "test-project" + } +} +`, sinkName) +} + +func testAccAccountAuditLogSinkPubSubConfigUpdate(sinkName string) string { + return fmt.Sprintf(` +resource "temporalcloud_account_audit_log_sink" "test" { + sink_name = %[1]q + enabled = false + pubsub = { + service_account_id = "test-updated-sa" + topic_name = "test-updated-topic" + gcp_project_id = "test-updated-project" + } +} +`, sinkName) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index dcc0db7..553f294 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -179,6 +179,7 @@ func (p *TerraformCloudProvider) Resources(ctx context.Context) []func() resourc NewUserGroupMembersResource, NewGroupAccessResource, NewConnectivityRuleResource, + NewAccountAuditLogSinkResource, } } @@ -195,6 +196,7 @@ func (p *TerraformCloudProvider) DataSources(ctx context.Context) []func() datas NewNexusEndpointDataSource, NewNexusEndpointsDataSource, NewConnectivityRuleDataSource, + NewAccountAuditLogSinkDataSource, } } diff --git a/internal/types/sink.go b/internal/types/sink.go index b851fe4..41dc446 100644 --- a/internal/types/sink.go +++ b/internal/types/sink.go @@ -21,6 +21,18 @@ var ( "region": types.StringType, "service_account_email": types.StringType, } + + KinesisSpecModelAttrTypes = map[string]attr.Type{ + "role_name": types.StringType, + "destination_uri": types.StringType, + "region": types.StringType, + } + + PubSubSpecModelAttrTypes = map[string]attr.Type{ + "service_account_id": types.StringType, + "topic_name": types.StringType, + "gcp_project_id": types.StringType, + } ) type S3SpecModel struct { @@ -56,3 +68,25 @@ type GCSSpecModel struct { // The service account email associated with the GCS bucket and service account ServiceAccountEmail types.String `tfsdk:"service_account_email"` } + +type KinesisSpecModel struct { + // The IAM role that Temporal Cloud assumes for writing records to the customer's Kinesis stream + RoleName types.String `tfsdk:"role_name"` + + // The destination URI of the Kinesis stream where Temporal will send data + DestinationUri types.String `tfsdk:"destination_uri"` + + // The region of the Kinesis stream + Region types.String `tfsdk:"region"` +} + +type PubSubSpecModel struct { + // The customer service account ID that Temporal Cloud impersonates for writing records to the customer's PubSub topic + ServiceAccountId types.String `tfsdk:"service_account_id"` + + // The destination PubSub topic name for Temporal + TopicName types.String `tfsdk:"topic_name"` + + // The GCP project ID of the PubSub topic and service account + GcpProjectId types.String `tfsdk:"gcp_project_id"` +}