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 +}