118 lines
3.7 KiB
Go
118 lines
3.7 KiB
Go
package ssm
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"os/exec"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/service/ssm"
|
|
"github.com/aws/aws-sdk-go/service/ssm/ssmiface"
|
|
"github.com/hashicorp/packer/builder/amazon/common/awserrors"
|
|
packersdk "github.com/hashicorp/packer/packer-plugin-sdk/packer"
|
|
"github.com/hashicorp/packer/packer-plugin-sdk/retry"
|
|
"github.com/hashicorp/packer/packer-plugin-sdk/shell-local/localexec"
|
|
)
|
|
|
|
type Session struct {
|
|
SvcClient ssmiface.SSMAPI
|
|
Region string
|
|
InstanceID string
|
|
LocalPort, RemotePort int
|
|
}
|
|
|
|
func (s Session) buildTunnelInput() *ssm.StartSessionInput {
|
|
portNumber, localPortNumber := strconv.Itoa(s.RemotePort), strconv.Itoa(s.LocalPort)
|
|
params := map[string][]*string{
|
|
"portNumber": []*string{aws.String(portNumber)},
|
|
"localPortNumber": []*string{aws.String(localPortNumber)},
|
|
}
|
|
|
|
return &ssm.StartSessionInput{
|
|
DocumentName: aws.String("AWS-StartPortForwardingSession"),
|
|
Parameters: params,
|
|
Target: aws.String(s.InstanceID),
|
|
}
|
|
}
|
|
|
|
// getCommand return a valid ordered set of arguments to pass to the driver command.
|
|
func (s Session) getCommand(ctx context.Context) ([]string, string, error) {
|
|
input := s.buildTunnelInput()
|
|
|
|
var session *ssm.StartSessionOutput
|
|
err := retry.Config{
|
|
ShouldRetry: func(err error) bool { return awserrors.Matches(err, "TargetNotConnected", "") },
|
|
RetryDelay: (&retry.Backoff{InitialBackoff: 200 * time.Millisecond, MaxBackoff: 60 * time.Second, Multiplier: 2}).Linear,
|
|
}.Run(ctx, func(ctx context.Context) (err error) {
|
|
session, err = s.SvcClient.StartSessionWithContext(ctx, input)
|
|
return err
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
if session == nil {
|
|
return nil, "", fmt.Errorf("an active Amazon SSM Session is required before trying to open a session tunnel")
|
|
}
|
|
|
|
// AWS session-manager-plugin requires a valid session be passed in JSON.
|
|
sessionDetails, err := json.Marshal(session)
|
|
if err != nil {
|
|
return nil, *session.SessionId, fmt.Errorf("error encountered in reading session details %s", err)
|
|
}
|
|
|
|
// AWS session-manager-plugin requires the parameters used in the session to be passed in JSON as well.
|
|
sessionParameters, err := json.Marshal(input)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("error encountered in reading session parameter details %s", err)
|
|
}
|
|
|
|
// Args must be in this order
|
|
args := []string{
|
|
string(sessionDetails),
|
|
s.Region,
|
|
"StartSession",
|
|
"", // ProfileName
|
|
string(sessionParameters),
|
|
*session.StreamUrl,
|
|
}
|
|
return args, *session.SessionId, nil
|
|
}
|
|
|
|
// Start an interactive Systems Manager session with a remote instance via the
|
|
// AWS session-manager-plugin. To terminate the session you must cancell the
|
|
// context. If you do not wish to terminate the session manually: calling
|
|
// StopSession on a instance of this driver will terminate the active session
|
|
// created from calling StartSession.
|
|
func (s Session) Start(ctx context.Context, ui packersdk.Ui) error {
|
|
for ctx.Err() == nil {
|
|
log.Printf("ssm: Starting PortForwarding session to instance %s", s.InstanceID)
|
|
args, sessionID, err := s.getCommand(ctx)
|
|
if sessionID != "" {
|
|
defer func() {
|
|
_, err := s.SvcClient.TerminateSession(&ssm.TerminateSessionInput{SessionId: aws.String(sessionID)})
|
|
if err != nil {
|
|
ui.Error(fmt.Sprintf("Error terminating SSM Session %q. Please terminate the session manually: %s", sessionID, err))
|
|
}
|
|
}()
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd := exec.CommandContext(ctx, "session-manager-plugin", args...)
|
|
|
|
ui.Message(fmt.Sprintf("Starting portForwarding session %q.", sessionID))
|
|
err = localexec.RunAndStream(cmd, ui, nil)
|
|
if err != nil {
|
|
ui.Error(err.Error())
|
|
}
|
|
}
|
|
return nil
|
|
}
|