From 8050e53115ffa5f8966de4234a4a24555432b2dd Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Sun, 29 Oct 2023 22:35:28 +0100 Subject: [PATCH 1/3] Update when aem.yml is updated --- examples/ssh/aem.tf | 4 + internal/provider/instance_resource.go | 109 +++++++++++++++---------- internal/provider/provider_utils.go | 35 ++++++++ 3 files changed, 104 insertions(+), 44 deletions(-) create mode 100644 internal/provider/provider_utils.go diff --git a/examples/ssh/aem.tf b/examples/ssh/aem.tf index ba58475..d3e4244 100644 --- a/examples/ssh/aem.tf +++ b/examples/ssh/aem.tf @@ -22,3 +22,7 @@ resource "aem_instance" "single" { output "aem_instances" { value = aem_instance.single.instances } + +output "aem_config_file_checksum" { + value = aem_instance.single.compose.config_file_checksum +} diff --git a/internal/provider/instance_resource.go b/internal/provider/instance_resource.go index 2f35c90..627936e 100644 --- a/internal/provider/instance_resource.go +++ b/internal/provider/instance_resource.go @@ -8,7 +8,11 @@ import ( "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/listplanmodifier" + "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-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/wttech/terraform-provider-aem/internal/client" @@ -33,11 +37,12 @@ type InstanceResourceModel struct { Settings types.Map `tfsdk:"settings"` } `tfsdk:"client"` Compose struct { - DataDir types.String `tfsdk:"data_dir"` - Version types.String `tfsdk:"version"` - ConfigFile types.String `tfsdk:"config_file"` - LibDir types.String `tfsdk:"lib_dir"` - InstanceId types.String `tfsdk:"instance_id"` + DataDir types.String `tfsdk:"data_dir"` + Version types.String `tfsdk:"version"` + ConfigFile types.String `tfsdk:"config_file"` + ConfigFileChecksum types.String `tfsdk:"config_file_checksum"` + LibDir types.String `tfsdk:"lib_dir"` + InstanceId types.String `tfsdk:"instance_id"` } `tfsdk:"compose"` Instances types.List `tfsdk:"instances"` } @@ -86,6 +91,7 @@ func (r *InstanceResource) Schema(ctx context.Context, req resource.SchemaReques Computed: true, Optional: true, Default: stringdefault.StaticString("/mnt/aemc"), + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, }, "version": schema.StringAttribute{ MarkdownDescription: "Version of AEM Compose tool to use on remote AEM machine", @@ -99,6 +105,9 @@ func (r *InstanceResource) Schema(ctx context.Context, req resource.SchemaReques Optional: true, Default: stringdefault.StaticString("aem/default/etc/aem.yml"), }, + "config_file_checksum": schema.StringAttribute{ + Computed: true, + }, "lib_dir": schema.StringAttribute{ MarkdownDescription: "Local path to directory from which AEM library files will be copied to the remote AEM machine", Computed: true, @@ -140,6 +149,12 @@ func (r *InstanceResource) Schema(ctx context.Context, req resource.SchemaReques }, }, }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplaceIf(func(ctx context.Context, request planmodifier.ListRequest, response *listplanmodifier.RequiresReplaceIfFuncResponse) { + // TODO check if: [1] list is not empty; [2] the same instances are still created; [3] dirs have not changed + response.RequiresReplace = true + }, "If the value of this attribute changes, Terraform will destroy and recreate the resource.", "If the value of this attribute changes, Terraform will destroy and recreate the resource."), + }, }, }, } @@ -169,69 +184,83 @@ func (r *InstanceResource) Configure(ctx context.Context, req resource.Configure } func (r *InstanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - model := r.defaultModel() + r.createOrUpdate(ctx, &req.Plan, &resp.Diagnostics, &resp.State) +} + +func (r *InstanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + r.createOrUpdate(ctx, &req.Plan, &resp.Diagnostics, &resp.State) +} + +func (r *InstanceResource) createOrUpdate(ctx context.Context, plan *tfsdk.Plan, diags *diag.Diagnostics, state *tfsdk.State) { + model := r.newModel() // Read Terraform plan data into the model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { + diags.Append(plan.Get(ctx, &model)...) + if diags.HasError() { return } - tflog.Info(ctx, "Creating AEM instance resource") + md5, err := hashFileMD5(model.Compose.ConfigFile.ValueString()) + if err != nil { + diags.AddError("Unable to calculate MD5 checksum for AEM configuration file", fmt.Sprintf("%s", err)) + return + } + model.Compose.ConfigFileChecksum = types.StringValue(md5) + + tflog.Info(ctx, "Started setting up AEM instance resource") ic, err := r.Client(ctx, model, time.Minute*5) if err != nil { - resp.Diagnostics.AddError("Unable to connect to AEM instance", fmt.Sprintf("%s", err)) + diags.AddError("Unable to connect to AEM instance", fmt.Sprintf("%s", err)) return } defer func(ic *InstanceClient) { err := ic.Close() if err != nil { - resp.Diagnostics.AddWarning("Unable to disconnect from AEM instance", fmt.Sprintf("%s", err)) + diags.AddWarning("Unable to disconnect from AEM instance", fmt.Sprintf("%s", err)) } }(ic) if err := ic.prepareDataDir(); err != nil { - resp.Diagnostics.AddError("Unable to prepare AEM data directory", fmt.Sprintf("%s", err)) + diags.AddError("Unable to prepare AEM data directory", fmt.Sprintf("%s", err)) return } if err := ic.installComposeWrapper(); err != nil { - resp.Diagnostics.AddError("Unable to install AEM Compose CLI", fmt.Sprintf("%s", err)) + diags.AddError("Unable to install AEM Compose CLI", fmt.Sprintf("%s", err)) return } if err := ic.copyConfigFile(); err != nil { - resp.Diagnostics.AddError("Unable to copy AEM configuration file", fmt.Sprintf("%s", err)) + diags.AddError("Unable to copy AEM configuration file", fmt.Sprintf("%s", err)) return } if err := ic.copyLibraryDir(); err != nil { - resp.Diagnostics.AddError("Unable to copy AEM library dir", fmt.Sprintf("%s", err)) + diags.AddError("Unable to copy AEM library dir", fmt.Sprintf("%s", err)) return } if err := ic.create(); err != nil { - resp.Diagnostics.AddError("Unable to create AEM instance", fmt.Sprintf("%s", err)) + diags.AddError("Unable to create AEM instance", fmt.Sprintf("%s", err)) return } if err := ic.launch(); err != nil { - resp.Diagnostics.AddError("Unable to launch AEM instance", fmt.Sprintf("%s", err)) + diags.AddError("Unable to launch AEM instance", fmt.Sprintf("%s", err)) return } - tflog.Info(ctx, "Created AEM instance resource") + tflog.Info(ctx, "Finished setting up AEM instance resource") status, err := ic.ReadStatus() if err != nil { - resp.Diagnostics.AddError("Unable to read AEM instance data", fmt.Sprintf("%s", err)) + diags.AddError("Unable to read AEM instance data", fmt.Sprintf("%s", err)) return } - resp.Diagnostics.Append(r.fillModelWithStatus(ctx, &model, status)...) + diags.Append(r.fillModelWithStatus(ctx, &model, status)...) // Save data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) + diags.Append(state.Set(ctx, &model)...) } -func (r *InstanceResource) defaultModel() InstanceResourceModel { +func (r *InstanceResource) newModel() InstanceResourceModel { model := InstanceResourceModel{} model.Instances = types.ListValueMust(types.ObjectType{AttrTypes: InstanceStatusItemModel{}.attrTypes()}, []attr.Value{}) return model @@ -264,7 +293,7 @@ func (r *InstanceResource) fillModelWithStatus(ctx context.Context, model *Insta } func (r *InstanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - model := r.defaultModel() + model := r.newModel() // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &model)...) @@ -272,6 +301,13 @@ func (r *InstanceResource) Read(ctx context.Context, req resource.ReadRequest, r return } + md5, err := hashFileMD5(model.Compose.ConfigFile.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to calculate MD5 checksum for AEM configuration file", fmt.Sprintf("%s", err)) + return + } + model.Compose.ConfigFileChecksum = types.StringValue(md5) + ic, err := r.Client(ctx, model, time.Second*15) if err != nil { resp.Diagnostics.AddWarning("Unable to connect to AEM instance", fmt.Sprintf("%s", err)) @@ -296,21 +332,6 @@ func (r *InstanceResource) Read(ctx context.Context, req resource.ReadRequest, r resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) } -func (r *InstanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var data InstanceResourceModel - - // Read Terraform plan data into the model - resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - if resp.Diagnostics.HasError() { - return - } - - // TODO ... update the resource - - // Save updated data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) -} - func (r *InstanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var data InstanceResourceModel @@ -327,12 +348,12 @@ func (r *InstanceResource) ImportState(ctx context.Context, req resource.ImportS resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } -func (r *InstanceResource) Client(ctx context.Context, data InstanceResourceModel, timeout time.Duration) (*InstanceClient, error) { +func (r *InstanceResource) Client(ctx context.Context, model InstanceResourceModel, timeout time.Duration) (*InstanceClient, error) { tflog.Info(ctx, "Connecting to AEM instance machine") - typeName := data.Client.Type.ValueString() + typeName := model.Client.Type.ValueString() var settings map[string]string - data.Client.Settings.ElementsAs(ctx, &settings, true) + model.Client.Settings.ElementsAs(ctx, &settings, true) cl, err := r.clientManager.Make(typeName, settings) if err != nil { @@ -343,7 +364,7 @@ func (r *InstanceResource) Client(ctx context.Context, data InstanceResourceMode return nil, err } - cl.Env["AEM_CLI_VERSION"] = data.Compose.Version.ValueString() + cl.Env["AEM_CLI_VERSION"] = model.Compose.Version.ValueString() cl.EnvDir = "/tmp" // TODO make configurable; or just in user home dir './' ? if err := cl.SetupEnv(); err != nil { @@ -351,5 +372,5 @@ func (r *InstanceResource) Client(ctx context.Context, data InstanceResourceMode } tflog.Info(ctx, "Connected to AEM instance machine") - return &InstanceClient{cl, ctx, data}, nil + return &InstanceClient{cl, ctx, model}, nil } diff --git a/internal/provider/provider_utils.go b/internal/provider/provider_utils.go new file mode 100644 index 0000000..df06f66 --- /dev/null +++ b/internal/provider/provider_utils.go @@ -0,0 +1,35 @@ +package provider + +import ( + "crypto/md5" + "encoding/hex" + "io" + "os" +) + +// TODO hash with ignoring line endings / OS-independent +func hashFileMD5(file string) (string, error) { + // Open the file + f, err := os.Open(file) + if err != nil { + return "", err + } + defer f.Close() + + // Create an MD5 hasher + hasher := md5.New() + + // Copy the file contents to the hasher + _, err = io.Copy(hasher, f) + if err != nil { + return "", err + } + + // Get the MD5 sum as a byte slice + md5Sum := hasher.Sum(nil) + + // Convert the byte slice to a hexadecimal string + md5String := hex.EncodeToString(md5Sum) + + return md5String, nil +} From 5fde82f4d8faf15c1a3a29fef0048bb963aecf0e Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Sun, 29 Oct 2023 23:31:39 +0100 Subject: [PATCH 2/3] Instance config file checksum handled properly --- examples/ssh/aem.tf | 7 +--- examples/ssh/aem/default/etc/aem.yml | 2 + internal/provider/instance/plan_modifiers.go | 42 +++++++++++++++++++ internal/provider/instance_resource.go | 28 ++++--------- .../provider_utils.go => utils/file.go} | 4 +- 5 files changed, 56 insertions(+), 27 deletions(-) create mode 100644 internal/provider/instance/plan_modifiers.go rename internal/{provider/provider_utils.go => utils/file.go} (89%) diff --git a/examples/ssh/aem.tf b/examples/ssh/aem.tf index d3e4244..75ee3d2 100644 --- a/examples/ssh/aem.tf +++ b/examples/ssh/aem.tf @@ -11,18 +11,13 @@ resource "aem_instance" "single" { } } compose { - version = "1.5.7" + version = "1.5.8" data_dir = "/home/ec2-user/aemc" lib_dir = "aem/home/lib" config_file = "aem/default/etc/aem.yml" } } - output "aem_instances" { value = aem_instance.single.instances } - -output "aem_config_file_checksum" { - value = aem_instance.single.compose.config_file_checksum -} diff --git a/examples/ssh/aem/default/etc/aem.yml b/examples/ssh/aem/default/etc/aem.yml index 2254d82..faded7c 100644 --- a/examples/ssh/aem/default/etc/aem.yml +++ b/examples/ssh/aem/default/etc/aem.yml @@ -18,11 +18,13 @@ instance: - -Duser.country=US - -Duser.timezone=UTC start_opts: [] + secret_vars: - ACME_SECRET=value env_vars: - ACME_VAR=value sling_props: [] + local_publish: active: true http_url: http://127.0.0.1:4503 diff --git a/internal/provider/instance/plan_modifiers.go b/internal/provider/instance/plan_modifiers.go new file mode 100644 index 0000000..e3a07e2 --- /dev/null +++ b/internal/provider/instance/plan_modifiers.go @@ -0,0 +1,42 @@ +package instance + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/wttech/terraform-provider-aem/internal/utils" +) + +func ConfigFileChecksumPlanModifier() planmodifier.String { + return &configFileChecksumPlanModifier{} +} + +type configFileChecksumPlanModifier struct{} + +func (m *configFileChecksumPlanModifier) Description(ctx context.Context) string { + return "Updates AEM configuration file checksum when contents change." +} + +func (m *configFileChecksumPlanModifier) MarkdownDescription(ctx context.Context) string { + return m.Description(ctx) +} + +func (m *configFileChecksumPlanModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + var configFile types.String + diags := req.Plan.GetAttribute(ctx, path.Root("compose").AtName("config_file"), &configFile) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !configFile.IsNull() { + configFilePath := configFile.ValueString() + checksum, err := utils.HashFileMD5(configFilePath) + if err != nil { + resp.Diagnostics.AddError("Unable to calculate checksum of AEM configuration file", fmt.Sprintf("path '%s', error: %s", configFilePath, err)) + return + } + resp.PlanValue = types.StringValue(checksum) + } +} diff --git a/internal/provider/instance_resource.go b/internal/provider/instance_resource.go index 627936e..839cbda 100644 --- a/internal/provider/instance_resource.go +++ b/internal/provider/instance_resource.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/wttech/terraform-provider-aem/internal/client" + "github.com/wttech/terraform-provider-aem/internal/provider/instance" "time" ) @@ -106,7 +107,8 @@ func (r *InstanceResource) Schema(ctx context.Context, req resource.SchemaReques Default: stringdefault.StaticString("aem/default/etc/aem.yml"), }, "config_file_checksum": schema.StringAttribute{ - Computed: true, + Computed: true, + PlanModifiers: []planmodifier.String{instance.ConfigFileChecksumPlanModifier()}, }, "lib_dir": schema.StringAttribute{ MarkdownDescription: "Local path to directory from which AEM library files will be copied to the remote AEM machine", @@ -150,9 +152,10 @@ func (r *InstanceResource) Schema(ctx context.Context, req resource.SchemaReques }, }, PlanModifiers: []planmodifier.List{ + listplanmodifier.UseStateForUnknown(), listplanmodifier.RequiresReplaceIf(func(ctx context.Context, request planmodifier.ListRequest, response *listplanmodifier.RequiresReplaceIfFuncResponse) { // TODO check if: [1] list is not empty; [2] the same instances are still created; [3] dirs have not changed - response.RequiresReplace = true + // response.RequiresReplace = true }, "If the value of this attribute changes, Terraform will destroy and recreate the resource.", "If the value of this attribute changes, Terraform will destroy and recreate the resource."), }, }, @@ -200,16 +203,9 @@ func (r *InstanceResource) createOrUpdate(ctx context.Context, plan *tfsdk.Plan, return } - md5, err := hashFileMD5(model.Compose.ConfigFile.ValueString()) - if err != nil { - diags.AddError("Unable to calculate MD5 checksum for AEM configuration file", fmt.Sprintf("%s", err)) - return - } - model.Compose.ConfigFileChecksum = types.StringValue(md5) - tflog.Info(ctx, "Started setting up AEM instance resource") - ic, err := r.Client(ctx, model, time.Minute*5) + ic, err := r.client(ctx, model, time.Minute*5) if err != nil { diags.AddError("Unable to connect to AEM instance", fmt.Sprintf("%s", err)) return @@ -301,14 +297,7 @@ func (r *InstanceResource) Read(ctx context.Context, req resource.ReadRequest, r return } - md5, err := hashFileMD5(model.Compose.ConfigFile.ValueString()) - if err != nil { - resp.Diagnostics.AddError("Unable to calculate MD5 checksum for AEM configuration file", fmt.Sprintf("%s", err)) - return - } - model.Compose.ConfigFileChecksum = types.StringValue(md5) - - ic, err := r.Client(ctx, model, time.Second*15) + ic, err := r.client(ctx, model, time.Second*15) if err != nil { resp.Diagnostics.AddWarning("Unable to connect to AEM instance", fmt.Sprintf("%s", err)) } else { @@ -345,10 +334,11 @@ func (r *InstanceResource) Delete(ctx context.Context, req resource.DeleteReques } func (r *InstanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // TODO implement it properly resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } -func (r *InstanceResource) Client(ctx context.Context, model InstanceResourceModel, timeout time.Duration) (*InstanceClient, error) { +func (r *InstanceResource) client(ctx context.Context, model InstanceResourceModel, timeout time.Duration) (*InstanceClient, error) { tflog.Info(ctx, "Connecting to AEM instance machine") typeName := model.Client.Type.ValueString() diff --git a/internal/provider/provider_utils.go b/internal/utils/file.go similarity index 89% rename from internal/provider/provider_utils.go rename to internal/utils/file.go index df06f66..83dc1e7 100644 --- a/internal/provider/provider_utils.go +++ b/internal/utils/file.go @@ -1,4 +1,4 @@ -package provider +package utils import ( "crypto/md5" @@ -8,7 +8,7 @@ import ( ) // TODO hash with ignoring line endings / OS-independent -func hashFileMD5(file string) (string, error) { +func HashFileMD5(file string) (string, error) { // Open the file f, err := os.Open(file) if err != nil { From 073f95bbd63a1de53bd12b601dea5903abdb9c1c Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Sun, 29 Oct 2023 23:32:49 +0100 Subject: [PATCH 3/3] Revert --- examples/ssh/aem/default/etc/aem.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/ssh/aem/default/etc/aem.yml b/examples/ssh/aem/default/etc/aem.yml index faded7c..2254d82 100644 --- a/examples/ssh/aem/default/etc/aem.yml +++ b/examples/ssh/aem/default/etc/aem.yml @@ -18,13 +18,11 @@ instance: - -Duser.country=US - -Duser.timezone=UTC start_opts: [] - secret_vars: - ACME_SECRET=value env_vars: - ACME_VAR=value sling_props: [] - local_publish: active: true http_url: http://127.0.0.1:4503