From f2724df21c3755c8460794d32007bad6b7a28702 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Tue, 17 Oct 2023 23:09:21 +0200 Subject: [PATCH 01/11] Instance data read --- README.md | 6 ++- internal/provider/instance_resource.go | 55 ++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 4 deletions(-) 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/internal/provider/instance_resource.go b/internal/provider/instance_resource.go index 5c914ac..6aaddff 100644 --- a/internal/provider/instance_resource.go +++ b/internal/provider/instance_resource.go @@ -26,13 +26,10 @@ 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 +42,19 @@ type InstanceResourceModel struct { LibDir types.String `tfsdk:"lib_dir"` InstanceId types.String `tfsdk:"instance_id"` } `tfsdk:"compose"` + Data InstanceResourceDataModel `tfsdk:"data"` +} + +type InstanceResourceDataModel struct { + Instances []struct { + ID types.String `yaml:"id" tfsdk:"id"` + URL types.String `yaml:"url" tfsdk:"url"` + AemVersion types.String `yaml:"aem_version" tfsdk:"aem_version"` + Attributes []types.String `yaml:"attributes" tfsdk:"attributes"` + RunModes []types.String `yaml:"run_modes" tfsdk:"run_modes"` + HealthChecks []types.String `yaml:"health_checks" tfsdk:"health_checks"` + Dir types.String `yaml:"dir" tfsdk:"dir"` + } `json:"instances" tfsdk:"instances"` } func (r *InstanceResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { @@ -96,6 +106,41 @@ func (r *InstanceResource) Schema(ctx context.Context, req resource.SchemaReques }, }, }, + "data": schema.SingleNestedBlock{ + 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, + }, + "health_checks": schema.ListAttribute{ + ElementType: types.StringType, + Computed: true, + }, + "dir": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + }, + }, }, } } @@ -191,6 +236,10 @@ func (r *InstanceResource) Create(ctx context.Context, req resource.CreateReques // Documentation: https://terraform.io/plugin/log tflog.Info(ctx, "Created AEM instance resource") + var dataRead InstanceResourceDataModel + // TODO request data from command 'sh aemw instance status --output-format yaml' + data.Data = dataRead + // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } From 351af25fb06fde311afa2d3764324ae99252666c Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Thu, 26 Oct 2023 23:07:04 +0200 Subject: [PATCH 02/11] Instance client --- go.mod | 7 +- go.sum | 3 +- internal/client/client.go | 2 +- internal/provider/client_context.go | 21 +-- internal/provider/instance_client.go | 84 +++++++++++ internal/provider/instance_resource.go | 201 +++++++++++-------------- 6 files changed, 180 insertions(+), 138 deletions(-) create mode 100644 internal/provider/instance_client.go 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..b0fa255 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -73,7 +73,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) 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..4bd91c1 --- /dev/null +++ b/internal/provider/instance_client.go @@ -0,0 +1,84 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +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 { + 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) installCompose() error { + 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)) + if err != nil { + return fmt.Errorf("cannot install AEM Compose CLI: %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 +} diff --git a/internal/provider/instance_resource.go b/internal/provider/instance_resource.go index 6aaddff..f22fb8c 100644 --- a/internal/provider/instance_resource.go +++ b/internal/provider/instance_resource.go @@ -10,18 +10,13 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/wttech/terraform-provider-aem/internal/client" + "gopkg.in/yaml.v3" ) // 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{} } @@ -42,10 +37,10 @@ type InstanceResourceModel struct { LibDir types.String `tfsdk:"lib_dir"` InstanceId types.String `tfsdk:"instance_id"` } `tfsdk:"compose"` - Data InstanceResourceDataModel `tfsdk:"data"` + Status *InstanceStatusModel `tfsdk:"status"` } -type InstanceResourceDataModel struct { +type InstanceStatusModel struct { Instances []struct { ID types.String `yaml:"id" tfsdk:"id"` URL types.String `yaml:"url" tfsdk:"url"` @@ -54,7 +49,7 @@ type InstanceResourceDataModel struct { RunModes []types.String `yaml:"run_modes" tfsdk:"run_modes"` HealthChecks []types.String `yaml:"health_checks" tfsdk:"health_checks"` Dir types.String `yaml:"dir" tfsdk:"dir"` - } `json:"instances" tfsdk:"instances"` + } `yaml:"instances" tfsdk:"instances"` } func (r *InstanceResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { @@ -106,7 +101,7 @@ func (r *InstanceResource) Schema(ctx context.Context, req resource.SchemaReques }, }, }, - "data": schema.SingleNestedBlock{ + "status": schema.SingleNestedBlock{ Attributes: map[string]schema.Attribute{ "instances": schema.ListNestedAttribute{ Computed: true, @@ -180,128 +175,48 @@ func (r *InstanceResource) Create(ctx context.Context, req resource.CreateReques 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, data) if err != nil { - resp.Diagnostics.AddError("Unable to determine AEM instance client", fmt.Sprintf("%s", err)) + resp.Diagnostics.AddError("Unable to connect to AEM instance", 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)) - 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.copyConfigFile(); err != nil { + resp.Diagnostics.AddError("Unable to copy AEM configuration file", fmt.Sprintf("%s", err)) return } - if !r.installCompose(ic) { + if err := ic.copyLibraryDir(); err != nil { + resp.Diagnostics.AddError("Unable to copy AEM library dir", fmt.Sprintf("%s", err)) return } - if !r.copyConfigFile(ic) { + if err := ic.create(); err != nil { + resp.Diagnostics.AddError("Unable to create AEM instance", fmt.Sprintf("%s", err)) return } - if !r.copyLibraryDir(ic) { + /* TODO systemd and stuff for later + if err := ic.launch(); err != nil { + resp.Diagnostics.AddError("Unable to launch AEM instance", fmt.Sprintf("%s", err)) return } - if !r.launch(ic) { - 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") - var dataRead InstanceResourceDataModel - // TODO request data from command 'sh aemw instance status --output-format yaml' - data.Data = dataRead - - // 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.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 -} - -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 - } - 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 -} - -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 -} - -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())) - - if err != nil { - ic.resp.Diagnostics.AddError("Unable to launch AEM instance", fmt.Sprintf("%s", err)) - return false + resp.Diagnostics.AddError("Unable to read AEM instance data", fmt.Sprintf("%s", err)) + return } - yml := string(ymlBytes) // TODO parse it and add to state + data.Status = &status - 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 + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func (r *InstanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { @@ -309,11 +224,32 @@ func (r *InstanceResource) Read(ctx context.Context, req resource.ReadRequest, r // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - if resp.Diagnostics.HasError() { return } + // TODO connect and read status when instance is running + /* + ic, err := r.Client(ctx, data) + if err != nil { + resp.Diagnostics.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)) + } + }(ic) + + dataRead, err := ic.ReadStatus() + if err != nil { + resp.Diagnostics.AddError("Unable to read AEM instance data", fmt.Sprintf("%s", err)) + return + } + data.Status = &dataRead + */ + // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -348,3 +284,42 @@ 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 (ic *InstanceClient) ReadStatus() (InstanceStatusModel, error) { + var status InstanceStatusModel + yamlBytes, err := ic.cl.RunShellWithEnv("sh aemw instance status --output-format yaml") + 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 +} + +func (r *InstanceResource) Client(ctx context.Context, data InstanceResourceModel) (*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(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 = data.Compose.DataDir.ValueString() + + if err := cl.SetupEnv(); err != nil { + return nil, err + } + + tflog.Info(ctx, "Connected to AEM instance machine") + return &InstanceClient{cl, ctx, data}, nil +} From 36a5503a86e1dd2b65a257fab13a911b36bcfded Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Thu, 26 Oct 2023 23:41:20 +0200 Subject: [PATCH 03/11] Bug fixes --- internal/provider/instance_client.go | 2 ++ internal/provider/instance_resource.go | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/internal/provider/instance_client.go b/internal/provider/instance_client.go index 4bd91c1..da2ba2c 100644 --- a/internal/provider/instance_client.go +++ b/internal/provider/instance_client.go @@ -17,9 +17,11 @@ func (ic *InstanceClient) Close() error { // 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) } diff --git a/internal/provider/instance_resource.go b/internal/provider/instance_resource.go index f22fb8c..466e3e7 100644 --- a/internal/provider/instance_resource.go +++ b/internal/provider/instance_resource.go @@ -187,6 +187,14 @@ func (r *InstanceResource) Create(ctx context.Context, req resource.CreateReques } }(ic) + if err := ic.prepareDataDir(); err != nil { + resp.Diagnostics.AddError("Unable to prepare AEM data directory", fmt.Sprintf("%s", err)) + return + } + if err := ic.installCompose(); err != nil { + resp.Diagnostics.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)) return @@ -314,7 +322,7 @@ func (r *InstanceResource) Client(ctx context.Context, data InstanceResourceMode } cl.Env["AEM_CLI_VERSION"] = data.Compose.Version.ValueString() - cl.EnvDir = data.Compose.DataDir.ValueString() + cl.EnvDir = "/tmp" // TODO make configurable; or just in user home dir './' ? if err := cl.SetupEnv(); err != nil { return nil, err From 1990a1a5cd8c291199411a8f3295a00cdaf0bfcd Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Thu, 26 Oct 2023 23:59:54 +0200 Subject: [PATCH 04/11] State to be working --- internal/provider/instance_client.go | 4 ++-- internal/provider/instance_resource.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/provider/instance_client.go b/internal/provider/instance_client.go index da2ba2c..e011bde 100644 --- a/internal/provider/instance_client.go +++ b/internal/provider/instance_client.go @@ -28,8 +28,8 @@ func (ic *InstanceClient) prepareDataDir() error { return nil } -func (ic *InstanceClient) installCompose() error { - out, err := ic.cl.RunShellWithEnv(fmt.Sprintf("cd %s && curl -s https://raw.githubusercontent.com/wttech/aemc/main/project-init.sh | sh", ic.DataDir())) +func (ic *InstanceClient) installCompose() error { // TODO do not rely on github script here maybe + out, err := ic.cl.RunShellWithEnv(fmt.Sprintf("cd %s && curl -s https://raw.githubusercontent.com/wttech/aemc/main/project-init.sh?token=1 | sh", ic.DataDir())) tflog.Info(ic.ctx, string(out)) if err != nil { return fmt.Errorf("cannot install AEM Compose CLI: %w", err) diff --git a/internal/provider/instance_resource.go b/internal/provider/instance_resource.go index 466e3e7..84c6792 100644 --- a/internal/provider/instance_resource.go +++ b/internal/provider/instance_resource.go @@ -295,7 +295,7 @@ func (r *InstanceResource) ImportState(ctx context.Context, req resource.ImportS func (ic *InstanceClient) ReadStatus() (InstanceStatusModel, error) { var status InstanceStatusModel - yamlBytes, err := ic.cl.RunShellWithEnv("sh aemw instance status --output-format yaml") + yamlBytes, err := ic.cl.RunShellWithEnv(fmt.Sprintf("cd %s && sh aemw instance status --output-format yaml", ic.DataDir())) if err != nil { return status, err } From 8d99ad771c2be0d6df33abf080b74e47ba04e1db Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Fri, 27 Oct 2023 18:02:11 +0200 Subject: [PATCH 05/11] Imprs --- internal/client/client.go | 10 ++++++- internal/provider/instance_client.go | 41 +++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/internal/client/client.go b/internal/client/client.go index b0fa255..5d8ec15 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -132,7 +132,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 +147,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/instance_client.go b/internal/provider/instance_client.go index e011bde..3748a24 100644 --- a/internal/provider/instance_client.go +++ b/internal/provider/instance_client.go @@ -3,6 +3,7 @@ package provider import ( "fmt" "github.com/hashicorp/terraform-plugin-log/tflog" + "gopkg.in/yaml.v3" ) type InstanceClient ClientContext[InstanceResourceModel] @@ -28,11 +29,17 @@ func (ic *InstanceClient) prepareDataDir() error { return nil } -func (ic *InstanceClient) installCompose() error { // TODO do not rely on github script here maybe - out, err := ic.cl.RunShellWithEnv(fmt.Sprintf("cd %s && curl -s https://raw.githubusercontent.com/wttech/aemc/main/project-init.sh?token=1 | sh", ic.DataDir())) - tflog.Info(ic.ctx, string(out)) +func (ic *InstanceClient) installComposeWrapper() error { + exists, err := ic.cl.FileExists(fmt.Sprintf("%s/aemw", ic.DataDir())) if err != nil { - return fmt.Errorf("cannot install AEM Compose CLI: %w", err) + 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 } @@ -84,3 +91,29 @@ func (ic *InstanceClient) launch() error { 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 +} From 2d1502f3813c4579dc076c33e20f5da6b157c5f1 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Fri, 27 Oct 2023 21:22:14 +0200 Subject: [PATCH 06/11] Something works --- internal/provider/instance_resource.go | 233 ++++++++++++++++--------- 1 file changed, 149 insertions(+), 84 deletions(-) diff --git a/internal/provider/instance_resource.go b/internal/provider/instance_resource.go index 84c6792..c43c52c 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,7 +12,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/wttech/terraform-provider-aem/internal/client" - "gopkg.in/yaml.v3" ) // Ensure provider defined types fully satisfy framework interfaces. @@ -37,19 +38,23 @@ type InstanceResourceModel struct { LibDir types.String `tfsdk:"lib_dir"` InstanceId types.String `tfsdk:"instance_id"` } `tfsdk:"compose"` - Status *InstanceStatusModel `tfsdk:"status"` + Instances types.List `tfsdk:"instances"` } -type InstanceStatusModel struct { - Instances []struct { - ID types.String `yaml:"id" tfsdk:"id"` - URL types.String `yaml:"url" tfsdk:"url"` - AemVersion types.String `yaml:"aem_version" tfsdk:"aem_version"` - Attributes []types.String `yaml:"attributes" tfsdk:"attributes"` - RunModes []types.String `yaml:"run_modes" tfsdk:"run_modes"` - HealthChecks []types.String `yaml:"health_checks" tfsdk:"health_checks"` - Dir types.String `yaml:"dir" tfsdk:"dir"` - } `yaml:"instances" tfsdk:"instances"` +type InstanceStatusItemModel struct { + //ID types.String `tfsdk:"id"` + URL types.String `tfsdk:"url"` + //AemVersion types.String `tfsdk:"aem_version"` + //Attributes []types.String `tfsdk:"attributes"` + //RunModes []types.String `tfsdk:"run_modes"` + //HealthChecks []types.String `tfsdk:"health_checks"` + //Dir types.String `tfsdk:"dir"` +} + +func (o InstanceStatusItemModel) attrTypes() map[string]attr.Type { + return map[string]attr.Type{ + "url": types.StringType, + } } func (r *InstanceResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { @@ -101,42 +106,75 @@ func (r *InstanceResource) Schema(ctx context.Context, req resource.SchemaReques }, }, }, - "status": schema.SingleNestedBlock{ - 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, - }, - "health_checks": schema.ListAttribute{ - ElementType: types.StringType, - Computed: true, - }, - "dir": schema.StringAttribute{ - Computed: true, - }, - }, + }, + + 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, + //}, + //"health_checks": schema.ListAttribute{ + // ElementType: types.StringType, + // Computed: true, + //}, + //"dir": schema.StringAttribute{ + // Computed: true, + //}, }, }, }, }, + // + //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, + // //}, + // //"health_checks": schema.ListAttribute{ + // // ElementType: types.StringType, + // // Computed: true, + // //}, + // //"dir": schema.StringAttribute{ + // // Computed: true, + // //}, + // }, + // }, + // }, } } @@ -164,18 +202,18 @@ 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") - ic, err := r.Client(ctx, data) + ic, err := r.Client(ctx, model) if err != nil { resp.Diagnostics.AddError("Unable to connect to AEM instance", fmt.Sprintf("%s", err)) return @@ -191,7 +229,7 @@ func (r *InstanceResource) Create(ctx context.Context, req resource.CreateReques resp.Diagnostics.AddError("Unable to prepare AEM data directory", fmt.Sprintf("%s", err)) return } - if err := ic.installCompose(); err != nil { + if err := ic.installComposeWrapper(); err != nil { resp.Diagnostics.AddError("Unable to install AEM Compose CLI", fmt.Sprintf("%s", err)) return } @@ -221,45 +259,84 @@ func (r *InstanceResource) Create(ctx context.Context, req resource.CreateReques resp.Diagnostics.AddError("Unable to read AEM instance data", fmt.Sprintf("%s", err)) return } - data.Status = &status + + resp.Diagnostics.Append(r.fillModelWithStatus(ctx, &model, status)...) // Save data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) } +func (r *InstanceResource) defaultModel() InstanceResourceModel { + return InstanceResourceModel{} +} + +func (r *InstanceResource) fillModelWithStatus(ctx context.Context, model *InstanceResourceModel, status InstanceStatus) diag.Diagnostics { + instances := make([]InstanceStatusItemModel, len(status.Data.Instances)) + for _, instance := range status.Data.Instances { + instances = append(instances, InstanceStatusItemModel{ + URL: types.StringValue(instance.URL), + }) + } + instanceList, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: InstanceStatusItemModel{}.attrTypes()}, instances) + model.Instances = instanceList + return diags +} + +//func (r *InstanceResource) fillModelWithStatus(model *InstanceResourceModel, status InstanceStatus) { +// model.Instances = make([]InstanceStatusItemModel, len(status.Data.Instances)) +// for i, _ := range status.Data.Instances { +// model.Instances[i] = InstanceStatusItemModel{ +// //ID: types.StringValue(instance.ID), +// //URL: types.StringValue(instance.URL), +// //AemVersion: types.StringValue(instance.AemVersion), +// //Attributes: make([]types.String, len(instance.Attributes)), +// //RunModes: make([]types.String, len(instance.RunModes)), +// //HealthChecks: make([]types.String, len(instance.HealthChecks)), +// //Dir: types.StringValue(instance.Dir), +// } +// //for j, attr := range instance.Attributes { +// // model.Instances[i].Attributes[j] = types.StringValue(attr) +// //} +// //for j, runMode := range instance.RunModes { +// // model.Instances[i].RunModes[j] = types.StringValue(runMode) +// //} +// //for j, healthCheck := range instance.HealthChecks { +// // model.Instances[i].HealthChecks[j] = types.StringValue(healthCheck) +// //} +// } +//} + 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 } - // TODO connect and read status when instance is running - /* - ic, err := r.Client(ctx, data) - if err != nil { - resp.Diagnostics.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)) - } - }(ic) - - dataRead, err := ic.ReadStatus() + ic, err := r.Client(ctx, model) + if err != nil { + resp.Diagnostics.AddError("Unable to connect to AEM instance", fmt.Sprintf("%s", err)) + return + } + defer func(ic *InstanceClient) { + err := ic.Close() if err != nil { - resp.Diagnostics.AddError("Unable to read AEM instance data", fmt.Sprintf("%s", err)) - return + resp.Diagnostics.AddWarning("Unable to disconnect from AEM instance", fmt.Sprintf("%s", err)) } - data.Status = &dataRead - */ + }(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) { @@ -293,18 +370,6 @@ func (r *InstanceResource) ImportState(ctx context.Context, req resource.ImportS resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } -func (ic *InstanceClient) ReadStatus() (InstanceStatusModel, error) { - var status InstanceStatusModel - 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 -} - func (r *InstanceResource) Client(ctx context.Context, data InstanceResourceModel) (*InstanceClient, error) { tflog.Info(ctx, "Connecting to AEM instance machine") From 2aa8c7ac31a272fce69d089b59f1020c9218a030 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Fri, 27 Oct 2023 23:37:47 +0200 Subject: [PATCH 07/11] Instance attributes --- apply.sh | 3 + develop.sh | 2 +- internal/provider/instance_resource.go | 133 +++++++++---------------- run.sh | 12 +++ show.sh | 3 + 5 files changed, 68 insertions(+), 85 deletions(-) create mode 100755 apply.sh create mode 100755 run.sh create mode 100755 show.sh 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/internal/provider/instance_resource.go b/internal/provider/instance_resource.go index c43c52c..17c0319 100644 --- a/internal/provider/instance_resource.go +++ b/internal/provider/instance_resource.go @@ -42,18 +42,24 @@ type InstanceResourceModel struct { } type InstanceStatusItemModel struct { - //ID types.String `tfsdk:"id"` - URL types.String `tfsdk:"url"` - //AemVersion types.String `tfsdk:"aem_version"` - //Attributes []types.String `tfsdk:"attributes"` - //RunModes []types.String `tfsdk:"run_modes"` - //HealthChecks []types.String `tfsdk:"health_checks"` - //Dir types.String `tfsdk:"dir"` + 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"` + //HealthChecks types.List `tfsdk:"health_checks"` } func (o InstanceStatusItemModel) attrTypes() map[string]attr.Type { return map[string]attr.Type{ - "url": types.StringType, + "id": types.StringType, + "url": types.StringType, + "aem_version": types.StringType, + "dir": types.StringType, + "attributes": types.ListType{ElemType: types.StringType}, + //"health_checks": types.ListType{ElemType: types.StringType}, + //"run_modes": types.ListType{ElemType: types.StringType}, } } @@ -113,19 +119,19 @@ func (r *InstanceResource) Schema(ctx context.Context, req resource.SchemaReques Computed: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ - //"id": schema.StringAttribute{ - // Computed: true, - //}, + "id": schema.StringAttribute{ + Computed: true, + }, "url": schema.StringAttribute{ Computed: true, }, - //"aem_version": schema.StringAttribute{ - // Computed: true, - //}, - //"attributes": schema.ListAttribute{ - // ElementType: types.StringType, - // 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, @@ -134,47 +140,13 @@ func (r *InstanceResource) Schema(ctx context.Context, req resource.SchemaReques // ElementType: types.StringType, // Computed: true, //}, - //"dir": schema.StringAttribute{ - // Computed: true, - //}, + "dir": schema.StringAttribute{ + Computed: true, + }, }, }, }, }, - // - //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, - // //}, - // //"health_checks": schema.ListAttribute{ - // // ElementType: types.StringType, - // // Computed: true, - // //}, - // //"dir": schema.StringAttribute{ - // // Computed: true, - // //}, - // }, - // }, - // }, } } @@ -271,40 +243,33 @@ func (r *InstanceResource) defaultModel() InstanceResourceModel { } 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 _, instance := range status.Data.Instances { - instances = append(instances, InstanceStatusItemModel{ - URL: types.StringValue(instance.URL), - }) + 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...) + //healthCheckList, diags := types.ListValueFrom(ctx, types.StringType, instance.HealthChecks) + //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, + //HealthChecks: healthCheckList, + } } instanceList, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: InstanceStatusItemModel{}.attrTypes()}, instances) + allDiags.Append(diags...) model.Instances = instanceList - return diags -} -//func (r *InstanceResource) fillModelWithStatus(model *InstanceResourceModel, status InstanceStatus) { -// model.Instances = make([]InstanceStatusItemModel, len(status.Data.Instances)) -// for i, _ := range status.Data.Instances { -// model.Instances[i] = InstanceStatusItemModel{ -// //ID: types.StringValue(instance.ID), -// //URL: types.StringValue(instance.URL), -// //AemVersion: types.StringValue(instance.AemVersion), -// //Attributes: make([]types.String, len(instance.Attributes)), -// //RunModes: make([]types.String, len(instance.RunModes)), -// //HealthChecks: make([]types.String, len(instance.HealthChecks)), -// //Dir: types.StringValue(instance.Dir), -// } -// //for j, attr := range instance.Attributes { -// // model.Instances[i].Attributes[j] = types.StringValue(attr) -// //} -// //for j, runMode := range instance.RunModes { -// // model.Instances[i].RunModes[j] = types.StringValue(runMode) -// //} -// //for j, healthCheck := range instance.HealthChecks { -// // model.Instances[i].HealthChecks[j] = types.StringValue(healthCheck) -// //} -// } -//} + return allDiags +} func (r *InstanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { model := r.defaultModel() 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 From a880d1a90b91427bb21cbcb279cf47f647235364 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Sat, 28 Oct 2023 00:02:12 +0200 Subject: [PATCH 08/11] Run modes --- examples/ssh/aem.tf | 5 +++++ internal/provider/instance_resource.go | 27 +++++++++----------------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/examples/ssh/aem.tf b/examples/ssh/aem.tf index 34638c4..12575ba 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_instance_single" { + value = aem_instance.single.instances +} diff --git a/internal/provider/instance_resource.go b/internal/provider/instance_resource.go index 17c0319..4ca7424 100644 --- a/internal/provider/instance_resource.go +++ b/internal/provider/instance_resource.go @@ -47,8 +47,7 @@ type InstanceStatusItemModel struct { AemVersion types.String `tfsdk:"aem_version"` Dir types.String `tfsdk:"dir"` Attributes types.List `tfsdk:"attributes"` - //RunModes types.List `tfsdk:"run_modes"` - //HealthChecks types.List `tfsdk:"health_checks"` + RunModes types.List `tfsdk:"run_modes"` } func (o InstanceStatusItemModel) attrTypes() map[string]attr.Type { @@ -58,8 +57,7 @@ func (o InstanceStatusItemModel) attrTypes() map[string]attr.Type { "aem_version": types.StringType, "dir": types.StringType, "attributes": types.ListType{ElemType: types.StringType}, - //"health_checks": types.ListType{ElemType: types.StringType}, - //"run_modes": types.ListType{ElemType: types.StringType}, + "run_modes": types.ListType{ElemType: types.StringType}, } } @@ -132,14 +130,10 @@ func (r *InstanceResource) Schema(ctx context.Context, req resource.SchemaReques ElementType: types.StringType, Computed: true, }, - //"run_modes": schema.ListAttribute{ - // ElementType: types.StringType, - // Computed: true, - //}, - //"health_checks": schema.ListAttribute{ - // ElementType: types.StringType, - // Computed: true, - //}, + "run_modes": schema.ListAttribute{ + ElementType: types.StringType, + Computed: true, + }, "dir": schema.StringAttribute{ Computed: true, }, @@ -249,10 +243,8 @@ func (r *InstanceResource) fillModelWithStatus(ctx context.Context, model *Insta 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...) - //healthCheckList, diags := types.ListValueFrom(ctx, types.StringType, instance.HealthChecks) - //allDiags.Append(diags...) + runModeList, diags := types.ListValueFrom(ctx, types.StringType, instance.RunModes) + allDiags.Append(diags...) instances[i] = InstanceStatusItemModel{ ID: types.StringValue(instance.ID), @@ -260,8 +252,7 @@ func (r *InstanceResource) fillModelWithStatus(ctx context.Context, model *Insta AemVersion: types.StringValue(instance.AemVersion), Dir: types.StringValue(instance.Dir), Attributes: attributeList, - //RunModes: runModeList, - //HealthChecks: healthCheckList, + RunModes: runModeList, } } instanceList, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: InstanceStatusItemModel{}.attrTypes()}, instances) From 7fbda5c7b77d83011568bc18c999f6fa953a4900 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Sat, 28 Oct 2023 00:20:27 +0200 Subject: [PATCH 09/11] Launch works --- examples/ssh/aem.tf | 2 +- internal/provider/instance_resource.go | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/ssh/aem.tf b/examples/ssh/aem.tf index 12575ba..ba58475 100644 --- a/examples/ssh/aem.tf +++ b/examples/ssh/aem.tf @@ -19,6 +19,6 @@ resource "aem_instance" "single" { } -output "aem_instance_single" { +output "aem_instances" { value = aem_instance.single.instances } diff --git a/internal/provider/instance_resource.go b/internal/provider/instance_resource.go index 4ca7424..ee19cc7 100644 --- a/internal/provider/instance_resource.go +++ b/internal/provider/instance_resource.go @@ -211,12 +211,10 @@ func (r *InstanceResource) Create(ctx context.Context, req resource.CreateReques resp.Diagnostics.AddError("Unable to create AEM instance", fmt.Sprintf("%s", err)) return } - /* TODO systemd and stuff for later if err := ic.launch(); err != nil { resp.Diagnostics.AddError("Unable to launch AEM instance", fmt.Sprintf("%s", err)) return } - */ tflog.Info(ctx, "Created AEM instance resource") From 2fcb09a09a355cc4ecf3c884d41da87ed812b42a Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Sun, 29 Oct 2023 21:10:14 +0100 Subject: [PATCH 10/11] Create and read --- internal/client/client.go | 3 +- internal/provider/instance_resource.go | 43 ++++++++++++++------------ 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/internal/client/client.go b/internal/client/client.go index 5d8ec15..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 { diff --git a/internal/provider/instance_resource.go b/internal/provider/instance_resource.go index ee19cc7..2a2ad14 100644 --- a/internal/provider/instance_resource.go +++ b/internal/provider/instance_resource.go @@ -12,6 +12,7 @@ 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. @@ -179,7 +180,7 @@ func (r *InstanceResource) Create(ctx context.Context, req resource.CreateReques tflog.Info(ctx, "Creating AEM instance resource") - ic, err := r.Client(ctx, model) + 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)) return @@ -231,7 +232,9 @@ func (r *InstanceResource) Create(ctx context.Context, req resource.CreateReques } func (r *InstanceResource) defaultModel() InstanceResourceModel { - return InstanceResourceModel{} + model := InstanceResourceModel{} + model.Instances = types.ListValueMust(types.ObjectType{AttrTypes: InstanceStatusItemModel{}.attrTypes()}, []attr.Value{}) + return model } func (r *InstanceResource) fillModelWithStatus(ctx context.Context, model *InstanceResourceModel, status InstanceStatus) diag.Diagnostics { @@ -269,26 +272,26 @@ func (r *InstanceResource) Read(ctx context.Context, req resource.ReadRequest, r return } - ic, err := r.Client(ctx, model) - if err != nil { - resp.Diagnostics.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)) + 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 } - }(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)...) } - resp.Diagnostics.Append(r.fillModelWithStatus(ctx, &model, status)...) - // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) } @@ -324,7 +327,7 @@ 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) (*InstanceClient, error) { +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() @@ -336,7 +339,7 @@ func (r *InstanceResource) Client(ctx context.Context, data InstanceResourceMode return nil, err } - if err := cl.ConnectWithRetry(func() { tflog.Info(ctx, "Awaiting connection to AEM instance machine") }); err != nil { + if err := cl.ConnectWithRetry(timeout, func() { tflog.Info(ctx, "Awaiting connection to AEM instance machine") }); err != nil { return nil, err } From d05c0f15bce265a17f50d7c080820217fbdf0b7a Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Sun, 29 Oct 2023 21:11:28 +0100 Subject: [PATCH 11/11] Bug --- internal/provider/instance_resource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/provider/instance_resource.go b/internal/provider/instance_resource.go index 2a2ad14..2f35c90 100644 --- a/internal/provider/instance_resource.go +++ b/internal/provider/instance_resource.go @@ -273,7 +273,7 @@ func (r *InstanceResource) Read(ctx context.Context, req resource.ReadRequest, r } ic, err := r.Client(ctx, model, time.Second*15) - if err == nil { + if err != nil { resp.Diagnostics.AddWarning("Unable to connect to AEM instance", fmt.Sprintf("%s", err)) } else { defer func(ic *InstanceClient) {