Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ testacc:
# Example: Run specific namespace export sink tests
.PHONY: test-namespace-export-sink
test-namespace-export-sink:
TF_ACC=1 go test ./internal/provider -run TestAccNamespaceExportSink_S3 -v $(TESTARGS) -timeout 120m
TF_ACC=1 go test ./internal/provider -run TestAccNamespaceExportSink_GCS -v $(TESTARGS) -timeout 120m

8 changes: 6 additions & 2 deletions docs/resources/namespace_export_sink.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,13 @@ Provisions a namespace export sink.
Required:

- `bucket_name` (String) The name of the destination GCS bucket where Temporal will send data.
- `gcp_project_id` (String) The GCP project ID associated with the GCS bucket and service account.
- `region` (String) The region of the gcs bucket
- `service_account_id` (String) The customer service account ID that Temporal Cloud impersonates for writing records to the customer's GCS bucket.

Optional:

- `gcp_project_id` (String) The GCP project ID associated with the GCS bucket and service account. If not provided, the service_account_email must be provided.
- `service_account_email` (String) The service account email associated with the GCS bucket and service account. If not provided, the service_account_id and gcp_project_id must be provided.
- `service_account_id` (String) The customer service account ID that Temporal Cloud impersonates for writing records to the customer's GCS bucket. If not provided, the service_account_email must be provided.


