From 2e3e08d1338f9a698d4aee265963f929c8954e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Sacrist=C3=A1n=20Izcue?= Date: Wed, 29 May 2024 01:04:06 +0200 Subject: [PATCH] chore(cloudamqp_integration_aws_eventbridge): Migrate towards terraform plugin framework This is the first resource to move towards the new system and should serve as a template for all others. The reason to choose this one as the first is because it is the smallest file that still has tests to prove everything still works as before. --- cloudamqp/provider.go | 49 ++-- .../resource_cloudamqp_aws_eventbridge.go | 271 ++++++++++++------ 2 files changed, 203 insertions(+), 117 deletions(-) diff --git a/cloudamqp/provider.go b/cloudamqp/provider.go index fbfe8541..fa9c3af1 100644 --- a/cloudamqp/provider.go +++ b/cloudamqp/provider.go @@ -99,7 +99,11 @@ func (p *cloudamqpProvider) DataSources(_ context.Context) []func() datasource.D } func (p *cloudamqpProvider) Resources(_ context.Context) []func() resource.Resource { - return []func() resource.Resource{} + return []func() resource.Resource{ + func() resource.Resource { + return &awsEventBridgeResource{} + }, + } } func New(version string, client *http.Client) provider.Provider { @@ -145,28 +149,27 @@ func Provider(v string, client *http.Client) *schemaSdk.Provider { "cloudamqp_vpc_info": dataSourceVpcInfo(), }, ResourcesMap: map[string]*schemaSdk.Resource{ - "cloudamqp_account_action": resourceAccountAction(), - "cloudamqp_alarm": resourceAlarm(), - "cloudamqp_custom_domain": resourceCustomDomain(), - "cloudamqp_extra_disk_size": resourceExtraDiskSize(), - "cloudamqp_instance": resourceInstance(), - "cloudamqp_integration_aws_eventbridge": resourceAwsEventBridge(), - "cloudamqp_integration_log": resourceIntegrationLog(), - "cloudamqp_integration_metric": resourceIntegrationMetric(), - "cloudamqp_node_actions": resourceNodeAction(), - "cloudamqp_notification": resourceNotification(), - "cloudamqp_plugin_community": resourcePluginCommunity(), - "cloudamqp_plugin": resourcePlugin(), - "cloudamqp_privatelink_aws": resourcePrivateLinkAws(), - "cloudamqp_privatelink_azure": resourcePrivateLinkAzure(), - "cloudamqp_rabbitmq_configuration": resourceRabbitMqConfiguration(), - "cloudamqp_security_firewall": resourceSecurityFirewall(), - "cloudamqp_upgrade_rabbitmq": resourceUpgradeRabbitMQ(), - "cloudamqp_vpc_connect": resourceVpcConnect(), - "cloudamqp_vpc_gcp_peering": resourceVpcGcpPeering(), - "cloudamqp_vpc_peering": resourceVpcPeering(), - "cloudamqp_vpc": resourceVpc(), - "cloudamqp_webhook": resourceWebhook(), + "cloudamqp_account_action": resourceAccountAction(), + "cloudamqp_alarm": resourceAlarm(), + "cloudamqp_custom_domain": resourceCustomDomain(), + "cloudamqp_extra_disk_size": resourceExtraDiskSize(), + "cloudamqp_instance": resourceInstance(), + "cloudamqp_integration_log": resourceIntegrationLog(), + "cloudamqp_integration_metric": resourceIntegrationMetric(), + "cloudamqp_node_actions": resourceNodeAction(), + "cloudamqp_notification": resourceNotification(), + "cloudamqp_plugin_community": resourcePluginCommunity(), + "cloudamqp_plugin": resourcePlugin(), + "cloudamqp_privatelink_aws": resourcePrivateLinkAws(), + "cloudamqp_privatelink_azure": resourcePrivateLinkAzure(), + "cloudamqp_rabbitmq_configuration": resourceRabbitMqConfiguration(), + "cloudamqp_security_firewall": resourceSecurityFirewall(), + "cloudamqp_upgrade_rabbitmq": resourceUpgradeRabbitMQ(), + "cloudamqp_vpc_connect": resourceVpcConnect(), + "cloudamqp_vpc_gcp_peering": resourceVpcGcpPeering(), + "cloudamqp_vpc_peering": resourceVpcPeering(), + "cloudamqp_vpc": resourceVpc(), + "cloudamqp_webhook": resourceWebhook(), }, ConfigureFunc: configureClient(client), } diff --git a/cloudamqp/resource_cloudamqp_aws_eventbridge.go b/cloudamqp/resource_cloudamqp_aws_eventbridge.go index 3cebb119..8256114d 100644 --- a/cloudamqp/resource_cloudamqp_aws_eventbridge.go +++ b/cloudamqp/resource_cloudamqp_aws_eventbridge.go @@ -1,62 +1,121 @@ package cloudamqp import ( + "context" + "encoding/json" "fmt" "log" "strconv" "strings" "github.com/cloudamqp/terraform-provider-cloudamqp/api" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "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/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "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" ) -func resourceAwsEventBridge() *schema.Resource { - return &schema.Resource{ - Create: resourceAwsEventBridgeCreate, - Read: resourceAwsEventBridgeRead, - Delete: resourceAwsEventBridgeDelete, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - Schema: map[string]*schema.Schema{ - "instance_id": { - Type: schema.TypeInt, - ForceNew: true, +type awsEventBridgeResource struct { + client *api.API +} + +type awsEventBridgeResourceModel struct { + Id types.String `tfsdk:"id"` + InstanceID types.Int64 `tfsdk:"instance_id"` + AwsAccountId types.String `tfsdk:"aws_account_id"` + AwsRegion types.String `tfsdk:"aws_region"` + Vhost types.String `tfsdk:"vhost"` + QueueName types.String `tfsdk:"queue"` + WithHeaders types.Bool `tfsdk:"with_headers"` + Status types.String `tfsdk:"status"` +} + +type awsEventBridgeResourceApiModel struct { + AwsAccountId string `json:"aws_account_id"` + AwsRegion string `json:"aws_region"` + Vhost string `json:"vhost"` + QueueName string `json:"queue"` + WithHeaders bool `json:"with_headers"` +} + +func (r *awsEventBridgeResource) Configure(ctx context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { + // Always perform a nil check when handling ProviderData because Terraform + // sets that data after it calls the ConfigureProvider RPC. + if request.ProviderData == nil { + return + } + + client, ok := request.ProviderData.(*api.API) + + if !ok { + response.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *api.API, got: %T. Please report this issue to the provider developers.", request.ProviderData), + ) + + return + } + + r.client = client +} + +func (r *awsEventBridgeResource) Metadata(ctx context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "cloudamqp_integration_aws_eventbridge" +} + +func (r *awsEventBridgeResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "instance_id": schema.Int64Attribute{ Required: true, Description: "Instance identifier", + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, }, - "aws_account_id": { - Type: schema.TypeString, - ForceNew: true, + "aws_account_id": schema.StringAttribute{ Required: true, Description: "The 12 digit AWS Account ID where you want the events to be sent to.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, - "aws_region": { - Type: schema.TypeString, - ForceNew: true, + "aws_region": schema.StringAttribute{ Required: true, Description: "The AWS region where you the events to be sent to. (e.g. us-west-1, us-west-2, ..., etc.)", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, - "vhost": { - Type: schema.TypeString, - ForceNew: true, + "vhost": schema.StringAttribute{ Required: true, Description: "The VHost the queue resides in.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, - "queue": { - Type: schema.TypeString, - ForceNew: true, + "queue": schema.StringAttribute{ Required: true, Description: "A (durable) queue on your RabbitMQ instance.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, - "with_headers": { - Type: schema.TypeBool, - ForceNew: true, + "with_headers": schema.BoolAttribute{ Required: true, Description: "Include message headers in the event data.", + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, }, - "status": { - Type: schema.TypeString, + "status": schema.StringAttribute{ Computed: true, Description: "Always set to null, unless there is an error starting the EventBridge", }, @@ -64,95 +123,119 @@ func resourceAwsEventBridge() *schema.Resource { } } -func resourceAwsEventBridgeCreate(d *schema.ResourceData, meta interface{}) error { - var ( - api = meta.(*api.API) - keys = awsEventbridgeAttributeKeys() - params = make(map[string]interface{}) - instanceID = d.Get("instance_id").(int) - ) +func (r *awsEventBridgeResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var data awsEventBridgeResourceModel + + // Read Terraform plan data into the model + response.Diagnostics.Append(request.Plan.Get(ctx, &data)...) + + if response.Diagnostics.HasError() { + return + } + + apiModel := awsEventBridgeResourceApiModel{ + AwsAccountId: data.AwsAccountId.ValueString(), + AwsRegion: data.AwsRegion.ValueString(), + Vhost: data.Vhost.ValueString(), + QueueName: data.QueueName.ValueString(), + WithHeaders: data.WithHeaders.ValueBool(), + } - for _, k := range keys { - if v := d.Get(k); v != nil { - params[k] = v - } + var params map[string]interface{} + temp, err := json.Marshal(apiModel) + if err != nil { + response.Diagnostics.AddError( + "Unable to Create Resource", + "An unexpected error occurred while creating the resource create request. "+ + "Please report this issue to the provider developers.\n\n"+ + "JSON Error: "+err.Error(), + ) + return } + // TODO: This is totally a hack to get the struct into a map[string]interface{} + // It is very unlikely this will fail after the first one succeeds, so it should be fine to ignore the error + // Maybe after the api is moved into the repo we can improve the interface + _ = json.Unmarshal(temp, ¶ms) - data, err := api.CreateAwsEventBridge(instanceID, params) + apiResponse, err := r.client.CreateAwsEventBridge(int(data.InstanceID.ValueInt64()), params) if err != nil { - return err + response.Diagnostics.AddError( + "Failed to Create Resource", + "An error occurred while calling the api to create the surface, verify your permissions are correct.\n\n"+ + "JSON Error: "+err.Error(), + ) + return } - d.SetId(data["id"].(string)) - return nil + data.Id = types.StringValue(apiResponse["id"].(string)) + data.Status = types.StringNull() + + // Save data into Terraform state + response.Diagnostics.Append(response.State.Set(ctx, &data)...) } -func resourceAwsEventBridgeRead(d *schema.ResourceData, meta interface{}) error { - if strings.Contains(d.Id(), ",") { - log.Printf("[DEBUG] cloudamqp::resource::aws-eventbridge::read id contains : %v", d.Id()) - s := strings.Split(d.Id(), ",") +func (r *awsEventBridgeResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var state awsEventBridgeResourceModel + + // Read Terraform plan data into the model + response.Diagnostics.Append(request.State.Get(ctx, &state)...) + + if strings.Contains(state.Id.ValueString(), ",") { + log.Printf("[DEBUG] cloudamqp::resource::aws-eventbridge::read id contains : %v", state.Id.String()) + s := strings.Split(state.Id.ValueString(), ",") log.Printf("[DEBUG] cloudamqp::resource::aws-eventbridge::read split ids: %v, %v", s[0], s[1]) - d.SetId(s[0]) + state.Id = types.StringValue(s[0]) instanceID, _ := strconv.Atoi(s[1]) - d.Set("instance_id", instanceID) + state.InstanceID = types.Int64Value(int64(instanceID)) } - if d.Get("instance_id").(int) == 0 { - return fmt.Errorf("missing instance identifier: {resource_id},{instance_id}") + if state.InstanceID.ValueInt64() == 0 { + response.Diagnostics.AddError("Missing instance identifier {resource_id},{instance_id}", "") + return } var ( - api = meta.(*api.API) - instanceID = d.Get("instance_id").(int) + id = state.Id.ValueString() + instanceID = int(state.InstanceID.ValueInt64()) ) - log.Printf("[DEBUG] cloudamqp::resource::aws-eventbridge::read ID: %v, instanceID %v", d.Id(), instanceID) - data, err := api.ReadAwsEventBridge(instanceID, d.Id()) + log.Printf("[DEBUG] cloudamqp::resource::aws-eventbridge::read ID: %v, instanceID %v", id, instanceID) + data, err := r.client.ReadAwsEventBridge(instanceID, id) if err != nil { - return err + response.Diagnostics.AddError("Something went wrong while reading the aws event bridge", fmt.Sprintf("%v", err)) + return } - for k, v := range data { - if validateAwsEventBridgeSchemaAttribute(k) { - if v == nil { - continue - } - if err = d.Set(k, v); err != nil { - return fmt.Errorf("error setting %s for resource %s: %s", k, d.Id(), err) - } - } - } + state.AwsAccountId = types.StringValue(data["aws_account_id"].(string)) + state.AwsRegion = types.StringValue(data["aws_region"].(string)) + state.Vhost = types.StringValue(data["vhost"].(string)) + state.QueueName = types.StringValue(data["queue"].(string)) + state.WithHeaders = types.BoolValue(data["with_headers"].(bool)) - return nil -} + // Save data into Terraform state + response.Diagnostics.Append(response.State.Set(ctx, &state)...) -func resourceAwsEventBridgeDelete(d *schema.ResourceData, meta interface{}) error { - var ( - api = meta.(*api.API) - instanceID = d.Get("instance_id").(int) - ) + return +} - return api.DeleteAwsEventBridge(instanceID, d.Id()) +func (r *awsEventBridgeResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + // This resource does not implement the Update function } -func awsEventbridgeAttributeKeys() []string { - return []string{ - "aws_account_id", - "aws_region", - "vhost", - "queue", - "with_headers", +func (r *awsEventBridgeResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + var data awsEventBridgeResourceModel + + // Read Terraform plan data into the model + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + var id = data.Id.ValueString() + err := r.client.DeleteAwsEventBridge(int(data.InstanceID.ValueInt64()), id) + + if err != nil { + response.Diagnostics.AddError("An error occurred while deleting cloudamqp_integration_aws_eventbridge", + fmt.Sprintf("Error deleting Cloudamqp event bridge %s: %s", id, err), + ) } } -func validateAwsEventBridgeSchemaAttribute(key string) bool { - switch key { - case "aws_account_id", - "aws_region", - "vhost", - "queue", - "with_headers", - "status": - return true - } - return false +func (r *awsEventBridgeResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), request, response) }