diff --git a/README.md b/README.md index 710d93a..2749f64 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,11 @@ make testacc Run command: `sh develop.sh `. -For example: `sh develop.sh examples/ssh plan`. +For example: + +- `sh develop.sh examples/ssh plan` +- `sh develop.sh examples/ssh apply -auto-approve` +- `sh develop.sh examples/ssh destroy -auto-approve` ## Debugging the Provider diff --git a/apply.sh b/apply.sh new file mode 100755 index 0000000..13a62a0 --- /dev/null +++ b/apply.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +sh run.sh examples/ssh apply -auto-approve diff --git a/develop.sh b/develop.sh index 541ec20..06f5aaa 100755 --- a/develop.sh +++ b/develop.sh @@ -20,7 +20,7 @@ then fi TF_CLI_CONFIG_FILE="$(pwd)/dev_overrides.tfrc" -TF_LOG=info # TODO or info? +TF_LOG=debug echo "Executing Terraform command at dir: $TF_DIR" (export TF_CLI_CONFIG_FILE && export TF_LOG && cd "$TF_DIR" && terraform "${@:2}") diff --git a/examples/ssh/aem.tf b/examples/ssh/aem.tf index 34638c4..ba58475 100644 --- a/examples/ssh/aem.tf +++ b/examples/ssh/aem.tf @@ -17,3 +17,8 @@ resource "aem_instance" "single" { config_file = "aem/default/etc/aem.yml" } } + + +output "aem_instances" { + value = aem_instance.single.instances +} diff --git a/go.mod b/go.mod index 8826a73..566d0f7 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,10 @@ require ( github.com/hashicorp/terraform-plugin-framework v1.3.5 github.com/hashicorp/terraform-plugin-go v0.18.0 github.com/hashicorp/terraform-plugin-log v0.9.0 + github.com/melbahja/goph v1.3.1 + github.com/spf13/cast v1.5.0 + golang.org/x/crypto v0.11.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -40,7 +44,6 @@ require ( github.com/kr/fs v0.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect - github.com/melbahja/goph v1.3.1 // indirect github.com/mitchellh/cli v1.1.5 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect @@ -51,11 +54,9 @@ require ( github.com/posener/complete v1.2.3 // indirect github.com/russross/blackfriday v1.6.0 // indirect github.com/shopspring/decimal v1.3.1 // indirect - github.com/spf13/cast v1.5.0 // indirect github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/zclconf/go-cty v1.13.2 // indirect - golang.org/x/crypto v0.11.0 // indirect golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect golang.org/x/mod v0.11.0 // indirect golang.org/x/net v0.12.0 // indirect diff --git a/go.sum b/go.sum index 97c1f20..0f78e85 100644 --- a/go.sum +++ b/go.sum @@ -147,8 +147,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= @@ -222,6 +222,7 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/client/client.go b/internal/client/client.go index 5441290..41df4c0 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -40,8 +40,7 @@ func (c Client) Connect() error { return c.connection.Connect() } -func (c Client) ConnectWithRetry(callback func()) error { - timeout := time.Minute * 5 +func (c Client) ConnectWithRetry(timeout time.Duration, callback func()) error { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() for { @@ -73,7 +72,7 @@ func (c Client) Run(cmdLine []string) (*goph.Cmd, error) { func (c Client) SetupEnv() error { file, err := os.CreateTemp(os.TempDir(), "tf-provider-aem-env-*.sh") - path := os.TempDir() + "/" + file.Name() + path := file.Name() defer func() { _ = file.Close(); _ = os.Remove(path) }() if err != nil { return fmt.Errorf("cannot create temporary file for remote shell environment script: %w", err) @@ -132,7 +131,7 @@ func (c Client) DirEnsure(path string) error { func (c Client) FileExists(path string) (bool, error) { out, err := c.RunShell(fmt.Sprintf("test -f %s && echo '0' || echo '1'", path)) if err != nil { - return false, err + return false, fmt.Errorf("cannot check if file exists '%s': %w", path, err) } return strings.TrimSpace(string(out)) == "0", nil } @@ -147,6 +146,14 @@ func (c Client) FileMove(oldPath string, newPath string) error { return nil } +func (c Client) DirExists(path string) (bool, error) { + out, err := c.RunShell(fmt.Sprintf("test -d %s && echo '0' || echo '1'", path)) + if err != nil { + return false, fmt.Errorf("cannot check if directory exists '%s': %w", path, err) + } + return strings.TrimSpace(string(out)) == "0", nil +} + func (c Client) DirCopy(localPath string, remotePath string, override bool) error { if err := c.DirEnsure(remotePath); err != nil { return err diff --git a/internal/provider/client_context.go b/internal/provider/client_context.go index 9a00e6b..e8c3969 100644 --- a/internal/provider/client_context.go +++ b/internal/provider/client_context.go @@ -2,30 +2,11 @@ package provider import ( "context" - "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/wttech/terraform-provider-aem/internal/client" ) -type ClientCreateContext[T interface{}] struct { +type ClientContext[T interface{}] struct { cl *client.Client ctx context.Context data T - req resource.CreateRequest - resp *resource.CreateResponse -} - -type ClientDeleteContext[T interface{}] struct { - cl *client.Client - ctx context.Context - data T - req resource.DeleteRequest - resp *resource.DeleteResponse -} - -type ClientReadContext[T interface{}] struct { - cl *client.Client - ctx context.Context - data T - req resource.ReadRequest - resp *resource.ReadResponse } diff --git a/internal/provider/instance_client.go b/internal/provider/instance_client.go new file mode 100644 index 0000000..3748a24 --- /dev/null +++ b/internal/provider/instance_client.go @@ -0,0 +1,119 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-log/tflog" + "gopkg.in/yaml.v3" +) + +type InstanceClient ClientContext[InstanceResourceModel] + +func (ic *InstanceClient) DataDir() string { + return ic.data.Compose.DataDir.ValueString() +} + +func (ic *InstanceClient) Close() error { + return ic.cl.Disconnect() +} + +// TODO chown data dir to ssh user or 'aem' user (create him maybe) +func (ic *InstanceClient) prepareDataDir() error { + /* TODO to avoid re-uploading library files (probably temporary) + if _, err := ic.cl.RunShell(fmt.Sprintf("rm -fr %s", ic.DataDir())); err != nil { + return fmt.Errorf("cannot clean up AEM data directory: %w", err) + } + */ + if _, err := ic.cl.RunShell(fmt.Sprintf("mkdir -p %s", ic.DataDir())); err != nil { + return fmt.Errorf("cannot create AEM data directory: %w", err) + } + return nil +} + +func (ic *InstanceClient) installComposeWrapper() error { + exists, err := ic.cl.FileExists(fmt.Sprintf("%s/aemw", ic.DataDir())) + if err != nil { + return fmt.Errorf("cannot check if AEM Compose CLI wrapper is installed: %w", err) + } + if !exists { + out, err := ic.cl.RunShellWithEnv(fmt.Sprintf("cd %s && curl -s 'https://raw.githubusercontent.com/wttech/aemc/main/pkg/project/common/aemw' -o 'aemw'", ic.DataDir())) + tflog.Info(ic.ctx, string(out)) + if err != nil { + return fmt.Errorf("cannot download AEM Compose CLI wrapper: %w", err) + } + } + return nil +} + +func (ic *InstanceClient) copyConfigFile() error { + configFile := ic.data.Compose.ConfigFile.ValueString() + if err := ic.cl.FileCopy(configFile, fmt.Sprintf("%s/aem/default/etc/aem.yml", ic.DataDir()), true); err != nil { + return fmt.Errorf("unable to copy AEM configuration file: %w", err) + } + return nil +} + +func (ic *InstanceClient) copyLibraryDir() error { + localLibDir := ic.data.Compose.LibDir.ValueString() + remoteLibDir := fmt.Sprintf("%s/aem/home/lib", ic.DataDir()) + if err := ic.cl.DirCopy(localLibDir, remoteLibDir, false); err != nil { + return fmt.Errorf("unable to copy AEM library dir: %w", err) + } + return nil +} + +func (ic *InstanceClient) create() error { + tflog.Info(ic.ctx, "Creating AEM instance(s)") + + textOut, err := ic.cl.RunShellWithEnv(fmt.Sprintf("cd %s && sh aemw instance create", ic.DataDir())) + if err != nil { + return fmt.Errorf("unable to create AEM instance: %w", err) + } + + textStr := string(textOut) // TODO how about streaming it line by line to tflog ;) + tflog.Info(ic.ctx, "Created AEM instance(s)") + tflog.Info(ic.ctx, textStr) // TODO consider checking 'changed' flag here if needed + + return nil +} + +func (ic *InstanceClient) launch() error { + tflog.Info(ic.ctx, "Launching AEM instance(s)") + + // TODO register systemd service instead and start it + textOut, err := ic.cl.RunShellWithEnv(fmt.Sprintf("cd %s && sh aemw instance launch", ic.DataDir())) + if err != nil { + return fmt.Errorf("unable to launch AEM instance: %w", err) + } + + textStr := string(textOut) // TODO how about streaming it line by line to tflog ;) + tflog.Info(ic.ctx, "Launched AEM instance(s)") + tflog.Info(ic.ctx, textStr) // TODO consider checking 'changed' flag here if needed + + return nil +} + +type InstanceStatus struct { + Data struct { + Instances []struct { + ID string `yaml:"id"` + URL string `yaml:"url"` + AemVersion string `yaml:"aem_version"` + Attributes []string `yaml:"attributes"` + RunModes []string `yaml:"run_modes"` + HealthChecks []string `yaml:"health_checks"` + Dir string `yaml:"dir"` + } `yaml:"instances"` + } +} + +func (ic *InstanceClient) ReadStatus() (InstanceStatus, error) { + var status InstanceStatus + yamlBytes, err := ic.cl.RunShellWithEnv(fmt.Sprintf("cd %s && sh aemw instance status --output-format yaml", ic.DataDir())) + if err != nil { + return status, err + } + if err := yaml.Unmarshal(yamlBytes, &status); err != nil { + return status, fmt.Errorf("unable to parse AEM instance status: %w", err) + } + return status, nil +} diff --git a/internal/provider/instance_resource.go b/internal/provider/instance_resource.go index 5c914ac..2f35c90 100644 --- a/internal/provider/instance_resource.go +++ b/internal/provider/instance_resource.go @@ -3,6 +3,8 @@ package provider import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -10,29 +12,21 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/wttech/terraform-provider-aem/internal/client" + "time" ) // Ensure provider defined types fully satisfy framework interfaces. var _ resource.Resource = &InstanceResource{} var _ resource.ResourceWithImportState = &InstanceResource{} -type InstanceCreateContext ClientCreateContext[InstanceResourceModel] - -func (ic InstanceCreateContext) DataDir() string { - return ic.data.Compose.DataDir.ValueString() -} - func NewInstanceResource() resource.Resource { return &InstanceResource{} } -// InstanceResource defines the resource implementation. - type InstanceResource struct { clientManager *client.ClientManager } -// InstanceResourceModel describes the resource data model. type InstanceResourceModel struct { Client struct { Type types.String `tfsdk:"type"` @@ -45,6 +39,27 @@ type InstanceResourceModel struct { LibDir types.String `tfsdk:"lib_dir"` InstanceId types.String `tfsdk:"instance_id"` } `tfsdk:"compose"` + Instances types.List `tfsdk:"instances"` +} + +type InstanceStatusItemModel struct { + ID types.String `tfsdk:"id"` + URL types.String `tfsdk:"url"` + AemVersion types.String `tfsdk:"aem_version"` + Dir types.String `tfsdk:"dir"` + Attributes types.List `tfsdk:"attributes"` + RunModes types.List `tfsdk:"run_modes"` +} + +func (o InstanceStatusItemModel) attrTypes() map[string]attr.Type { + return map[string]attr.Type{ + "id": types.StringType, + "url": types.StringType, + "aem_version": types.StringType, + "dir": types.StringType, + "attributes": types.ListType{ElemType: types.StringType}, + "run_modes": types.ListType{ElemType: types.StringType}, + } } func (r *InstanceResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { @@ -97,6 +112,36 @@ func (r *InstanceResource) Schema(ctx context.Context, req resource.SchemaReques }, }, }, + + Attributes: map[string]schema.Attribute{ + "instances": schema.ListNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "url": schema.StringAttribute{ + Computed: true, + }, + "aem_version": schema.StringAttribute{ + Computed: true, + }, + "attributes": schema.ListAttribute{ + ElementType: types.StringType, + Computed: true, + }, + "run_modes": schema.ListAttribute{ + ElementType: types.StringType, + Computed: true, + }, + "dir": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + }, } } @@ -124,149 +169,131 @@ func (r *InstanceResource) Configure(ctx context.Context, req resource.Configure } func (r *InstanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - - var data InstanceResourceModel + model := r.defaultModel() // Read Terraform plan data into the model - resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } tflog.Info(ctx, "Creating AEM instance resource") - tflog.Info(ctx, "Connecting to AEM instance machine") - typeName := data.Client.Type.ValueString() - var settings map[string]string - data.Client.Settings.ElementsAs(ctx, &settings, true) - - cl, err := r.clientManager.Make(typeName, settings) + ic, err := r.Client(ctx, model, time.Minute*5) if err != nil { - resp.Diagnostics.AddError("Unable to determine AEM instance client", fmt.Sprintf("%s", err)) - return - } - - cl.Env["AEM_CLI_VERSION"] = data.Compose.Version.ValueString() - if err := cl.SetupEnv(); err != nil { - resp.Diagnostics.AddError("Unable to setup shell environment script", fmt.Sprintf("%s", err)) + resp.Diagnostics.AddError("Unable to connect to AEM instance", fmt.Sprintf("%s", err)) return } - - if err := cl.ConnectWithRetry(func() { tflog.Info(ctx, "Awaiting connection to AEM instance machine") }); err != nil { - resp.Diagnostics.AddError("Unable to connect to AEM instance machine", fmt.Sprintf("%s", err)) - return - } - tflog.Info(ctx, "Connected to AEM instance machine") - - ic := InstanceCreateContext{cl, ctx, data, req, resp} - - defer func(client *client.Client) { - err := client.Disconnect() + defer func(ic *InstanceClient) { + err := ic.Close() if err != nil { - resp.Diagnostics.AddWarning("Unable to disconnect from AEM instance machine", fmt.Sprintf("%s", err)) + resp.Diagnostics.AddWarning("Unable to disconnect from AEM instance", fmt.Sprintf("%s", err)) } - }(cl) + }(ic) - if !r.prepareDataDir(ic) { + if err := ic.prepareDataDir(); err != nil { + resp.Diagnostics.AddError("Unable to prepare AEM data directory", fmt.Sprintf("%s", err)) return } - if !r.installCompose(ic) { + if err := ic.installComposeWrapper(); err != nil { + resp.Diagnostics.AddError("Unable to install AEM Compose CLI", fmt.Sprintf("%s", err)) return } - if !r.copyConfigFile(ic) { + if err := ic.copyConfigFile(); err != nil { + resp.Diagnostics.AddError("Unable to copy AEM configuration file", fmt.Sprintf("%s", err)) return } - if !r.copyLibraryDir(ic) { + if err := ic.copyLibraryDir(); err != nil { + resp.Diagnostics.AddError("Unable to copy AEM library dir", fmt.Sprintf("%s", err)) return } - if !r.launch(ic) { + if err := ic.create(); err != nil { + resp.Diagnostics.AddError("Unable to create AEM instance", fmt.Sprintf("%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") - - // Write logs using the tflog package - // Documentation: https://terraform.io/plugin/log - tflog.Info(ctx, "Created AEM instance resource") - - // Save data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) -} - -// TODO chown data dir to ssh user or aem user (create him maybe) -func (r *InstanceResource) prepareDataDir(ic InstanceCreateContext) bool { - if _, err := ic.cl.RunShell(fmt.Sprintf("rm -fr %s", ic.DataDir())); err != nil { - ic.resp.Diagnostics.AddError("Cannot clean up AEM data directory", fmt.Sprintf("%s", err)) - return false + if err := ic.launch(); err != nil { + resp.Diagnostics.AddError("Unable to launch AEM instance", fmt.Sprintf("%s", err)) + return } - if _, err := ic.cl.RunShell(fmt.Sprintf("mkdir -p %s", ic.DataDir())); err != nil { - ic.resp.Diagnostics.AddError("Cannot create AEM data directory", fmt.Sprintf("%s", err)) - return false - } - return true -} + tflog.Info(ctx, "Created AEM instance resource") -func (r *InstanceResource) installCompose(ic InstanceCreateContext) bool { - out, err := ic.cl.RunShellWithEnv(fmt.Sprintf("cd %s && curl -s https://raw.githubusercontent.com/wttech/aemc/main/project-init.sh | sh", ic.DataDir())) - tflog.Info(ic.ctx, string(out)) + status, err := ic.ReadStatus() if err != nil { - ic.resp.Diagnostics.AddError("Unable to install AEM Compose CLI", fmt.Sprintf("%s", err)) - return false + resp.Diagnostics.AddError("Unable to read AEM instance data", fmt.Sprintf("%s", err)) + return } - return true -} -func (r *InstanceResource) copyConfigFile(ic InstanceCreateContext) bool { - configFile := ic.data.Compose.ConfigFile.ValueString() - if err := ic.cl.FileCopy(configFile, fmt.Sprintf("%s/aem/default/etc/aem.yml", ic.DataDir()), true); err != nil { - ic.resp.Diagnostics.AddError("Unable to copy AEM configuration file", fmt.Sprintf("%s", err)) - return false - } - return true -} + resp.Diagnostics.Append(r.fillModelWithStatus(ctx, &model, status)...) -func (r *InstanceResource) copyLibraryDir(ic InstanceCreateContext) bool { - localLibDir := ic.data.Compose.LibDir.ValueString() - remoteLibDir := fmt.Sprintf("%s/aem/home/lib", ic.DataDir()) - if err := ic.cl.DirCopy(localLibDir, remoteLibDir, false); err != nil { - ic.resp.Diagnostics.AddError("Unable to copy AEM library dir", fmt.Sprintf("%s", err)) - return false - } - return true + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) } -func (r *InstanceResource) launch(ic InstanceCreateContext) bool { - tflog.Info(ic.ctx, "Launching AEM instance(s)") - - // TODO register systemd service instead and start it - ymlBytes, err := ic.cl.RunShellWithEnv(fmt.Sprintf("cd %s && sh aemw instance launch", ic.DataDir())) +func (r *InstanceResource) defaultModel() InstanceResourceModel { + model := InstanceResourceModel{} + model.Instances = types.ListValueMust(types.ObjectType{AttrTypes: InstanceStatusItemModel{}.attrTypes()}, []attr.Value{}) + return model +} - if err != nil { - ic.resp.Diagnostics.AddError("Unable to launch AEM instance", fmt.Sprintf("%s", err)) - return false +func (r *InstanceResource) fillModelWithStatus(ctx context.Context, model *InstanceResourceModel, status InstanceStatus) diag.Diagnostics { + var allDiags diag.Diagnostics + + instances := make([]InstanceStatusItemModel, len(status.Data.Instances)) + for i, instance := range status.Data.Instances { + attributeList, diags := types.ListValueFrom(ctx, types.StringType, instance.Attributes) + allDiags.Append(diags...) + runModeList, diags := types.ListValueFrom(ctx, types.StringType, instance.RunModes) + allDiags.Append(diags...) + + instances[i] = InstanceStatusItemModel{ + ID: types.StringValue(instance.ID), + URL: types.StringValue(instance.URL), + AemVersion: types.StringValue(instance.AemVersion), + Dir: types.StringValue(instance.Dir), + Attributes: attributeList, + RunModes: runModeList, + } } - yml := string(ymlBytes) // TODO parse it and add to state + instanceList, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: InstanceStatusItemModel{}.attrTypes()}, instances) + allDiags.Append(diags...) + model.Instances = instanceList - tflog.Info(ic.ctx, "Launched AEM instance(s)") - tflog.Info(ic.ctx, yml) // TODO parse output; add it as data to the state; consider checking 'changed' flag from AEMCLI - return true + return allDiags } func (r *InstanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var data InstanceResourceModel + model := r.defaultModel() // Read Terraform prior state data into the model - resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - + resp.Diagnostics.Append(req.State.Get(ctx, &model)...) if resp.Diagnostics.HasError() { return } + 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 { + defer func(ic *InstanceClient) { + err := ic.Close() + if err != nil { + resp.Diagnostics.AddWarning("Unable to disconnect from AEM instance", fmt.Sprintf("%s", err)) + } + }(ic) + + status, err := ic.ReadStatus() + if err != nil { // + resp.Diagnostics.AddError("Unable to read AEM instance data", fmt.Sprintf("%s", err)) + return + } + + resp.Diagnostics.Append(r.fillModelWithStatus(ctx, &model, status)...) + } + // Save updated data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) } func (r *InstanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { @@ -299,3 +326,30 @@ func (r *InstanceResource) Delete(ctx context.Context, req resource.DeleteReques func (r *InstanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } + +func (r *InstanceResource) Client(ctx context.Context, data InstanceResourceModel, timeout time.Duration) (*InstanceClient, error) { + tflog.Info(ctx, "Connecting to AEM instance machine") + + typeName := data.Client.Type.ValueString() + var settings map[string]string + data.Client.Settings.ElementsAs(ctx, &settings, true) + + cl, err := r.clientManager.Make(typeName, settings) + if err != nil { + return nil, err + } + + if err := cl.ConnectWithRetry(timeout, func() { tflog.Info(ctx, "Awaiting connection to AEM instance machine") }); err != nil { + return nil, err + } + + cl.Env["AEM_CLI_VERSION"] = data.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 +} diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..8b4a8a9 --- /dev/null +++ b/run.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +TF_DIR=$1 +if [ -z "$TF_DIR" ] +then + echo "Usage: $0 " + exit 1 +fi + +TF_CLI_CONFIG_FILE="$(pwd)/dev_overrides.tfrc" + +(export TF_CLI_CONFIG_FILE && cd "$TF_DIR" && terraform "${@:2}") diff --git a/show.sh b/show.sh new file mode 100755 index 0000000..53039fc --- /dev/null +++ b/show.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +sh run.sh examples/ssh show -json | python -m json.tool