diff --git a/go.mod b/go.mod index 2506cc6..566d0f7 100644 --- a/go.mod +++ b/go.mod @@ -63,5 +63,6 @@ require ( golang.org/x/sys v0.10.0 // indirect golang.org/x/text v0.11.0 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect + google.golang.org/grpc v1.56.1 // indirect google.golang.org/protobuf v1.31.0 // indirect ) diff --git a/internal/client/client.go b/internal/client/client.go index 67b1c4a..9c7ced5 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -66,22 +66,13 @@ func (c Client) Connection() Connection { return c.connection } -func (c Client) Run(cmdLine []string) (*goph.Cmd, error) { +func (c Client) Command(cmdLine []string) (*goph.Cmd, error) { return c.connection.Command(cmdLine) } func (c Client) SetupEnv() error { - file, err := os.CreateTemp(os.TempDir(), "tf-provider-aem-env-*.sh") - 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) - } - if _, err := file.WriteString(c.envScriptString()); err != nil { - return fmt.Errorf("cannot write temporary file for remote shell environment script: %w", err) - } - if err := c.FileCopy(path, c.envScriptPath(), true); err != nil { - return err + if err := c.FileWrite(c.envScriptPath(), c.envScriptString()); err != nil { + return fmt.Errorf("cannot setup environment script: %w", err) } return nil } @@ -101,29 +92,20 @@ func (c Client) envScriptString() string { return sb.String() } -func (c Client) RunShellWithEnv(cmd string) ([]byte, error) { - return c.RunShell(fmt.Sprintf("source %s && %s", c.envScriptPath(), cmd)) +func (c Client) RunShellCommand(cmd string) ([]byte, error) { + return c.RunShellPurely(fmt.Sprintf("source %s && %s", c.envScriptPath(), cmd)) } -func (c Client) RunShellScriptWithEnv(dir string, cmdScript string) ([]byte, error) { - file, err := os.CreateTemp(os.TempDir(), "tf-provider-aem-script-*.sh") - path := file.Name() - defer func() { _ = file.Close(); _ = os.Remove(path) }() - if err != nil { - return nil, fmt.Errorf("cannot create temporary file for remote shell script: %w", err) - } - if _, err := file.WriteString(cmdScript); err != nil { - return nil, fmt.Errorf("cannot write temporary file for remote shell script: %w", err) +func (c Client) RunShellScript(cmdName string, cmdScript string, dir string) ([]byte, error) { + remotePath := fmt.Sprintf("%s/%s.sh", c.EnvDir, cmdName) + if err := c.FileWrite(remotePath, cmdScript); err != nil { + return nil, fmt.Errorf("cannot write temporary script at remote path '%s': %w", remotePath, err) } - remotePath := fmt.Sprintf("%s/%s", c.EnvDir, filepath.Base(file.Name())) defer func() { _ = c.FileDelete(remotePath) }() - if err := c.FileCopy(path, remotePath, true); err != nil { - return nil, err - } - return c.RunShellWithEnv(fmt.Sprintf("cd %s && sh %s", dir, remotePath)) + return c.RunShellCommand(fmt.Sprintf("cd %s && sh %s", dir, remotePath)) } -func (c Client) RunShell(cmd string) ([]byte, error) { +func (c Client) RunShellPurely(cmd string) ([]byte, error) { cmdObj, err := c.connection.Command([]string{"sh", "-c", "\"" + cmd + "\""}) if err != nil { return nil, fmt.Errorf("cannot create command '%s': %w", cmd, err) @@ -139,7 +121,7 @@ func (c Client) RunShell(cmd string) ([]byte, error) { } func (c Client) DirEnsure(path string) error { - _, err := c.RunShell(fmt.Sprintf("mkdir -p %s", path)) + _, err := c.RunShellPurely(fmt.Sprintf("mkdir -p %s", path)) if err != nil { return fmt.Errorf("cannot ensure directory '%s': %w", path, err) } @@ -147,7 +129,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)) + out, err := c.RunShellPurely(fmt.Sprintf("test -f %s && echo '0' || echo '1'", path)) if err != nil { return false, fmt.Errorf("cannot check if file exists '%s': %w", path, err) } @@ -158,14 +140,14 @@ func (c Client) FileMove(oldPath string, newPath string) error { if err := c.DirEnsure(filepath.Dir(newPath)); err != nil { return err } - if _, err := c.RunShell(fmt.Sprintf("mv %s %s", oldPath, newPath)); err != nil { + if _, err := c.RunShellPurely(fmt.Sprintf("mv %s %s", oldPath, newPath)); err != nil { return fmt.Errorf("cannot move file '%s' to '%s': %w", oldPath, newPath, err) } return nil } func (c Client) DirExists(path string) (bool, error) { - out, err := c.RunShell(fmt.Sprintf("test -d %s && echo '0' || echo '1'", path)) + out, err := c.RunShellPurely(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) } @@ -197,13 +179,12 @@ func (c Client) DirCopy(localPath string, remotePath string, override bool) erro } func (c Client) FileDelete(path string) error { - if _, err := c.RunShell(fmt.Sprintf("rm -rf %s", path)); err != nil { + if _, err := c.RunShellPurely(fmt.Sprintf("rm -rf %s", path)); err != nil { return fmt.Errorf("cannot delete file '%s': %w", path, err) } return nil } -// TODO seems that if file exists it is not skipping copying file func (c Client) FileCopy(localPath string, remotePath string, override bool) error { if !override { exists, err := c.FileExists(remotePath) @@ -229,3 +210,30 @@ func (c Client) FileCopy(localPath string, remotePath string, override bool) err } return nil } + +func (c Client) PathCopy(localPath string, remotePath string, override bool) error { + stat, err := os.Stat(localPath) + if err != nil { + return fmt.Errorf("cannot stat path '%s': %w", localPath, err) + } + if stat.IsDir() { + return c.DirCopy(localPath, remotePath, override) + } + return c.FileCopy(localPath, remotePath, override) +} + +func (c Client) FileWrite(remotePath string, text string) error { + file, err := os.CreateTemp(os.TempDir(), "tf-provider-aem-*.tmp") + path := file.Name() + defer func() { _ = file.Close(); _ = os.Remove(path) }() + if err != nil { + return fmt.Errorf("cannot create local writable temporary file to be copied to remote path '%s': %w", remotePath, err) + } + if _, err := file.WriteString(text); err != nil { + return fmt.Errorf("cannot write text to local temporary file to be copied to remote path '%s': %w", remotePath, err) + } + if err := c.FileCopy(path, remotePath, true); err != nil { + return err + } + return nil +} diff --git a/internal/provider/instance_client.go b/internal/provider/instance_client.go index cec50e1..0a3ddb9 100644 --- a/internal/provider/instance_client.go +++ b/internal/provider/instance_client.go @@ -9,7 +9,7 @@ import ( type InstanceClient ClientContext[InstanceResourceModel] func (ic *InstanceClient) DataDir() string { - return ic.data.Compose.DataDir.ValueString() + return ic.data.System.DataDir.ValueString() } func (ic *InstanceClient) Close() error { @@ -18,7 +18,7 @@ func (ic *InstanceClient) Close() error { // 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("mkdir -p %s", ic.DataDir())); err != nil { + if _, err := ic.cl.RunShellPurely(fmt.Sprintf("mkdir -p %s", ic.DataDir())); err != nil { return fmt.Errorf("cannot create AEM data directory: %w", err) } return nil @@ -30,7 +30,7 @@ func (ic *InstanceClient) installComposeWrapper() error { 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())) + out, err := ic.cl.RunShellCommand(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) @@ -39,19 +39,21 @@ func (ic *InstanceClient) installComposeWrapper() error { 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 { +func (ic *InstanceClient) writeConfigFile() error { + configYAML := ic.data.Compose.Config.ValueString() + if err := ic.cl.FileWrite(fmt.Sprintf("%s/aem/default/etc/aem.yml", ic.DataDir()), configYAML); 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) +func (ic *InstanceClient) copyFiles() error { + var filesMap map[string]string + ic.data.Files.ElementsAs(ic.ctx, &filesMap, true) + for localPath, remotePath := range filesMap { + if err := ic.cl.PathCopy(localPath, remotePath, true); err != nil { + return fmt.Errorf("unable to copy path '%s' to '%s': %w", localPath, remotePath, err) + } } return nil } @@ -59,7 +61,7 @@ func (ic *InstanceClient) copyLibraryDir() error { 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())) + textOut, err := ic.cl.RunShellCommand(fmt.Sprintf("cd %s && sh aemw instance create", ic.DataDir())) if err != nil { return fmt.Errorf("unable to create AEM instance: %w", err) } @@ -75,7 +77,7 @@ 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())) + textOut, err := ic.cl.RunShellCommand(fmt.Sprintf("cd %s && sh aemw instance launch", ic.DataDir())) if err != nil { return fmt.Errorf("unable to launch AEM instance: %w", err) } @@ -92,7 +94,7 @@ func (ic *InstanceClient) terminate() error { tflog.Info(ic.ctx, "Terminating AEM instance(s)") // TODO use systemd service instead and stop it - textOut, err := ic.cl.RunShellWithEnv(fmt.Sprintf("cd %s && sh aemw instance terminate", ic.DataDir())) + textOut, err := ic.cl.RunShellCommand(fmt.Sprintf("cd %s && sh aemw instance terminate", ic.DataDir())) if err != nil { return fmt.Errorf("unable to terminate AEM instance: %w", err) } @@ -105,7 +107,7 @@ func (ic *InstanceClient) terminate() error { } func (ic *InstanceClient) deleteDataDir() error { - if _, err := ic.cl.RunShell(fmt.Sprintf("rm -fr %s", ic.DataDir())); err != nil { + if _, err := ic.cl.RunShellPurely(fmt.Sprintf("rm -fr %s", ic.DataDir())); err != nil { return fmt.Errorf("cannot delete AEM data directory: %w", err) } return nil @@ -127,7 +129,7 @@ type InstanceStatus struct { 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())) + yamlBytes, err := ic.cl.RunShellCommand(fmt.Sprintf("cd %s && sh aemw instance status --output-format yaml", ic.DataDir())) if err != nil { return status, err } @@ -138,27 +140,27 @@ func (ic *InstanceClient) ReadStatus() (InstanceStatus, error) { } // TODO when create fails this could be run twice; how about protecting it with lock? -func (ic *InstanceClient) bootstrap() error { - return ic.runHook("bootstrap", ".", ic.data.Hook.Bootstrap.ValueString()) +func (ic *InstanceClient) runBootstrapHook() error { + return ic.runHook("bootstrap", ic.data.System.Bootstrap.ValueString(), ".") } // TODO when create fails this could be run twice; how about protecting it with lock? -func (ic *InstanceClient) initialize() error { - return ic.runHook("initialize", ic.DataDir(), ic.data.Hook.Initialize.ValueString()) +func (ic *InstanceClient) runInitHook() error { + return ic.runHook("init", ic.data.Compose.Init.ValueString(), ic.DataDir()) } -func (ic *InstanceClient) provision() error { - return ic.runHook("provision", ic.DataDir(), ic.data.Hook.Provision.ValueString()) +func (ic *InstanceClient) runLaunchHook() error { + return ic.runHook("launch", ic.data.Compose.Launch.ValueString(), ic.DataDir()) } -func (ic *InstanceClient) runHook(name string, dir, cmdScript string) error { +func (ic *InstanceClient) runHook(name, cmdScript, dir string) error { if cmdScript == "" { return nil } tflog.Info(ic.ctx, fmt.Sprintf("Executing instance hook '%s'", name)) - textOut, err := ic.cl.RunShellScriptWithEnv(dir, cmdScript) + textOut, err := ic.cl.RunShellScript(name, cmdScript, dir) if err != nil { return fmt.Errorf("unable to execute hook '%s' properly: %w", name, err) } diff --git a/internal/provider/instance_resource.go b/internal/provider/instance_resource.go index f813ef7..55dbd92 100644 --- a/internal/provider/instance_resource.go +++ b/internal/provider/instance_resource.go @@ -48,8 +48,8 @@ type InstanceResourceModel struct { Compose struct { Version types.String `tfsdk:"version"` Config types.String `tfsdk:"config"` - Init types.String `tfsdk:"initialize"` - Launch types.String `tfsdk:"provision"` + Init types.String `tfsdk:"init"` + Launch types.String `tfsdk:"launch"` } `tfsdk:"compose"` Instances types.List `tfsdk:"instances"` } @@ -108,6 +108,7 @@ func (r *InstanceResource) Schema(ctx context.Context, req resource.SchemaReques "service": schema.StringAttribute{ MarkdownDescription: "Contents of the 'systemd' service configuration file", Optional: true, + Computed: true, Default: stringdefault.StaticString(instance.ServiceTemplate), }, "env": schema.MapAttribute{ // TODO handle it @@ -246,8 +247,13 @@ func (r *InstanceResource) createOrUpdate(ctx context.Context, plan *tfsdk.Plan, } }(ic) + if err := ic.copyFiles(); err != nil { + diags.AddError("Unable to copy files", fmt.Sprintf("%s", err)) + return + } + if create { - if err := ic.bootstrap(); err != nil { + if err := ic.runBootstrapHook(); err != nil { diags.AddError("Unable to bootstrap AEM machine", fmt.Sprintf("%s", err)) return } @@ -260,16 +266,12 @@ func (r *InstanceResource) createOrUpdate(ctx context.Context, plan *tfsdk.Plan, diags.AddError("Unable to install AEM Compose CLI", fmt.Sprintf("%s", err)) return } - if err := ic.copyConfigFile(); err != nil { - diags.AddError("Unable to copy AEM configuration file", fmt.Sprintf("%s", err)) - return - } - if err := ic.copyLibraryDir(); err != nil { - diags.AddError("Unable to copy AEM library dir", fmt.Sprintf("%s", err)) + if err := ic.writeConfigFile(); err != nil { + diags.AddError("Unable to write AEM configuration file", fmt.Sprintf("%s", err)) return } if create { - if err := ic.initialize(); err != nil { + if err := ic.runInitHook(); err != nil { diags.AddError("Unable to initialize AEM instance", fmt.Sprintf("%s", err)) return } @@ -282,7 +284,7 @@ func (r *InstanceResource) createOrUpdate(ctx context.Context, plan *tfsdk.Plan, diags.AddError("Unable to launch AEM instance", fmt.Sprintf("%s", err)) return } - if err := ic.provision(); err != nil { + if err := ic.runLaunchHook(); err != nil { diags.AddError("Unable to provision AEM instance", fmt.Sprintf("%s", err)) return } @@ -344,7 +346,7 @@ func (r *InstanceResource) Read(ctx context.Context, req resource.ReadRequest, r ic, err := r.client(ctx, model, time.Second*15) if err != nil { - tflog.Info(ctx, "Cannot read AEM instance state as it is not possible to connect at the moment. Possible reasons: machine IP change is in progress, machine is not yet created or booting up, etc.") + tflog.Info(ctx, "Cannot read AEM instance state as it is not possible to connect at the moment. Possible reasons: machine IP change is in progress, machine is not yet created or booting up, etc.") } else { defer func(ic *InstanceClient) { err := ic.Close()