Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update when aem.yml is updated #9

Merged
merged 3 commits into from
Oct 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions examples/ssh/aem.tf
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +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
}
42 changes: 42 additions & 0 deletions internal/provider/instance/plan_modifiers.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
103 changes: 57 additions & 46 deletions internal/provider/instance_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ 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"
"github.com/wttech/terraform-provider-aem/internal/provider/instance"
"time"
)

Expand All @@ -33,11 +38,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"`
}
Expand Down Expand Up @@ -86,6 +92,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",
Expand All @@ -99,6 +106,10 @@ 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,
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",
Computed: true,
Expand Down Expand Up @@ -140,6 +151,13 @@ 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
}, "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."),
},
},
},
}
Expand Down Expand Up @@ -169,69 +187,76 @@ 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")
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 {
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
Expand Down Expand Up @@ -264,15 +289,15 @@ 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)...)
if resp.Diagnostics.HasError() {
return
}

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 {
Expand All @@ -296,21 +321,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

Expand All @@ -324,15 +334,16 @@ 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, 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 {
Expand All @@ -343,13 +354,13 @@ 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 {
return nil, err
}

tflog.Info(ctx, "Connected to AEM instance machine")
return &InstanceClient{cl, ctx, data}, nil
return &InstanceClient{cl, ctx, model}, nil
}
35 changes: 35 additions & 0 deletions internal/utils/file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package utils

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
}