<a id="nestedatt--s3"></a>
Expand Down
69 changes: 59 additions & 10 deletions internal/provider/namespace_export_sink_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ package provider
import (
"context"
"fmt"
"regexp"
"strings"

"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-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
Expand Down Expand Up @@ -162,21 +164,34 @@ func (r *namespaceExportSinkResource) Schema(ctx context.Context, req resource.S
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 GCS bucket.",
Required: true,
Description: "The customer service account ID that Temporal Cloud impersonates for writing records to the customer's GCS bucket. If not provided, the service_account_email must be provided.",
Optional: true,
Computed: true,
},
"bucket_name": schema.StringAttribute{
Description: "The name of the destination GCS bucket where Temporal will send data.",
Required: true,
},
"gcp_project_id": schema.StringAttribute{
Description: "The GCP project ID associated with the GCS bucket and service account.",
Required: true,
Description: "The GCP project ID associated with the GCS bucket and service account. If not provided, the service_account_email must be provided.",
Optional: true,
Computed: true,
},
"region": schema.StringAttribute{
Description: "The region of the gcs bucket",
Required: true,
},
"service_account_email": schema.StringAttribute{
Description: "The service account email associated with the GCS bucket and service account. If not provided, the service_account_id and gcp_project_id must be provided.",
Optional: true,
Validators: []validator.String{
stringvalidator.RegexMatches(
regexp.MustCompile(`^(\S+)@(\S+).iam.gserviceaccount.com$`),
"Service account email must be in the format of '<sa>@<gcp_project>.iam.gserviceaccount.com' where <sa> is the service account ID and <gcp_project> is a valid GCP project ID",
),
},
Computed: true,
},
},
Validators: []validator.Object{
objectvalidator.ExactlyOneOf(path.Expressions{
Expand Down Expand Up @@ -271,12 +286,15 @@ func updateSinkModelFromSpec(ctx context.Context, state *namespaceExportSinkReso

gcsObj := types.ObjectNull(internaltypes.GcsSpecModelAttrTypes)
if sink.GetSpec().GetGcs() != nil {
saEmail := fmt.Sprintf("%s@%s.iam.gserviceaccount.com", sink.GetSpec().GetGcs().GetSaId(), sink.GetSpec().GetGcs().GetGcpProjectId())
gcsSpec := internaltypes.GCSSpecModel{
SaId: types.StringValue(sink.GetSpec().GetGcs().GetSaId()),
BucketName: types.StringValue(sink.GetSpec().GetGcs().GetBucketName()),
GcpProjectId: types.StringValue(sink.GetSpec().GetGcs().GetGcpProjectId()),
Region: types.StringValue(sink.GetSpec().GetGcs().GetRegion()),
SaId: types.StringValue(sink.GetSpec().GetGcs().GetSaId()),
BucketName: types.StringValue(sink.GetSpec().GetGcs().GetBucketName()),
GcpProjectId: types.StringValue(sink.GetSpec().GetGcs().GetGcpProjectId()),
Region: types.StringValue(sink.GetSpec().GetGcs().GetRegion()),
ServiceAccountEmail: types.StringValue(saEmail),
}

gcsObj, diags = types.ObjectValueFrom(ctx, internaltypes.GcsSpecModelAttrTypes, gcsSpec)
diags.Append(diags...)
if diags.HasError() {
Expand Down Expand Up @@ -361,6 +379,22 @@ func (r *namespaceExportSinkResource) ImportState(ctx context.Context, req resou
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}

func parseSAPrincipal(saPrincipal string) (string, string) {
var gcpProjectId, saId string
saPrincipalPattern := regexp.MustCompile(`^(\S+)@(\S+).iam.gserviceaccount.com$`)

submatch := saPrincipalPattern.FindStringSubmatch(saPrincipal)

if len(submatch) != 3 {
return "", ""
}

saId = submatch[1]
gcpProjectId = submatch[2]

return saId, gcpProjectId
}

func getSinkSpecFromModel(ctx context.Context, plan *namespaceExportSinkResourceModel) (*namespacev1.ExportSinkSpec, diag.Diagnostics) {
var diags diag.Diagnostics

Expand Down Expand Up @@ -395,13 +429,28 @@ func getSinkSpecFromModel(ctx context.Context, plan *namespaceExportSinkResource
return nil, diags
}

saId := gcsSpec.SaId.ValueString()
gcpProjectId := gcsSpec.GcpProjectId.ValueString()
if saId == "" && gcpProjectId == "" && gcsSpec.ServiceAccountEmail.ValueString() != "" {
saId, gcpProjectId = parseSAPrincipal(gcsSpec.ServiceAccountEmail.ValueString())

}

if saId == "" || gcpProjectId == "" {
diags.AddError(
"Missing Service Account Configuration",
"Either provide both service_account_id and gcp_project_id, or provide a valid service_account_email",
)
return nil, diags
}

return &namespacev1.ExportSinkSpec{
Name: plan.SinkName.ValueString(),
Enabled: plan.Enabled.ValueBool(),
Gcs: &sinkv1.GCSSpec{
SaId: gcsSpec.SaId.ValueString(),
SaId: saId,
BucketName: gcsSpec.BucketName.ValueString(),
GcpProjectId: gcsSpec.GcpProjectId.ValueString(),
GcpProjectId: gcpProjectId,
Region: gcsSpec.Region.ValueString(),
},
}, nil
Expand Down
133 changes: 98 additions & 35 deletions internal/provider/namespace_export_sink_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func TestNamespaceExportSinkResource_Schema(t *testing.T) {
}

func TestAccNamespaceExportSink_S3(t *testing.T) {

namespaceName := fmt.Sprintf("tf-test-ns-export-aws-%s", randomString(8))
sinkRegion := "us-east-1"
namespaceRegion := fmt.Sprintf("aws-%s", sinkRegion)
Expand Down Expand Up @@ -83,43 +84,75 @@ func TestAccNamespaceExportSink_S3(t *testing.T) {

func TestAccNamespaceExportSink_GCS(t *testing.T) {
namespaceName := fmt.Sprintf("tf-test-ns-export-gcp-%s", randomString(8))

sinkRegion := "us-central1"
namespaceRegion := fmt.Sprintf("gcp-%s", sinkRegion)

sinkName := fmt.Sprintf("tf-test-sink-%s", randomString(8))

creationGCSCheckFun := resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("temporalcloud_namespace_export_sink.test", "sink_name", sinkName),
resource.TestCheckResourceAttr("temporalcloud_namespace_export_sink.test", "enabled", "true"),
resource.TestCheckResourceAttr("temporalcloud_namespace_export_sink.test", "gcs.bucket_name", "test-bucket"),
resource.TestCheckResourceAttr("temporalcloud_namespace_export_sink.test", "gcs.region", sinkRegion),
resource.TestCheckResourceAttr("temporalcloud_namespace_export_sink.test", "gcs.service_account_id", "test-sa"),
resource.TestCheckResourceAttr("temporalcloud_namespace_export_sink.test", "gcs.gcp_project_id", "test-project"),
)

updateGCSCheckFun := resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("temporalcloud_namespace_export_sink.test", "enabled", "false"),
resource.TestCheckResourceAttr("temporalcloud_namespace_export_sink.test", "gcs.bucket_name", "updated-bucket"),
resource.TestCheckResourceAttr("temporalcloud_namespace_export_sink.test", "gcs.region", sinkRegion),
resource.TestCheckResourceAttr("temporalcloud_namespace_export_sink.test", "gcs.service_account_id", "test-updated-sa"),
resource.TestCheckResourceAttr("temporalcloud_namespace_export_sink.test", "gcs.gcp_project_id", "test-updated-project"),
)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and Read testing
{
Config: testAccNamespaceExportSinkGCSConfig(namespaceName, namespaceRegion, sinkName, sinkRegion),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("temporalcloud_namespace_export_sink.test", "sink_name", sinkName),
resource.TestCheckResourceAttr("temporalcloud_namespace_export_sink.test", "enabled", "true"),
resource.TestCheckResourceAttr("temporalcloud_namespace_export_sink.test", "gcs.bucket_name", "test-bucket"),
resource.TestCheckResourceAttr("temporalcloud_namespace_export_sink.test", "gcs.region", sinkRegion),
resource.TestCheckResourceAttr("temporalcloud_namespace_export_sink.test", "gcs.service_account_id", "test-sa"),
resource.TestCheckResourceAttr("temporalcloud_namespace_export_sink.test", "gcs.gcp_project_id", "test-project"),
),
Config: testAccNamespaceExportSinkGCSConfig(namespaceName, namespaceRegion, sinkName, sinkRegion, false),
Check: creationGCSCheckFun,
},
// ImportState testing
{
ResourceName: "temporalcloud_namespace_export_sink.test",
ImportState: true,
ImportStateVerify: true,
},
// Update testing
// Update with SA email
{
Config: testAccNamespaceExportSinkGCSConfigUpdate(namespaceName, namespaceRegion, sinkName, sinkRegion),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("temporalcloud_namespace_export_sink.test", "enabled", "false"),
resource.TestCheckResourceAttr("temporalcloud_namespace_export_sink.test", "gcs.bucket_name", "updated-bucket"),
resource.TestCheckResourceAttr("temporalcloud_namespace_export_sink.test", "gcs.region", sinkRegion),
resource.TestCheckResourceAttr("temporalcloud_namespace_export_sink.test", "gcs.service_account_id", "test-updated-sa"),
resource.TestCheckResourceAttr("temporalcloud_namespace_export_sink.test", "gcs.gcp_project_id", "test-updated-project"),
),
Config: testAccNamespaceExportSinkGCSConfigUpdate(namespaceName, namespaceRegion, sinkName, sinkRegion, true),
Check: updateGCSCheckFun,
},
// Delete testing
{
ResourceName: "temporalcloud_namespace_export_sink.test",
ImportState: true,
ImportStateVerify: true,
Destroy: true,
},
// Create with SA email
{
Config: testAccNamespaceExportSinkGCSConfig(namespaceName, namespaceRegion, sinkName, sinkRegion, true),
Check: creationGCSCheckFun,
},
// Update with not SA email
{
Config: testAccNamespaceExportSinkGCSConfigUpdate(namespaceName, namespaceRegion, sinkName, sinkRegion, false),
Check: updateGCSCheckFun,
},
// ImportState testing
{
ResourceName: "temporalcloud_namespace_export_sink.test",
ImportState: true,
ImportStateVerify: true,
},
// Update with SA email
{
Config: testAccNamespaceExportSinkGCSConfigUpdate(namespaceName, namespaceRegion, sinkName, sinkRegion, true),
Check: updateGCSCheckFun,
},
// Delete testing
{
Expand Down Expand Up @@ -184,7 +217,27 @@ resource "temporalcloud_namespace_export_sink" "test" {
`, namespaceName, namespaceRegion, sinkName, sinkRegion)
}

func testAccNamespaceExportSinkGCSConfig(namespaceName, namespaceRegion, sinkName, sinkRegion string) string {
func testAccNamespaceExportSinkGCSConfig(namespaceName, namespaceRegion, sinkName, sinkRegion string, isSAEmail bool) string {
var export_config string
if !isSAEmail {
export_config = fmt.Sprintf(`
gcs = {
bucket_name = "test-bucket"
region = %[1]q
service_account_id = "test-sa"
gcp_project_id = "test-project"
}
`, sinkRegion)
} else {
export_config = fmt.Sprintf(`
gcs = {
bucket_name = "test-bucket"
region = %[1]q
service_account_email = "[email protected]"
}
`, sinkRegion)
}

return fmt.Sprintf(`
provider "temporalcloud" {

Expand All @@ -201,17 +254,32 @@ resource "temporalcloud_namespace_export_sink" "test" {
namespace = temporalcloud_namespace.terraform.id
sink_name = %[3]q
enabled = true
gcs = {
bucket_name = "test-bucket"
region = %[4]q
service_account_id = "test-sa"
gcp_project_id = "test-project"
}
%[4]s
}
`, namespaceName, namespaceRegion, sinkName, sinkRegion)
`, namespaceName, namespaceRegion, sinkName, export_config)
}

func testAccNamespaceExportSinkGCSConfigUpdate(namespaceName, namespaceRegion, sinkName, sinkRegion string) string {
func testAccNamespaceExportSinkGCSConfigUpdate(namespaceName, namespaceRegion, sinkName, sinkRegion string, isSAEmail bool) string {
var export_config string
if !isSAEmail {
export_config = fmt.Sprintf(`
gcs = {
bucket_name = "updated-bucket"
region = %[1]q
service_account_id = "test-updated-sa"
gcp_project_id = "test-updated-project"
}
`, sinkRegion)
} else {
export_config = fmt.Sprintf(`
gcs = {
bucket_name = "updated-bucket"
region = %[1]q
service_account_email = "[email protected]"
}
`, sinkRegion)
}

return fmt.Sprintf(`
resource "temporalcloud_namespace" "terraform" {
name = %[1]q
Expand All @@ -223,12 +291,7 @@ resource "temporalcloud_namespace_export_sink" "test" {
namespace = temporalcloud_namespace.terraform.id
sink_name = %[3]q
enabled = false
gcs = {
bucket_name = "updated-bucket"
region = %[4]q
service_account_id = "test-updated-sa"
gcp_project_id = "test-updated-project"
}
%[4]s
}
`, namespaceName, namespaceRegion, sinkName, sinkRegion)
`, namespaceName, namespaceRegion, sinkName, export_config)
}
12 changes: 8 additions & 4 deletions internal/types/sink.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ var (
}

GcsSpecModelAttrTypes = map[string]attr.Type{
"service_account_id": types.StringType,
"bucket_name": types.StringType,
"gcp_project_id": types.StringType,
"region": types.StringType,
"service_account_id": types.StringType,
"bucket_name": types.StringType,
"gcp_project_id": types.StringType,
"region": types.StringType,
"service_account_email": types.StringType,
}
)

Expand Down Expand Up @@ -51,4 +52,7 @@ type GCSSpecModel struct {

// The region of the gcs bucket
Region types.String `tfsdk:"region"`

// The service account email associated with the GCS bucket and service account
ServiceAccountEmail types.String `tfsdk:"service_account_email"`
}
Loading