terraform-provider-edge-con.../internal/provider/app_resource.go
2025-11-11 14:15:52 +01:00

311 lines
10 KiB
Go

package provider
import (
"context"
"fmt"
"github.com/DevFW-CICD/terraform-provider-edge-connect/internal/client"
"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"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &appResource{}
_ resource.ResourceWithConfigure = &appResource{}
_ resource.ResourceWithImportState = &appResource{}
)
// NewAppResource is a helper function to simplify the provider implementation.
func NewAppResource() resource.Resource {
return &appResource{}
}
// appResource is the resource implementation.
type appResource struct {
client *client.Client
}
// appResourceModel maps the resource schema data.
type appResourceModel struct {
ID types.String `tfsdk:"id"`
Region types.String `tfsdk:"region"`
Organization types.String `tfsdk:"organization"`
Name types.String `tfsdk:"name"`
Version types.String `tfsdk:"version"`
ImageType types.String `tfsdk:"image_type"`
ImagePath types.String `tfsdk:"image_path"`
DefaultFlavor types.String `tfsdk:"default_flavor"`
Deployment types.String `tfsdk:"deployment"`
DeploymentManifest types.String `tfsdk:"deployment_manifest"`
AccessPorts types.String `tfsdk:"access_ports"`
Annotations types.String `tfsdk:"annotations"`
CreatedAt types.String `tfsdk:"created_at"`
UpdatedAt types.String `tfsdk:"updated_at"`
}
// Metadata returns the resource type name.
func (r *appResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_app"
}
// Schema defines the schema for the resource.
func (r *appResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Manages an Edge Connect application specification.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "The unique identifier for the app (format: region/organization/name/version).",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"region": schema.StringAttribute{
Description: "The region where the app is deployed (e.g., 'EU').",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"organization": schema.StringAttribute{
Description: "The organization that owns the app.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"name": schema.StringAttribute{
Description: "The name of the application.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"version": schema.StringAttribute{
Description: "The version of the application.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"image_type": schema.StringAttribute{
Description: "The type of image (e.g., 'Docker').",
Required: true,
},
"image_path": schema.StringAttribute{
Description: "The path to the container image.",
Required: true,
},
"default_flavor": schema.StringAttribute{
Description: "The default flavor for the app (e.g., 'EU.small', 'EU.medium', 'EU.big', 'EU.large').",
Optional: true,
},
"deployment": schema.StringAttribute{
Description: "The deployment type (e.g., 'kubernetes').",
Required: true,
},
"deployment_manifest": schema.StringAttribute{
Description: "The Kubernetes deployment manifest (YAML).",
Optional: true,
},
"access_ports": schema.StringAttribute{
Description: "The access ports in format 'protocol:port' (e.g., 'tcp:80,tcp:443').",
Optional: true,
},
"annotations": schema.StringAttribute{
Description: "Annotations for the app.",
Optional: true,
},
"created_at": schema.StringAttribute{
Description: "The timestamp when the app was created.",
Computed: true,
},
"updated_at": schema.StringAttribute{
Description: "The timestamp when the app was last updated.",
Computed: true,
},
},
}
}
// Configure adds the provider configured client to the resource.
func (r *appResource) Configure(_ 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 Resource Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = client
}
// Create creates the resource and sets the initial Terraform state.
func (r *appResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan appResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Create new app
app := client.App{
Region: plan.Region.ValueString(),
ImageType: plan.ImageType.ValueString(),
ImagePath: plan.ImagePath.ValueString(),
DefaultFlavor: plan.DefaultFlavor.ValueString(),
Deployment: plan.Deployment.ValueString(),
DeploymentManifest: plan.DeploymentManifest.ValueString(),
AccessPorts: plan.AccessPorts.ValueString(),
Annotations: plan.Annotations.ValueString(),
}
app.Key.Organization = plan.Organization.ValueString()
app.Key.Name = plan.Name.ValueString()
app.Key.Version = plan.Version.ValueString()
createdApp, err := r.client.CreateApp(plan.Region.ValueString(), app)
if err != nil {
resp.Diagnostics.AddError(
"Error creating app",
"Could not create app: "+err.Error(),
)
return
}
// Map response body to schema and populate Computed attribute values
plan.ID = types.StringValue(fmt.Sprintf("%s/%s/%s/%s",
plan.Region.ValueString(),
createdApp.Key.Organization,
createdApp.Key.Name,
createdApp.Key.Version))
plan.CreatedAt = types.StringValue(createdApp.CreatedAt)
plan.UpdatedAt = types.StringValue(createdApp.UpdatedAt)
tflog.Trace(ctx, "created app resource")
diags = resp.State.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
}
// Read refreshes the Terraform state with the latest data.
func (r *appResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state appResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Get app from API
app := client.App{}
app.Key.Organization = state.Organization.ValueString()
app.Key.Name = state.Name.ValueString()
app.Key.Version = state.Version.ValueString()
readApp, err := r.client.GetApp(state.Region.ValueString(), app)
if err != nil {
resp.Diagnostics.AddError(
"Error reading app",
"Could not read app: "+err.Error(),
)
return
}
// Map response to state
state.ImageType = types.StringValue(readApp.ImageType)
state.ImagePath = types.StringValue(readApp.ImagePath)
state.DefaultFlavor = types.StringValue(readApp.DefaultFlavor)
state.Deployment = types.StringValue(readApp.Deployment)
state.DeploymentManifest = types.StringValue(readApp.DeploymentManifest)
state.AccessPorts = types.StringValue(readApp.AccessPorts)
state.Annotations = types.StringValue(readApp.Annotations)
state.CreatedAt = types.StringValue(readApp.CreatedAt)
state.UpdatedAt = types.StringValue(readApp.UpdatedAt)
diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
}
// Update updates the resource and sets the updated Terraform state on success.
func (r *appResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan appResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Update app
app := client.App{
Region: plan.Region.ValueString(),
ImageType: plan.ImageType.ValueString(),
ImagePath: plan.ImagePath.ValueString(),
DefaultFlavor: plan.DefaultFlavor.ValueString(),
Deployment: plan.Deployment.ValueString(),
DeploymentManifest: plan.DeploymentManifest.ValueString(),
AccessPorts: plan.AccessPorts.ValueString(),
Annotations: plan.Annotations.ValueString(),
}
app.Key.Organization = plan.Organization.ValueString()
app.Key.Name = plan.Name.ValueString()
app.Key.Version = plan.Version.ValueString()
updatedApp, err := r.client.UpdateApp(plan.Region.ValueString(), app)
if err != nil {
resp.Diagnostics.AddError(
"Error updating app",
"Could not update app: "+err.Error(),
)
return
}
// Update computed attributes
plan.UpdatedAt = types.StringValue(updatedApp.UpdatedAt)
diags = resp.State.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
}
// Delete deletes the resource and removes the Terraform state on success.
func (r *appResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state appResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Delete app
app := client.App{}
app.Key.Organization = state.Organization.ValueString()
app.Key.Name = state.Name.ValueString()
app.Key.Version = state.Version.ValueString()
err := r.client.DeleteApp(state.Region.ValueString(), app)
if err != nil {
resp.Diagnostics.AddError(
"Error deleting app",
"Could not delete app: "+err.Error(),
)
return
}
}
// ImportState imports the resource state.
func (r *appResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
// Import format: region/organization/name/version
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}