diff --git a/README.md b/README.md
index e29011b..0306bed 100644
--- a/README.md
+++ b/README.md
@@ -85,6 +85,10 @@ For example:
- `sh develop.sh examples/aws_ssh apply -auto-approve`
- `sh develop.sh examples/aws_ssh destroy -auto-approve`
+- `sh develop.sh examples/aws_ssm plan`
+- `sh develop.sh examples/aws_ssm apply -auto-approve`
+- `sh develop.sh examples/aws_ssm destroy -auto-approve`
+
## Debugging the Provider
1. Run command `go run . -debug` from IDEA in debug mode and copy the value of `TF_REATTACH_PROVIDERS` from the output.
diff --git a/docs/resources/instance.md b/docs/resources/instance.md
index f1b77be..c92faec 100644
--- a/docs/resources/instance.md
+++ b/docs/resources/instance.md
@@ -48,7 +48,9 @@ Required:
Optional:
+- `action_timeout` (String) Used when trying to connect to the AEM instance machine (often right after creating it). Need to be enough long because various types of connections (like AWS SSM or SSH) may need some time to boot up the agent.
- `credentials` (Map of String, Sensitive) Credentials for the connection type
+- `state_timeout` (String) Used when reading the AEM instance state when determining the plan.
diff --git a/examples/aws_ssh/aem.yml b/examples/aws_ssh/aem.yml
index e397306..8133864 100644
--- a/examples/aws_ssh/aem.yml
+++ b/examples/aws_ssh/aem.yml
@@ -74,12 +74,15 @@ instance:
timeout: 10m
# Max time in which socket connection to instance should be established
reachable:
+ skip: false
timeout: 3s
# Bundle state tracking
bundle_stable:
+ skip: false
symbolic_names_ignored: []
# OSGi events tracking
event_stable:
+ skip: false
# Topics indicating that instance is not stable
topics_unstable:
- "org/osgi/framework/ServiceEvent/*"
@@ -91,8 +94,20 @@ instance:
- "org.osgi.service.component.runtime.ServiceComponentRuntime"
- "java.util.ResourceBundle"
received_max_age: 5s
+ # OSGi components state tracking
+ component_stable:
+ skip: false
+ pids:
+ include: ['com.day.crx.packaging.*', 'org.apache.sling.installer.*']
+ exclude: ['org.apache.sling.installer.hc.*', 'org.apache.sling.installer.core.impl.console.*']
+ match:
+ "disabled": []
+ "no config": []
+ "unsatisfied (reference)": []
+ "satisfied": []
# Sling Installer tracking
installer:
+ skip: false
# JMX state checking
state: true
# Pause Installation nodes checking
@@ -100,7 +115,11 @@ instance:
# Specific endpoints / paths (like login page)
path_ready:
timeout: 10s
-
+ login_page:
+ skip: false
+ path: "/libs/granite/core/content/login.html"
+ status_code: 200
+ contained_text: QUICKSTART_HOMEPAGE
# Managed locally (set up automatically)
local:
@@ -138,6 +157,7 @@ instance:
package:
# Force re-uploading/installing of snapshot AEM packages (just built / unreleased)
snapshot_patterns: [ "**/*-SNAPSHOT.zip" ]
+ snapshot_ignored: false
# Use checksums to avoid re-deployments when snapshot AEM packages are unchanged
snapshot_deploy_skipping: true
# Disable following workflow launchers for a package deployment time only
@@ -151,6 +171,16 @@ instance:
console: false
# Fail on case 'installed with errors'
strict: true
+ # Number of changes after which the commit to the repository is performed
+ install_save_threshold: 1024
+ # Allows to relax dependency handling if needed
+ install_dependency_handling: required
+ # Controls how 'rep:policy' nodes are handled during import
+ install_ac_handling: ''
+
+ # 'SSL By Default'
+ ssl:
+ setup_timeout: 30s
# OSGi Framework
osgi:
@@ -166,6 +196,10 @@ instance:
crypto:
key_bundle_symbolic_name: com.adobe.granite.crypto.file
+ # Replication
+ replication:
+ bundle_symbolic_name: com.day.cq.cq-replication
+
# Workflow Manager
workflow:
launcher:
diff --git a/examples/aws_ssh/aws.tf b/examples/aws_ssh/aws.tf
index e7cf16d..7132fdd 100644
--- a/examples/aws_ssh/aws.tf
+++ b/examples/aws_ssh/aws.tf
@@ -37,7 +37,7 @@ resource "aws_iam_instance_profile" "aem_ec2" {
}
resource "aws_iam_role" "aem_ec2" {
- name = "${local.workspace}_aem_ec2"
+ name = "${local.workspace}_aem_ec2"
assume_role_policy = trimspace(< 0 {
- return nil, fmt.Errorf("cannot run command '%s': %w\n\n%s", cmdObj, err, string(out))
- }
- return nil, err
- }
return out, nil
}
diff --git a/internal/client/client_manager.go b/internal/client/client_manager.go
index f76c463..8f37b5d 100644
--- a/internal/client/client_manager.go
+++ b/internal/client/client_manager.go
@@ -1,6 +1,7 @@
package client
import (
+ "context"
"fmt"
"github.com/spf13/cast"
)
@@ -40,8 +41,12 @@ func (c ClientManager) connection(typeName string, settings map[string]string) (
}, nil
case "aws-ssm":
return &AWSSSMConnection{
- InstanceID: settings["instance_id"],
- Region: settings["region"],
+ instanceID: settings["instance_id"],
+ region: settings["region"],
+ context: context.Background(),
+ commandOutputTimeout: cast.ToDuration(settings["command_output_timeout"]),
+ commandWaitMin: cast.ToDuration(settings["command_wait_min"]),
+ commandWaitMax: cast.ToDuration(settings["command_wait_max"]),
}, nil
}
return nil, fmt.Errorf("unknown AEM client type: %s", typeName)
diff --git a/internal/client/connection.go b/internal/client/connection.go
index 77421a7..7b1f772 100644
--- a/internal/client/connection.go
+++ b/internal/client/connection.go
@@ -1,14 +1,10 @@
package client
-import (
- "github.com/melbahja/goph"
-)
-
type Connection interface {
Info() string
User() string
Connect() error
Disconnect() error
- Command(cmdLine []string) (*goph.Cmd, error)
+ Command(cmdLine []string) ([]byte, error)
CopyFile(localPath string, remotePath string) error
}
diff --git a/internal/client/connection_aws_ssm.go b/internal/client/connection_aws_ssm.go
index 1644c4a..e2a5608 100644
--- a/internal/client/connection_aws_ssm.go
+++ b/internal/client/connection_aws_ssm.go
@@ -1,38 +1,137 @@
package client
-import "github.com/melbahja/goph"
+import (
+ "context"
+ "encoding/base64"
+ "fmt"
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/config"
+ "github.com/aws/aws-sdk-go-v2/service/ssm"
+ "os"
+ "strings"
+ "time"
+)
type AWSSSMConnection struct {
- InstanceID string
- Region string
+ instanceID string
+ region string
+ client *ssm.Client
+ sessionId *string
+ context context.Context
+ commandOutputTimeout time.Duration
+ commandWaitMax time.Duration
+ commandWaitMin time.Duration
}
-func (A AWSSSMConnection) Info() string {
- //TODO implement me
- panic("implement me")
+func (a *AWSSSMConnection) Info() string {
+ region := a.region
+ if region == "" {
+ region = ""
+ }
+ return fmt.Sprintf("ssm: instance_id='%s', region='%s'", a.instanceID, region)
}
-func (A AWSSSMConnection) User() string {
- //TODO implement me
- panic("implement me")
+func (a *AWSSSMConnection) User() string {
+ out, err := a.Command([]string{"whoami"})
+ if err != nil {
+ panic(fmt.Sprintf("ssm: cannot determine connected user: %s", err))
+ }
+ return strings.TrimSpace(string(out))
}
-func (A AWSSSMConnection) Connect() error {
- //TODO implement me
- panic("implement me")
+func (a *AWSSSMConnection) Connect() error {
+ if a.commandOutputTimeout == 0 {
+ a.commandOutputTimeout = 5 * time.Hour
+ }
+ if a.commandWaitMin == 0 {
+ a.commandWaitMin = 5 * time.Millisecond
+ }
+ if a.commandWaitMax == 0 {
+ a.commandWaitMax = 5 * time.Second
+ }
+
+ var optFns []func(*config.LoadOptions) error
+ if a.region != "" {
+ optFns = append(optFns, config.WithRegion(a.region))
+ }
+
+ cfg, err := config.LoadDefaultConfig(a.context, optFns...)
+ if err != nil {
+ return err
+ }
+
+ client := ssm.NewFromConfig(cfg)
+ sessionIn := &ssm.StartSessionInput{Target: aws.String(a.instanceID)}
+ sessionOut, err := client.StartSession(a.context, sessionIn)
+ if err != nil {
+ return fmt.Errorf("ssm: error starting session: %v", err)
+ }
+
+ a.client = client
+ a.sessionId = sessionOut.SessionId
+
+ return nil
}
-func (A AWSSSMConnection) Disconnect() error {
- //TODO implement me
- panic("implement me")
+func (a *AWSSSMConnection) Disconnect() error {
+ sessionIn := &ssm.TerminateSessionInput{SessionId: a.sessionId}
+ _, err := a.client.TerminateSession(a.context, sessionIn)
+ if err != nil {
+ return fmt.Errorf("ssm: error terminating session: %v", err)
+ }
+
+ return nil
}
-func (A AWSSSMConnection) Command(cmdLine []string) (*goph.Cmd, error) {
- //TODO implement me
- panic("implement me")
+func (a *AWSSSMConnection) Command(cmdLine []string) ([]byte, error) {
+ command := strings.Join(cmdLine, " ")
+ commandIn := &ssm.SendCommandInput{
+ DocumentName: aws.String("AWS-RunShellScript"),
+ InstanceIds: []string{a.instanceID},
+ Parameters: map[string][]string{
+ "commands": {command},
+ },
+ }
+ runOut, err := a.client.SendCommand(a.context, commandIn)
+ if err != nil {
+ return nil, fmt.Errorf("ssm: error executing command: %v", err)
+ }
+
+ commandId := runOut.Command.CommandId
+ invocationIn := &ssm.GetCommandInvocationInput{
+ CommandId: commandId,
+ InstanceId: aws.String(a.instanceID),
+ }
+ var optFns []func(opt *ssm.CommandExecutedWaiterOptions)
+ if a.commandWaitMax > 0 && a.commandWaitMin > 0 {
+ optFns = []func(opt *ssm.CommandExecutedWaiterOptions){func(opt *ssm.CommandExecutedWaiterOptions) {
+ opt.MinDelay = a.commandWaitMin
+ opt.MaxDelay = a.commandWaitMax
+ }}
+ }
+
+ waiter := ssm.NewCommandExecutedWaiter(a.client, optFns...)
+ invocationOut, err := waiter.WaitForOutput(a.context, invocationIn, a.commandOutputTimeout)
+ if err != nil {
+ invocationOut, err = a.client.GetCommandInvocation(a.context, invocationIn)
+ if invocationOut != nil {
+ return nil, fmt.Errorf("ssm: error executing command: %v", *invocationOut.StandardErrorContent)
+ } else if err != nil {
+ return nil, fmt.Errorf("ssm: error executing command: %v", err)
+ }
+ }
+
+ return []byte(*invocationOut.StandardOutputContent), nil
}
-func (A AWSSSMConnection) CopyFile(localPath string, remotePath string) error {
- //TODO implement me
- panic("implement me")
+func (a *AWSSSMConnection) CopyFile(localPath string, remotePath string) error {
+ fileContent, err := os.ReadFile(localPath)
+ if err != nil {
+ return fmt.Errorf("ssm: error reading local file: %v", err)
+ }
+ encodedContent := base64.StdEncoding.EncodeToString(fileContent)
+
+ cmd := fmt.Sprintf("echo -n %s | base64 -d > %s", encodedContent, remotePath)
+ _, err = a.Command([]string{cmd})
+ return err
}
diff --git a/internal/client/connection_ssh.go b/internal/client/connection_ssh.go
index 958e972..cf57f95 100644
--- a/internal/client/connection_ssh.go
+++ b/internal/client/connection_ssh.go
@@ -87,13 +87,19 @@ func (s *SSHConnection) Disconnect() error {
return nil
}
-func (s *SSHConnection) Command(cmdLine []string) (*goph.Cmd, error) {
+func (s *SSHConnection) Command(cmdLine []string) ([]byte, error) {
name, args := s.splitCommandLine(cmdLine)
cmd, err := s.client.Command(name, args...)
if err != nil {
return nil, fmt.Errorf("ssh: cannot create command '%s' for host '%s': %w", strings.Join(cmdLine, " "), s.host, err)
}
- return cmd, nil
+ out, err := cmd.CombinedOutput()
+ if err != nil && len(out) > 0 {
+ return nil, fmt.Errorf("ssh: cannot run command '%s': %w\n\n%s", cmd, err, string(out))
+ } else if err != nil {
+ return nil, fmt.Errorf("ssh: cannot run command '%s': %w", cmd, err)
+ }
+ return out, nil
}
func (s *SSHConnection) splitCommandLine(cmdLine []string) (string, []string) {
diff --git a/internal/provider/instance/aem.yml b/internal/provider/instance/aem.yml
index e397306..8133864 100644
--- a/internal/provider/instance/aem.yml
+++ b/internal/provider/instance/aem.yml
@@ -74,12 +74,15 @@ instance:
timeout: 10m
# Max time in which socket connection to instance should be established
reachable:
+ skip: false
timeout: 3s
# Bundle state tracking
bundle_stable:
+ skip: false
symbolic_names_ignored: []
# OSGi events tracking
event_stable:
+ skip: false
# Topics indicating that instance is not stable
topics_unstable:
- "org/osgi/framework/ServiceEvent/*"
@@ -91,8 +94,20 @@ instance:
- "org.osgi.service.component.runtime.ServiceComponentRuntime"
- "java.util.ResourceBundle"
received_max_age: 5s
+ # OSGi components state tracking
+ component_stable:
+ skip: false
+ pids:
+ include: ['com.day.crx.packaging.*', 'org.apache.sling.installer.*']
+ exclude: ['org.apache.sling.installer.hc.*', 'org.apache.sling.installer.core.impl.console.*']
+ match:
+ "disabled": []
+ "no config": []
+ "unsatisfied (reference)": []
+ "satisfied": []
# Sling Installer tracking
installer:
+ skip: false
# JMX state checking
state: true
# Pause Installation nodes checking
@@ -100,7 +115,11 @@ instance:
# Specific endpoints / paths (like login page)
path_ready:
timeout: 10s
-
+ login_page:
+ skip: false
+ path: "/libs/granite/core/content/login.html"
+ status_code: 200
+ contained_text: QUICKSTART_HOMEPAGE
# Managed locally (set up automatically)
local:
@@ -138,6 +157,7 @@ instance:
package:
# Force re-uploading/installing of snapshot AEM packages (just built / unreleased)
snapshot_patterns: [ "**/*-SNAPSHOT.zip" ]
+ snapshot_ignored: false
# Use checksums to avoid re-deployments when snapshot AEM packages are unchanged
snapshot_deploy_skipping: true
# Disable following workflow launchers for a package deployment time only
@@ -151,6 +171,16 @@ instance:
console: false
# Fail on case 'installed with errors'
strict: true
+ # Number of changes after which the commit to the repository is performed
+ install_save_threshold: 1024
+ # Allows to relax dependency handling if needed
+ install_dependency_handling: required
+ # Controls how 'rep:policy' nodes are handled during import
+ install_ac_handling: ''
+
+ # 'SSL By Default'
+ ssl:
+ setup_timeout: 30s
# OSGi Framework
osgi:
@@ -166,6 +196,10 @@ instance:
crypto:
key_bundle_symbolic_name: com.adobe.granite.crypto.file
+ # Replication
+ replication:
+ bundle_symbolic_name: com.day.cq.cq-replication
+
# Workflow Manager
workflow:
launcher:
diff --git a/internal/provider/instance_model.go b/internal/provider/instance_model.go
index 815353b..77755f2 100644
--- a/internal/provider/instance_model.go
+++ b/internal/provider/instance_model.go
@@ -22,9 +22,11 @@ import (
type InstanceResourceModel struct {
Client struct {
- Type types.String `tfsdk:"type"`
- Settings types.Map `tfsdk:"settings"`
- Credentials types.Map `tfsdk:"credentials"`
+ Type types.String `tfsdk:"type"`
+ Settings types.Map `tfsdk:"settings"`
+ Credentials types.Map `tfsdk:"credentials"`
+ ActionTimeout types.String `tfsdk:"action_timeout"`
+ StateTimeout types.String `tfsdk:"state_timeout"`
} `tfsdk:"client"`
Files types.Map `tfsdk:"files"`
System struct {
@@ -122,6 +124,18 @@ func (r *InstanceResource) Schema(ctx context.Context, req resource.SchemaReques
Optional: true,
Sensitive: true,
},
+ "action_timeout": schema.StringAttribute{
+ MarkdownDescription: "Used when trying to connect to the AEM instance machine (often right after creating it). Need to be enough long because various types of connections (like AWS SSM or SSH) may need some time to boot up the agent.",
+ Optional: true,
+ Computed: true,
+ Default: stringdefault.StaticString("10m"),
+ },
+ "state_timeout": schema.StringAttribute{
+ MarkdownDescription: "Used when reading the AEM instance state when determining the plan.",
+ Optional: true,
+ Computed: true,
+ Default: stringdefault.StaticString("30s"),
+ },
},
},
"system": schema.SingleNestedBlock{
@@ -190,7 +204,7 @@ func (r *InstanceResource) Schema(ctx context.Context, req resource.SchemaReques
MarkdownDescription: "Version of AEM Compose tool to use on remote machine.",
Computed: true,
Optional: true,
- Default: stringdefault.StaticString("1.5.9"),
+ Default: stringdefault.StaticString("1.6.12"),
},
"config": schema.StringAttribute{
MarkdownDescription: "Contents o f the AEM Compose YML configuration file.",
diff --git a/internal/provider/instance_resource.go b/internal/provider/instance_resource.go
index fddfaa0..77524bb 100644
--- a/internal/provider/instance_resource.go
+++ b/internal/provider/instance_resource.go
@@ -7,6 +7,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-log/tflog"
+ "github.com/spf13/cast"
"github.com/wttech/terraform-provider-aem/internal/client"
"golang.org/x/exp/maps"
"time"
@@ -66,7 +67,7 @@ func (r *InstanceResource) createOrUpdate(ctx context.Context, plan *tfsdk.Plan,
tflog.Info(ctx, "Started setting up AEM instance resource")
- ic, err := r.client(ctx, plannedModel, time.Minute*5)
+ ic, err := r.client(ctx, plannedModel, cast.ToDuration(plannedModel.Client.ActionTimeout.ValueString()))
if err != nil {
diags.AddError("Unable to connect to AEM instance", fmt.Sprintf("%s", err))
return
@@ -138,7 +139,7 @@ func (r *InstanceResource) Read(ctx context.Context, req resource.ReadRequest, r
return
}
- ic, err := r.client(ctx, model, time.Second*15)
+ ic, err := r.client(ctx, model, cast.ToDuration(model.Client.StateTimeout.ValueString()))
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.")
} else {
@@ -173,7 +174,7 @@ func (r *InstanceResource) Delete(ctx context.Context, req resource.DeleteReques
tflog.Info(ctx, "Started deleting AEM instance resource")
- ic, err := r.client(ctx, model, time.Minute*5)
+ ic, err := r.client(ctx, model, cast.ToDuration(model.Client.StateTimeout.ValueString()))
if err != nil {
resp.Diagnostics.AddError("Unable to connect to AEM instance", fmt.Sprintf("%s", err))
return