From 33256f2c87882a697de16a423c933d55b4482b88 Mon Sep 17 00:00:00 2001 From: Dana Fallon <8871189+danafallon@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:22:33 -0700 Subject: [PATCH 1/2] Implement deployment update --- docs/resources/deployment.md | 3 +- examples/deployments/main.tf | 14 ++- internal/provider/deployment_resource.go | 100 +++++++++++------- .../provider/models/deployment_api_model.go | 1 + .../models/deployment_resource_model.go | 1 + internal/provider/models/translate.go | 90 +++++++++++++++- 6 files changed, 167 insertions(+), 42 deletions(-) diff --git a/docs/resources/deployment.md b/docs/resources/deployment.md index f8dce28..0de7eb0 100644 --- a/docs/resources/deployment.md +++ b/docs/resources/deployment.md @@ -24,11 +24,12 @@ Artie Deployment resource ### Optional - `advanced_settings` (Attributes) (see [below for nested schema](#nestedatt--advanced_settings)) +- `destination_uuid` (String) - `status` (String) ### Read-Only -- `destination_uuid` (String) +- `company_uuid` (String) - `has_undeployed_changes` (Boolean) - `last_updated_at` (String) - `uuid` (String) diff --git a/examples/deployments/main.tf b/examples/deployments/main.tf index e3cd1b1..d5a5b1c 100644 --- a/examples/deployments/main.tf +++ b/examples/deployments/main.tf @@ -16,7 +16,7 @@ import { } resource "artie_deployment" "example" { - name = "MongoDB ➡️ BigQuery" + name = "MongoDB ➡️ BigQueryyy" source = { name = "MongoDB" config = { @@ -24,6 +24,7 @@ resource "artie_deployment" "example" { host = "mongodb+srv://cluster0.szddg49.mongodb.net/" port = 0 user = "artie" + dynamodb = {} } tables = [ { @@ -34,11 +35,18 @@ resource "artie_deployment" "example" { } }, { - name = "stock" - schema = "" + name = "stock" + schema = "" + advanced_settings = {} + }, + { + name = "new_table" + schema = "" + advanced_settings = {} } ] } + destination_uuid = "fa7d4efc-3957-41e5-b29c-66e2d49bffde" destination_config = { dataset = "customers" } diff --git a/internal/provider/deployment_resource.go b/internal/provider/deployment_resource.go index 44561a3..29789cc 100644 --- a/internal/provider/deployment_resource.go +++ b/internal/provider/deployment_resource.go @@ -1,6 +1,7 @@ package provider import ( + "bytes" "context" "encoding/json" "fmt" @@ -12,7 +13,9 @@ import ( "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/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-log/tflog" ) @@ -38,11 +41,12 @@ func (r *DeploymentResource) Schema(ctx context.Context, req resource.SchemaRequ resp.Schema = schema.Schema{ MarkdownDescription: "Artie Deployment resource", Attributes: map[string]schema.Attribute{ - "uuid": schema.StringAttribute{Computed: true}, + "uuid": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "company_uuid": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, "name": schema.StringAttribute{Required: true}, "status": schema.StringAttribute{Computed: true, Optional: true}, "last_updated_at": schema.StringAttribute{Computed: true}, - "destination_uuid": schema.StringAttribute{Computed: true}, + "destination_uuid": schema.StringAttribute{Computed: true, Optional: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, "has_undeployed_changes": schema.BoolAttribute{Computed: true}, "source": schema.SingleNestedAttribute{ Required: true, @@ -52,18 +56,18 @@ func (r *DeploymentResource) Schema(ctx context.Context, req resource.SchemaRequ Required: true, Attributes: map[string]schema.Attribute{ "host": schema.StringAttribute{Required: true}, - "snapshot_host": schema.StringAttribute{Optional: true}, + "snapshot_host": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("")}, "port": schema.Int64Attribute{Required: true}, "user": schema.StringAttribute{Required: true}, "database": schema.StringAttribute{Required: true}, "dynamodb": schema.SingleNestedAttribute{ Optional: true, Attributes: map[string]schema.Attribute{ - "region": schema.StringAttribute{Optional: true}, - "table_name": schema.StringAttribute{Optional: true}, - "streams_arn": schema.StringAttribute{Optional: true}, - "aws_access_key_id": schema.StringAttribute{Optional: true}, - "aws_secret_access_key": schema.StringAttribute{Optional: true}, + "region": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("")}, + "table_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("")}, + "streams_arn": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("")}, + "aws_access_key_id": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("")}, + "aws_secret_access_key": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("")}, }, }, // TODO Password @@ -73,7 +77,7 @@ func (r *DeploymentResource) Schema(ctx context.Context, req resource.SchemaRequ Required: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ - "uuid": schema.StringAttribute{Computed: true}, + "uuid": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, "name": schema.StringAttribute{Required: true}, "schema": schema.StringAttribute{Required: true}, "enable_history_mode": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, @@ -159,22 +163,11 @@ func (r *DeploymentResource) Create(ctx context.Context, req resource.CreateRequ // Read Terraform plan data into the model resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - if resp.Diagnostics.HasError() { return } - // If applicable, this is a great opportunity to initialize any necessary - // provider client data and make a call using it. - // httpResp, err := r.client.Do(httpReq) - // if err != nil { - // resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create example, got error: %s", err)) - // return - // } - - // For the purposes of this example code, hardcoding a response value to - // save into the Terraform state. - // data.Id = types.StringValue("example-id") + // TODO implement Create // Write logs using the tflog package // Documentation: https://terraform.io/plugin/log @@ -237,18 +230,58 @@ func (r *DeploymentResource) Update(ctx context.Context, req resource.UpdateRequ // Read Terraform plan data into the model resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - if resp.Diagnostics.HasError() { return } - // If applicable, this is a great opportunity to initialize any necessary - // provider client data and make a call using it. - // httpResp, err := r.client.Do(httpReq) - // if err != nil { - // resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update example, got error: %s", err)) - // return - // } + payload := map[string]any{ + "deploy": models.DeploymentResourceToAPIModel(data), + "updateDeployOnly": true, + } + payloadBytes, err := json.Marshal(payload) + if err != nil { + resp.Diagnostics.AddError("Unable to Update Deployment", err.Error()) + return + } + + ctx = tflog.SetField(ctx, "uuid", data.UUID.ValueString()) + ctx = tflog.SetField(ctx, "payload", string(payloadBytes)) + tflog.Info(ctx, "Updating deployment") + + apiReq, err := http.NewRequest("POST", fmt.Sprintf("%s/deployments/%s", r.endpoint, data.UUID.ValueString()), bytes.NewReader(payloadBytes)) + if err != nil { + resp.Diagnostics.AddError("Unable to Update Deployment", err.Error()) + return + } + + apiReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.apiKey)) + apiResp, err := http.DefaultClient.Do(apiReq) + if err != nil { + resp.Diagnostics.AddError("Unable to Update Deployment", err.Error()) + return + } + + if apiResp.StatusCode != http.StatusOK { + resp.Diagnostics.AddError("Unable to Update Deployment", fmt.Sprintf("Received status code %d", apiResp.StatusCode)) + return + } + + defer apiResp.Body.Close() + bodyBytes, err := io.ReadAll(apiResp.Body) + if err != nil { + resp.Diagnostics.AddError("Unable to Update Deployment", err.Error()) + return + } + + var deploymentResp models.DeploymentAPIResponse + err = json.Unmarshal(bodyBytes, &deploymentResp) + if err != nil { + resp.Diagnostics.AddError("Unable to Update Deployment", err.Error()) + return + } + + // Translate API response into Terraform state + models.DeploymentAPIToResourceModel(deploymentResp.Deployment, &data) // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) @@ -259,18 +292,11 @@ func (r *DeploymentResource) Delete(ctx context.Context, req resource.DeleteRequ // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - if resp.Diagnostics.HasError() { return } - // If applicable, this is a great opportunity to initialize any necessary - // provider client data and make a call using it. - // httpResp, err := r.client.Do(httpReq) - // if err != nil { - // resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete example, got error: %s", err)) - // return - // } + // TODO implement Delete } func (r *DeploymentResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { diff --git a/internal/provider/models/deployment_api_model.go b/internal/provider/models/deployment_api_model.go index f9d3324..0cae6bd 100644 --- a/internal/provider/models/deployment_api_model.go +++ b/internal/provider/models/deployment_api_model.go @@ -6,6 +6,7 @@ type DeploymentAPIResponse struct { type DeploymentAPIModel struct { UUID string `json:"uuid"` + CompanyUUID string `json:"companyUUID"` Name string `json:"name"` Status string `json:"status"` LastUpdatedAt string `json:"lastUpdatedAt"` diff --git a/internal/provider/models/deployment_resource_model.go b/internal/provider/models/deployment_resource_model.go index 9b1c49c..4f6fb1a 100644 --- a/internal/provider/models/deployment_resource_model.go +++ b/internal/provider/models/deployment_resource_model.go @@ -4,6 +4,7 @@ import "github.com/hashicorp/terraform-plugin-framework/types" type DeploymentResourceModel struct { UUID types.String `tfsdk:"uuid"` + CompanyUUID types.String `tfsdk:"company_uuid"` Name types.String `tfsdk:"name"` Status types.String `tfsdk:"status"` LastUpdatedAt types.String `tfsdk:"last_updated_at"` diff --git a/internal/provider/models/translate.go b/internal/provider/models/translate.go index e6ba9de..7569237 100644 --- a/internal/provider/models/translate.go +++ b/internal/provider/models/translate.go @@ -1,8 +1,13 @@ package models -import "github.com/hashicorp/terraform-plugin-framework/types" +import ( + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/types" +) func DeploymentAPIToResourceModel(apiModel DeploymentAPIModel, resourceModel *DeploymentResourceModel) { + resourceModel.UUID = types.StringValue(apiModel.UUID) + resourceModel.CompanyUUID = types.StringValue(apiModel.CompanyUUID) resourceModel.Name = types.StringValue(apiModel.Name) resourceModel.Status = types.StringValue(apiModel.Status) resourceModel.LastUpdatedAt = types.StringValue(apiModel.LastUpdatedAt) @@ -76,3 +81,86 @@ func DeploymentAPIToResourceModel(apiModel DeploymentAPIModel, resourceModel *De // TODO PartitionRegex } } + +func DeploymentResourceToAPIModel(resourceModel DeploymentResourceModel) DeploymentAPIModel { + tables := []TableAPIModel{} + for _, table := range resourceModel.Source.Tables { + tableUUID := table.UUID.ValueString() + if tableUUID == "" { + tableUUID = uuid.Nil.String() + } + tables = append(tables, TableAPIModel{ + UUID: tableUUID, + Name: table.Name.ValueString(), + Schema: table.Schema.ValueString(), + EnableHistoryMode: table.EnableHistoryMode.ValueBool(), + IndividualDeployment: table.IndividualDeployment.ValueBool(), + IsPartitioned: table.IsPartitioned.ValueBool(), + AdvancedSettings: TableAdvancedSettingsAPIModel{ + Alias: table.AdvancedSettings.Alias.ValueString(), + SkipDelete: table.AdvancedSettings.SkipDelete.ValueBool(), + FlushIntervalSeconds: table.AdvancedSettings.FlushIntervalSeconds.ValueInt64(), + BufferRows: table.AdvancedSettings.BufferRows.ValueInt64(), + FlushSizeKB: table.AdvancedSettings.FlushSizeKB.ValueInt64(), + AutoscaleMaxReplicas: table.AdvancedSettings.AutoscaleMaxReplicas.ValueInt64(), + AutoscaleTargetValue: table.AdvancedSettings.AutoscaleTargetValue.ValueInt64(), + K8sRequestCPU: table.AdvancedSettings.K8sRequestCPU.ValueInt64(), + K8sRequestMemoryMB: table.AdvancedSettings.K8sRequestMemoryMB.ValueInt64(), + // TODO BigQueryPartitionSettings, MergePredicates, ExcludeColumns + }, + }) + } + + return DeploymentAPIModel{ + UUID: resourceModel.UUID.ValueString(), + CompanyUUID: resourceModel.CompanyUUID.ValueString(), + Name: resourceModel.Name.ValueString(), + Status: resourceModel.Status.ValueString(), + LastUpdatedAt: resourceModel.LastUpdatedAt.ValueString(), + HasUndeployedChanges: resourceModel.HasUndeployedChanges.ValueBool(), + DestinationUUID: resourceModel.DestinationUUID.ValueString(), + Source: SourceAPIModel{ + Name: resourceModel.Source.Name.ValueString(), + Config: SourceConfigAPIModel{ + Host: resourceModel.Source.Config.Host.ValueString(), + SnapshotHost: resourceModel.Source.Config.SnapshotHost.ValueString(), + Port: resourceModel.Source.Config.Port.ValueInt64(), + User: resourceModel.Source.Config.User.ValueString(), + Database: resourceModel.Source.Config.Database.ValueString(), + DynamoDB: DynamoDBConfigAPIModel{ + Region: resourceModel.Source.Config.DynamoDB.Region.ValueString(), + TableName: resourceModel.Source.Config.DynamoDB.TableName.ValueString(), + StreamsArn: resourceModel.Source.Config.DynamoDB.StreamsArn.ValueString(), + AwsAccessKeyID: resourceModel.Source.Config.DynamoDB.AwsAccessKeyID.ValueString(), + AwsSecretAccessKey: resourceModel.Source.Config.DynamoDB.AwsSecretAccessKey.ValueString(), + }, + // TODO Password + }, + Tables: tables, + }, + DestinationConfig: DestinationConfigAPIModel{ + Dataset: resourceModel.DestinationConfig.Dataset.ValueString(), + Database: resourceModel.DestinationConfig.Database.ValueString(), + Schema: resourceModel.DestinationConfig.Schema.ValueString(), + SchemaOverride: resourceModel.DestinationConfig.SchemaOverride.ValueString(), + UseSameSchemaAsSource: resourceModel.DestinationConfig.UseSameSchemaAsSource.ValueBool(), + SchemaNamePrefix: resourceModel.DestinationConfig.SchemaNamePrefix.ValueString(), + BucketName: resourceModel.DestinationConfig.BucketName.ValueString(), + OptionalPrefix: resourceModel.DestinationConfig.OptionalPrefix.ValueString(), + }, + AdvancedSettings: DeploymentAdvancedSettingsAPIModel{ + DropDeletedColumns: resourceModel.AdvancedSettings.DropDeletedColumns.ValueBool(), + IncludeArtieUpdatedAtColumn: resourceModel.AdvancedSettings.IncludeArtieUpdatedAtColumn.ValueBool(), + IncludeDatabaseUpdatedAtColumn: resourceModel.AdvancedSettings.IncludeDatabaseUpdatedAtColumn.ValueBool(), + EnableHeartbeats: resourceModel.AdvancedSettings.EnableHeartbeats.ValueBool(), + EnableSoftDelete: resourceModel.AdvancedSettings.EnableSoftDelete.ValueBool(), + FlushIntervalSeconds: resourceModel.AdvancedSettings.FlushIntervalSeconds.ValueInt64(), + BufferRows: resourceModel.AdvancedSettings.BufferRows.ValueInt64(), + FlushSizeKB: resourceModel.AdvancedSettings.FlushSizeKB.ValueInt64(), + PublicationNameOverride: resourceModel.AdvancedSettings.PublicationNameOverride.ValueString(), + ReplicationSlotOverride: resourceModel.AdvancedSettings.ReplicationSlotOverride.ValueString(), + PublicationAutoCreateMode: resourceModel.AdvancedSettings.PublicationAutoCreateMode.ValueString(), + // TODO PartitionRegex + }, + } +} From 61a86e3e878eb2b4f76d87c4e9cbed67d057b738 Mon Sep 17 00:00:00 2001 From: Dana Fallon <8871189+danafallon@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:32:31 -0700 Subject: [PATCH 2/2] Better logging --- internal/provider/deployment_resource.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/provider/deployment_resource.go b/internal/provider/deployment_resource.go index 29789cc..94be4e0 100644 --- a/internal/provider/deployment_resource.go +++ b/internal/provider/deployment_resource.go @@ -244,11 +244,12 @@ func (r *DeploymentResource) Update(ctx context.Context, req resource.UpdateRequ return } - ctx = tflog.SetField(ctx, "uuid", data.UUID.ValueString()) + url := fmt.Sprintf("%s/deployments/%s", r.endpoint, data.UUID.ValueString()) + ctx = tflog.SetField(ctx, "url", url) ctx = tflog.SetField(ctx, "payload", string(payloadBytes)) - tflog.Info(ctx, "Updating deployment") + tflog.Info(ctx, "Updating deployment via API") - apiReq, err := http.NewRequest("POST", fmt.Sprintf("%s/deployments/%s", r.endpoint, data.UUID.ValueString()), bytes.NewReader(payloadBytes)) + apiReq, err := http.NewRequest("POST", url, bytes.NewReader(payloadBytes)) if err != nil { resp.Diagnostics.AddError("Unable to Update Deployment", err.Error()) return