diff --git a/docs/resources/destination_subscription.md b/docs/resources/destination_subscription.md index 68d44e7..68e701b 100644 --- a/docs/resources/destination_subscription.md +++ b/docs/resources/destination_subscription.md @@ -74,8 +74,20 @@ resource "segment_destination_subscription" "send_to_webhook" { ### Optional - `model_id` (String) The unique identifier for the linked ReverseETLModel, if this part of a Reverse ETL connection. +- `reverse_etl_schedule` (Attributes) (Reverse ETL only) The schedule for the subscription being attached to ReverseETL model. (see [below for nested schema](#nestedatt--reverse_etl_schedule)) ### Read-Only - `action_slug` (String) The URL-friendly key for the associated Destination action. - `id` (String) The unique identifier for the subscription. + + +### Nested Schema for `reverse_etl_schedule` + +Required: + +- `strategy` (String) Strategy supports three modes: PERIODIC, SPECIFIC_DAYS, or MANUAL. + +Optional: + +- `config` (String) Configures the schedule for the subscription. diff --git a/docs/resources/reverse_etl_model.md b/docs/resources/reverse_etl_model.md index 6414e8a..f8c5170 100644 --- a/docs/resources/reverse_etl_model.md +++ b/docs/resources/reverse_etl_model.md @@ -68,10 +68,13 @@ resource "segment_reverse_etl_model" "example" { - `name` (String) A short, human-readable description of the Model. - `query` (String) The SQL query that will be executed to extract data from the connected Source. - `query_identifier_column` (String) Indicates the column named in `query` that should be used to uniquely identify the extracted records. -- `schedule_config` (String) Depending on the chosen strategy, configures the schedule for this model. -- `schedule_strategy` (String) Determines the strategy used for triggering syncs, which will be used in conjunction with scheduleConfig. - `source_id` (String) Indicates which Source to attach this model to. +### Optional + +- `schedule_config` (String, Deprecated) Depending on the chosen strategy, configures the schedule for this model. +- `schedule_strategy` (String, Deprecated) Determines the strategy used for triggering syncs, which will be used in conjunction with scheduleConfig. + ### Read-Only - `id` (String) The unique identifier for the model. diff --git a/go.mod b/go.mod index 9e57f4d..57d4cee 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/hashicorp/terraform-plugin-framework-validators v0.13.0 github.com/hashicorp/terraform-plugin-go v0.24.0 github.com/hashicorp/terraform-plugin-testing v1.10.0 - github.com/segmentio/public-api-sdk-go v0.0.0-20240909200753-311bb8d791a2 + github.com/segmentio/public-api-sdk-go v0.0.0-20241025180535-501a23c07559 gotest.tools/gotestsum v1.12.0 ) diff --git a/go.sum b/go.sum index 5eaeb33..09f217f 100644 --- a/go.sum +++ b/go.sum @@ -187,6 +187,10 @@ github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3V github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/segmentio/public-api-sdk-go v0.0.0-20240909200753-311bb8d791a2 h1:vlKTelJ32DPBuiiSx2PJaxN9jJd3OFK2avHU/XR/qB8= github.com/segmentio/public-api-sdk-go v0.0.0-20240909200753-311bb8d791a2/go.mod h1:yKkoPfcOkkYjiZQj4lRWxji0Qwc6ncNEf7wCfywochY= +github.com/segmentio/public-api-sdk-go v0.0.0-20241017001201-fbbdab459db8 h1:pYJu97HA0FVdy+WCqQbS/baDzXxg2q0Vl+akD6SUBSI= +github.com/segmentio/public-api-sdk-go v0.0.0-20241017001201-fbbdab459db8/go.mod h1:yKkoPfcOkkYjiZQj4lRWxji0Qwc6ncNEf7wCfywochY= +github.com/segmentio/public-api-sdk-go v0.0.0-20241025180535-501a23c07559 h1:6jgXPksz5bEJUMbhp4biSIViye/Os2yfAOx9yy44e1g= +github.com/segmentio/public-api-sdk-go v0.0.0-20241025180535-501a23c07559/go.mod h1:yKkoPfcOkkYjiZQj4lRWxji0Qwc6ncNEf7wCfywochY= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= diff --git a/internal/provider/destination_subscription_resource.go b/internal/provider/destination_subscription_resource.go index 4f8aadc..4963cb6 100644 --- a/internal/provider/destination_subscription_resource.go +++ b/internal/provider/destination_subscription_resource.go @@ -2,6 +2,7 @@ package provider import ( "context" + "encoding/json" "fmt" "strings" @@ -9,11 +10,13 @@ import ( "github.com/segmentio/terraform-provider-segment/internal/provider/models" "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" + "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/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/segmentio/public-api-sdk-go/api" ) @@ -91,12 +94,27 @@ func (r *destinationSubscriptionResource) Schema(_ context.Context, _ resource.S Description: `The customer settings for action fields. Only settings included in the configuration will be managed by Terraform.`, CustomType: jsontypes.NormalizedType{}, }, + "reverse_etl_schedule": schema.SingleNestedAttribute{ + Optional: true, + Description: "(Reverse ETL only) The schedule for the subscription being attached to ReverseETL model.", + Attributes: map[string]schema.Attribute{ + "strategy": schema.StringAttribute{ + Required: true, + Description: "Strategy supports three modes: PERIODIC, SPECIFIC_DAYS, or MANUAL.", + }, + "config": schema.StringAttribute{ + Optional: true, + Description: "Configures the schedule for the subscription.", + CustomType: jsontypes.NormalizedType{}, + }, + }, + }, }, } } func (r *destinationSubscriptionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var plan models.DestinationSubscriptionState + var plan models.DestinationSubscriptionPlan diags := req.Plan.Get(ctx, &plan) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -110,6 +128,15 @@ func (r *destinationSubscriptionResource) Create(ctx context.Context, req resour return } + if !plan.ModelID.IsNull() && !plan.ModelID.IsUnknown() && (plan.ReverseETLSchedule.IsNull() || plan.ReverseETLSchedule.IsUnknown()) { + resp.Diagnostics.AddError( + "Reverse ETL model ID provided without reverse ETL schedule", + "Reverse ETL model ID must be provided with a reverse ETL schedule", + ) + + return + } + out, body, err := r.client.DestinationsAPI.CreateDestinationSubscription(r.authContext, plan.DestinationID.ValueString()).CreateDestinationSubscriptionAlphaInput(api.CreateDestinationSubscriptionAlphaInput{ Name: plan.Name.ValueString(), ActionId: plan.ActionID.ValueString(), @@ -130,9 +157,39 @@ func (r *destinationSubscriptionResource) Create(ctx context.Context, req resour return } - destinationSubscription := out.Data.GetDestinationSubscription() + resp.State.SetAttribute(ctx, path.Root("id"), out.Data.DestinationSubscription.Id) + resp.State.SetAttribute(ctx, path.Root("destination_id"), out.Data.DestinationSubscription.DestinationId) + + reverseETLSchedule, diags := getSchedule(ctx, plan.ReverseETLSchedule) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + + return + } + + updateOut, body, err := r.client.DestinationsAPI.UpdateSubscriptionForDestination(r.authContext, plan.DestinationID.ValueString(), out.Data.DestinationSubscription.Id).UpdateSubscriptionForDestinationAlphaInput(api.UpdateSubscriptionForDestinationAlphaInput{ + Input: api.DestinationSubscriptionUpdateInput{ + Name: plan.Name.ValueStringPointer(), + Trigger: plan.Trigger.ValueStringPointer(), + Enabled: plan.Enabled.ValueBoolPointer(), + Settings: settings, + ReverseETLModelId: plan.ModelID.ValueStringPointer(), + ReverseETLSchedule: reverseETLSchedule, + }, + }).Execute() + if body != nil { + defer body.Body.Close() + } + if err != nil { + resp.Diagnostics.AddError( + "Unable to update Destination subscription", + getError(err, body), + ) + + return + } - resp.State.SetAttribute(ctx, path.Root("id"), destinationSubscription.Id) + destinationSubscription := updateOut.Data.Subscription var state models.DestinationSubscriptionState err = state.Fill(destinationSubscription) @@ -203,7 +260,7 @@ func (r *destinationSubscriptionResource) Read(ctx context.Context, req resource } func (r *destinationSubscriptionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var plan models.DestinationSubscriptionState + var plan models.DestinationSubscriptionPlan diags := req.Plan.Get(ctx, &plan) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -224,12 +281,30 @@ func (r *destinationSubscriptionResource) Update(ctx context.Context, req resour return } + if !plan.ModelID.IsNull() && !plan.ModelID.IsUnknown() && (plan.ReverseETLSchedule.IsNull() || plan.ReverseETLSchedule.IsUnknown()) { + resp.Diagnostics.AddError( + "Reverse ETL model ID provided without reverse ETL schedule", + "Reverse ETL model ID must be provided with a reverse ETL schedule", + ) + + return + } + + reverseETLSchedule, diags := getSchedule(ctx, plan.ReverseETLSchedule) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + + return + } + out, body, err := r.client.DestinationsAPI.UpdateSubscriptionForDestination(r.authContext, state.DestinationID.ValueString(), state.ID.ValueString()).UpdateSubscriptionForDestinationAlphaInput(api.UpdateSubscriptionForDestinationAlphaInput{ Input: api.DestinationSubscriptionUpdateInput{ - Name: plan.Name.ValueStringPointer(), - Trigger: plan.Trigger.ValueStringPointer(), - Enabled: plan.Enabled.ValueBoolPointer(), - Settings: settings, + Name: plan.Name.ValueStringPointer(), + Trigger: plan.Trigger.ValueStringPointer(), + Enabled: plan.Enabled.ValueBoolPointer(), + Settings: settings, + ReverseETLModelId: plan.ModelID.ValueStringPointer(), + ReverseETLSchedule: reverseETLSchedule, }, }).Execute() if body != nil { @@ -320,3 +395,115 @@ func (r *destinationSubscriptionResource) Configure(_ context.Context, req resou r.client = config.client r.authContext = config.authContext } + +func getSchedule(ctx context.Context, planSchedule basetypes.ObjectValue) (*api.ReverseEtlScheduleDefinition, diag.Diagnostics) { + var reverseETLSchedule *api.ReverseEtlScheduleDefinition + var diags diag.Diagnostics + if !planSchedule.IsNull() && !planSchedule.IsUnknown() { + reverseETLSchedule = &api.ReverseEtlScheduleDefinition{} + + wrappedReverseETLModelScheduleStrategy, err := planSchedule.Attributes()["strategy"].ToTerraformValue(ctx) + if err != nil { + diags.AddError( + "Unable to decode reverse ETL schedule strategy", + err.Error(), + ) + + return nil, diags + } + + var reverseETLModelScheduleStrategy string + err = wrappedReverseETLModelScheduleStrategy.As(&reverseETLModelScheduleStrategy) + if err != nil { + diags.AddError( + "Unable to decode reverse ETL schedule strategy", + err.Error(), + ) + + return nil, diags + } + + reverseETLSchedule.Strategy = reverseETLModelScheduleStrategy + + wrappedReverseETLModelScheduleConfig, err := planSchedule.Attributes()["config"].ToTerraformValue(ctx) + if err != nil { + diags.AddError( + "Unable to decode reverse ETL schedule config", + err.Error(), + ) + + return nil, diags + } + + if !wrappedReverseETLModelScheduleConfig.IsNull() && wrappedReverseETLModelScheduleConfig.IsKnown() { + if reverseETLSchedule.Strategy == "PERIODIC" { + reverseETLModelScheduleConfig := api.ReverseEtlPeriodicScheduleConfig{} + var config string + err = wrappedReverseETLModelScheduleConfig.As(&config) + if err != nil { + diags.AddError( + "Unable to decode reverse ETL schedule config", + err.Error(), + ) + + return nil, diags + } + + err = json.Unmarshal([]byte(config), &reverseETLModelScheduleConfig) + if err != nil { + diags.AddError( + "Unable to decode reverse ETL schedule config", + err.Error(), + ) + + return nil, diags + } + + reverseETLSchedule.Config = *api.NewNullableConfig(&api.Config{ + ReverseEtlPeriodicScheduleConfig: &reverseETLModelScheduleConfig, + }) + } else if reverseETLSchedule.Strategy == "SPECIFIC_DAYS" { + reverseETLModelScheduleConfig := api.ReverseEtlSpecificTimeScheduleConfig{} + var config string + err = wrappedReverseETLModelScheduleConfig.As(&config) + if err != nil { + diags.AddError( + "Unable to decode reverse ETL schedule config", + err.Error(), + ) + + return nil, diags + } + + err = json.Unmarshal([]byte(config), &reverseETLModelScheduleConfig) + if err != nil { + diags.AddError( + "Unable to decode reverse ETL schedule config", + err.Error(), + ) + + return nil, diags + } + + reverseETLSchedule.Config = *api.NewNullableConfig(&api.Config{ + ReverseEtlSpecificTimeScheduleConfig: &reverseETLModelScheduleConfig, + }) + } else if reverseETLSchedule.Strategy == "MANUAL" { + diags.AddError( + "Manual reverse ETL schedule strategy does not require a config", + "Manual reverse ETL schedule strategy does not require a config", + ) + reverseETLSchedule.Config = *api.NewNullableConfig(nil) + } else { + diags.AddError( + "Unsupported reverse ETL schedule strategy", + fmt.Sprintf("Strategy %q is not supported", reverseETLSchedule.Strategy), + ) + + return nil, diags + } + } + } + + return reverseETLSchedule, diags +} diff --git a/internal/provider/destination_subscription_resource_test.go b/internal/provider/destination_subscription_resource_test.go index 2abd2f8..abc3244 100644 --- a/internal/provider/destination_subscription_resource_test.go +++ b/internal/provider/destination_subscription_resource_test.go @@ -36,7 +36,27 @@ func TestAccDestinationSubscriptionResource(t *testing.T) { } ` } else if req.URL.Path == "/destinations/my-destination-id/subscriptions/my-subscription-id" && req.Method == http.MethodPatch { - payload = ` + // First update is to set the model id + if updated < 1 { + payload = ` + { + "data": { + "subscription": { + "id": "my-subscription-id", + "name": "My subscription name", + "actionId": "my-action-id", + "actionSlug": "my-action-slug", + "destinationId": "my-destination-id", + "modelId": "", + "enabled": true, + "trigger": "type = \"track\"", + "settings": {} + } + } + } + ` + } else { + payload = ` { "data": { "subscription": { @@ -55,9 +75,12 @@ func TestAccDestinationSubscriptionResource(t *testing.T) { } } ` + } + updated++ } else if req.URL.Path == "/destinations/my-destination-id/subscriptions/my-subscription-id" && req.Method == http.MethodGet { - if updated == 0 { + // First update is to set the model id + if updated <= 1 { payload = ` { "data": { @@ -183,3 +206,236 @@ func TestAccDestinationSubscriptionResource(t *testing.T) { }, }) } + +func TestAccDestinationSubscriptionResourceWithModel(t *testing.T) { + t.Parallel() + + updated := 0 + fakeServer := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("content-type", "application/json") + + payload := "" + if req.URL.Path == "/destinations/my-destination-id/subscriptions" && req.Method == http.MethodPost { + payload = ` + { + "data": { + "destinationSubscription": { + "id": "my-subscription-id", + "name": "My subscription name", + "actionId": "my-action-id", + "actionSlug": "my-action-slug", + "destinationId": "my-destination-id", + "modelId": "my-model-id", + "reverseETLSchedule": { + "config": { "interval": "1d" }, + "strategy": "PERIODIC" + }, + "enabled": true, + "trigger": "type = \"track\"", + "settings": {} + } + } + } + ` + } else if req.URL.Path == "/destinations/my-destination-id/subscriptions/my-subscription-id" && req.Method == http.MethodPatch { + if updated < 1 { + payload = ` + { + "data": { + "subscription": { + "id": "my-subscription-id", + "name": "My subscription name", + "actionId": "my-action-id", + "actionSlug": "my-action-slug", + "destinationId": "my-destination-id", + "modelId": "my-model-id", + "reverseETLSchedule": { + "config": { "interval": "1d" }, + "strategy": "PERIODIC" + }, "enabled": true, + "trigger": "type = \"track\"", + "settings": {} + } + } + } + ` + } else { + payload = ` + { + "data": { + "subscription": { + "id": "my-subscription-id", + "name": "My new subscription name", + "actionId": "my-action-id", + "actionSlug": "my-action-slug", + "destinationId": "my-destination-id", + "modelId": "my-model-id", + "reverseETLSchedule": { + "config": { "interval": "1d" }, + "strategy": "PERIODIC" + }, "enabled": false, + "trigger": "type = \"track\"", + "settings": { + "test": "test" + } + } + } + } + ` + } + + updated++ + } else if req.URL.Path == "/destinations/my-destination-id/subscriptions/my-subscription-id" && req.Method == http.MethodGet { + if updated <= 1 { + payload = ` + { + "data": { + "subscription": { + "id": "my-subscription-id", + "name": "My subscription name", + "actionId": "my-action-id", + "actionSlug": "my-action-slug", + "destinationId": "my-destination-id", + "modelId": "my-model-id", + "reverseETLSchedule": { + "config": { "interval": "1d" }, + "strategy": "PERIODIC" + }, "enabled": true, + "trigger": "type = \"track\"", + "settings": {} + } + } + } + ` + } else { + payload = ` + { + "data": { + "subscription": { + "id": "my-subscription-id", + "name": "My new subscription name", + "actionId": "my-action-id", + "actionSlug": "my-action-slug", + "destinationId": "my-destination-id", + "modelId": "my-model-id", + "reverseETLSchedule": { + "config": { "interval": "1d" }, + "strategy": "PERIODIC" + }, "enabled": false, + "trigger": "type = \"track\"", + "settings": { + "test": "test" + } + } + } + } + ` + } + } + + _, _ = w.Write([]byte(payload)) + }), + ) + defer fakeServer.Close() + + providerConfig := ` + provider "segment" { + url = "` + fakeServer.URL + `" + token = "abc123" + } + ` + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: providerConfig + ` + resource "segment_destination_subscription" "test" { + destination_id = "my-destination-id" + name = "My subscription name" + enabled = true + action_id = "my-action-id" + trigger = "type = \"track\"" + settings = jsonencode({}) + model_id = "my-model-id" + reverse_etl_schedule = { + config = jsonencode({ interval = "1d" }), + strategy = "PERIODIC" + } + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("segment_destination_subscription.test", "id", "my-subscription-id"), + resource.TestCheckResourceAttr("segment_destination_subscription.test", "destination_id", "my-destination-id"), + resource.TestCheckResourceAttr("segment_destination_subscription.test", "name", "My subscription name"), + resource.TestCheckResourceAttr("segment_destination_subscription.test", "enabled", "true"), + resource.TestCheckResourceAttr("segment_destination_subscription.test", "action_id", "my-action-id"), + resource.TestCheckResourceAttr("segment_destination_subscription.test", "action_slug", "my-action-slug"), + resource.TestCheckResourceAttr("segment_destination_subscription.test", "trigger", "type = \"track\""), + resource.TestCheckResourceAttr("segment_destination_subscription.test", "model_id", "my-model-id"), + resource.TestCheckResourceAttr("segment_destination_subscription.test", "settings", "{}"), + resource.TestCheckResourceAttr("segment_destination_subscription.test", "reverse_etl_schedule.strategy", "PERIODIC"), + resource.TestCheckResourceAttr("segment_destination_subscription.test", "reverse_etl_schedule.config", "{\"interval\":\"1d\"}"), + ), + }, + // ImportState testing + { + ResourceName: "segment_destination_subscription.test", + Config: providerConfig + ` + resource "segment_destination_subscription" "test" { + destination_id = "my-destination-id" + name = "My subscription name" + enabled = true + action_id = "my-action-id" + trigger = "type = \"track\"" + settings = jsonencode({}) + model_id = "my-model-id" + reverse_etl_schedule = { + config = jsonencode({ interval = "1d" }), + strategy = "PERIODIC" + } + } + `, + ImportState: true, + ImportStateVerify: true, + ImportStateId: "my-destination-id:my-subscription-id", + }, + // Update and Read testing + { + Config: providerConfig + ` + resource "segment_destination_subscription" "test" { + destination_id = "my-destination-id" + name = "My new subscription name" + enabled = false + action_id = "my-action-id" + trigger = "type = \"track\"" + settings = jsonencode({ + "test": "test" + }) + model_id = "my-model-id" + reverse_etl_schedule = { + config = jsonencode({ interval = "1d" }), + strategy = "PERIODIC" + } + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("segment_destination_subscription.test", "id", "my-subscription-id"), + resource.TestCheckResourceAttr("segment_destination_subscription.test", "destination_id", "my-destination-id"), + resource.TestCheckResourceAttr("segment_destination_subscription.test", "name", "My new subscription name"), + resource.TestCheckResourceAttr("segment_destination_subscription.test", "enabled", "false"), + resource.TestCheckResourceAttr("segment_destination_subscription.test", "action_id", "my-action-id"), + resource.TestCheckResourceAttr("segment_destination_subscription.test", "action_slug", "my-action-slug"), + resource.TestCheckResourceAttr("segment_destination_subscription.test", "trigger", "type = \"track\""), + resource.TestCheckResourceAttr("segment_destination_subscription.test", "settings", "{\"test\":\"test\"}"), + resource.TestCheckResourceAttr("segment_destination_subscription.test", "model_id", "my-model-id"), + resource.TestCheckResourceAttr("segment_destination_subscription.test", "reverse_etl_schedule.strategy", "PERIODIC"), + resource.TestCheckResourceAttr("segment_destination_subscription.test", "reverse_etl_schedule.config", "{\"interval\":\"1d\"}"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} diff --git a/internal/provider/models/destination_subscription.go b/internal/provider/models/destination_subscription.go index ed35be9..0d1e56d 100644 --- a/internal/provider/models/destination_subscription.go +++ b/internal/provider/models/destination_subscription.go @@ -1,21 +1,42 @@ package models import ( + "fmt" + "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/segmentio/public-api-sdk-go/api" ) type DestinationSubscriptionState struct { - ID types.String `tfsdk:"id"` - DestinationID types.String `tfsdk:"destination_id"` - Name types.String `tfsdk:"name"` - Enabled types.Bool `tfsdk:"enabled"` - ActionID types.String `tfsdk:"action_id"` - ActionSlug types.String `tfsdk:"action_slug"` - Trigger types.String `tfsdk:"trigger"` - ModelID types.String `tfsdk:"model_id"` - Settings jsontypes.Normalized `tfsdk:"settings"` + ID types.String `tfsdk:"id"` + DestinationID types.String `tfsdk:"destination_id"` + Name types.String `tfsdk:"name"` + Enabled types.Bool `tfsdk:"enabled"` + ActionID types.String `tfsdk:"action_id"` + ActionSlug types.String `tfsdk:"action_slug"` + Trigger types.String `tfsdk:"trigger"` + ModelID types.String `tfsdk:"model_id"` + Settings jsontypes.Normalized `tfsdk:"settings"` + ReverseETLSchedule *ReverseETLScheduleState `tfsdk:"reverse_etl_schedule"` +} + +type DestinationSubscriptionPlan struct { + ID types.String `tfsdk:"id"` + DestinationID types.String `tfsdk:"destination_id"` + Name types.String `tfsdk:"name"` + Enabled types.Bool `tfsdk:"enabled"` + ActionID types.String `tfsdk:"action_id"` + ActionSlug types.String `tfsdk:"action_slug"` + Trigger types.String `tfsdk:"trigger"` + ModelID types.String `tfsdk:"model_id"` + Settings jsontypes.Normalized `tfsdk:"settings"` + ReverseETLSchedule types.Object `tfsdk:"reverse_etl_schedule"` +} + +type ReverseETLScheduleState struct { + Strategy types.String `tfsdk:"strategy"` + Config jsontypes.Normalized `tfsdk:"config"` } func (d *DestinationSubscriptionState) Fill(subscription api.DestinationSubscription) error { @@ -35,6 +56,35 @@ func (d *DestinationSubscriptionState) Fill(subscription api.DestinationSubscrip return err } d.Settings = settings + schedule, err := getReverseETLSchedule(subscription.ReverseETLSchedule) + if err != nil { + return err + } + d.ReverseETLSchedule = schedule return nil } + +func getReverseETLSchedule(schedule *api.ReverseEtlScheduleDefinition) (*ReverseETLScheduleState, error) { + if schedule == nil { + return nil, nil + } + + var config *string + if schedule.Config.IsSet() { + byteConfig, err := schedule.Config.MarshalJSON() + if err != nil { + return nil, fmt.Errorf("failed to marshal reverse ETL schedule config: %w", err) + } + stringConfig := string(byteConfig) + + if stringConfig != "null" { + config = &stringConfig + } + } + + return &ReverseETLScheduleState{ + Strategy: types.StringValue(schedule.Strategy), + Config: jsontypes.NewNormalizedPointerValue(config), + }, nil +} diff --git a/internal/provider/reverse_etl_model_resource.go b/internal/provider/reverse_etl_model_resource.go index 8b3bf7e..f47784e 100644 --- a/internal/provider/reverse_etl_model_resource.go +++ b/internal/provider/reverse_etl_model_resource.go @@ -68,8 +68,9 @@ func (r *reverseETLModelResource) Schema(_ context.Context, _ resource.SchemaReq Description: "Indicates whether the Model should have syncs enabled. When disabled, no syncs will be triggered, regardless of the enabled status of the attached destinations/subscriptions.", }, "schedule_strategy": schema.StringAttribute{ - Required: true, - Description: "Determines the strategy used for triggering syncs, which will be used in conjunction with scheduleConfig.", + Optional: true, + DeprecationMessage: "Remove this attribute's configuration as it no longer is used and the attribute will be removed in the next major version of the provider. Please use `reverse_etl_schedule` in the destination_subscription resource instead.", + Description: "Determines the strategy used for triggering syncs, which will be used in conjunction with scheduleConfig.", }, "query": schema.StringAttribute{ Required: true, @@ -80,9 +81,10 @@ func (r *reverseETLModelResource) Schema(_ context.Context, _ resource.SchemaReq Description: "Indicates the column named in `query` that should be used to uniquely identify the extracted records.", }, "schedule_config": schema.StringAttribute{ - Required: true, - Description: "Depending on the chosen strategy, configures the schedule for this model.", - CustomType: jsontypes.NormalizedType{}, + Optional: true, + DeprecationMessage: "Remove this attribute's configuration as it no longer is used and the attribute will be removed in the next major version of the provider. Please use `reverse_etl_schedule` in the destination_subscription resource instead.", + Description: "Depending on the chosen strategy, configures the schedule for this model.", + CustomType: jsontypes.NormalizedType{}, }, }, } @@ -96,22 +98,13 @@ func (r *reverseETLModelResource) Create(ctx context.Context, req resource.Creat return } - var scheduleConfig map[string]interface{} - diags = plan.ScheduleConfig.Unmarshal(&scheduleConfig) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - out, body, err := r.client.ReverseETLAPI.CreateReverseEtlModel(r.authContext).CreateReverseEtlModelInput(api.CreateReverseEtlModelInput{ Name: plan.Name.ValueString(), SourceId: plan.SourceID.ValueString(), Description: plan.Description.ValueString(), Enabled: plan.Enabled.ValueBool(), - ScheduleStrategy: plan.ScheduleStrategy.ValueString(), Query: plan.Query.ValueString(), QueryIdentifierColumn: plan.QueryIdentifierColumn.ValueString(), - ScheduleConfig: scheduleConfig, }).Execute() if body != nil { defer body.Body.Close() @@ -146,6 +139,10 @@ func (r *reverseETLModelResource) Create(ctx context.Context, req resource.Creat if resp.Diagnostics.HasError() { return } + + // Since we deprecated these values, we just need to set them to the plan values so there are no errors + resp.State.SetAttribute(ctx, path.Root("schedule_config"), plan.ScheduleConfig) + resp.State.SetAttribute(ctx, path.Root("schedule_strategy"), plan.ScheduleStrategy) } func (r *reverseETLModelResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { @@ -188,6 +185,14 @@ func (r *reverseETLModelResource) Read(ctx context.Context, req resource.ReadReq if resp.Diagnostics.HasError() { return } + + // Since we deprecated these values, we just need to set them to the plan values so there are no errors + if !previousState.ScheduleConfig.IsNull() && !previousState.ScheduleConfig.IsUnknown() { + resp.State.SetAttribute(ctx, path.Root("schedule_config"), previousState.ScheduleConfig) + } + if !previousState.ScheduleStrategy.IsNull() && !previousState.ScheduleStrategy.IsUnknown() { + resp.State.SetAttribute(ctx, path.Root("schedule_strategy"), previousState.ScheduleStrategy) + } } func (r *reverseETLModelResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { @@ -205,19 +210,10 @@ func (r *reverseETLModelResource) Update(ctx context.Context, req resource.Updat return } - var scheduleConfig map[string]interface{} - diags = plan.ScheduleConfig.Unmarshal(&scheduleConfig) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - out, body, err := r.client.ReverseETLAPI.UpdateReverseEtlModel(r.authContext, state.ID.ValueString()).UpdateReverseEtlModelInput(api.UpdateReverseEtlModelInput{ Name: plan.Name.ValueStringPointer(), Description: plan.Description.ValueStringPointer(), Enabled: plan.Enabled.ValueBoolPointer(), - ScheduleStrategy: plan.ScheduleStrategy.ValueStringPointer(), - ScheduleConfig: scheduleConfig, Query: plan.Query.ValueStringPointer(), QueryIdentifierColumn: plan.QueryIdentifierColumn.ValueStringPointer(), }).Execute() @@ -248,6 +244,10 @@ func (r *reverseETLModelResource) Update(ctx context.Context, req resource.Updat if resp.Diagnostics.HasError() { return } + + // Since we deprecated these values, we just need to set them to the plan values so there are no errors + resp.State.SetAttribute(ctx, path.Root("schedule_config"), plan.ScheduleConfig) + resp.State.SetAttribute(ctx, path.Root("schedule_strategy"), plan.ScheduleStrategy) } func (r *reverseETLModelResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {