2020-03-12 22:26:38 -04:00
|
|
|
package common
|
|
|
|
|
|
|
|
import (
|
2020-04-24 09:05:13 -04:00
|
|
|
"context"
|
2020-04-29 14:58:36 -04:00
|
|
|
"encoding/json"
|
2020-03-12 22:26:38 -04:00
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"os/exec"
|
2020-05-13 10:10:55 -04:00
|
|
|
"time"
|
2020-03-12 22:26:38 -04:00
|
|
|
|
2020-05-06 15:21:08 -04:00
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
2020-04-29 14:58:36 -04:00
|
|
|
"github.com/aws/aws-sdk-go/service/ssm"
|
2020-05-13 10:10:55 -04:00
|
|
|
"github.com/aws/aws-sdk-go/service/ssm/ssmiface"
|
|
|
|
"github.com/hashicorp/packer/common/retry"
|
2020-05-06 15:21:08 -04:00
|
|
|
"github.com/mitchellh/iochan"
|
2020-03-12 22:26:38 -04:00
|
|
|
)
|
|
|
|
|
2020-05-13 10:10:55 -04:00
|
|
|
const (
|
|
|
|
sessionManagerPluginName string = "session-manager-plugin"
|
2020-04-29 14:58:36 -04:00
|
|
|
|
2020-05-13 10:10:55 -04:00
|
|
|
//sessionCommand is the AWS-SDK equivalent to the command you would specify to `aws ssm ...`
|
|
|
|
sessionCommand string = "StartSession"
|
|
|
|
)
|
|
|
|
|
|
|
|
type SSMDriverConfig struct {
|
|
|
|
SvcClient ssmiface.SSMAPI
|
|
|
|
Region string
|
|
|
|
ProfileName string
|
|
|
|
SvcEndpoint string
|
|
|
|
}
|
2020-03-12 22:26:38 -04:00
|
|
|
|
2020-04-22 07:52:47 -04:00
|
|
|
type SSMDriver struct {
|
2020-05-13 10:10:55 -04:00
|
|
|
SSMDriverConfig
|
|
|
|
session *ssm.StartSessionOutput
|
|
|
|
sessionParams ssm.StartSessionInput
|
|
|
|
pluginCmdFunc func(context.Context) error
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewSSMDriver(config SSMDriverConfig) *SSMDriver {
|
|
|
|
d := SSMDriver{SSMDriverConfig: config}
|
|
|
|
return &d
|
2020-03-12 22:26:38 -04:00
|
|
|
}
|
|
|
|
|
2020-04-22 07:52:47 -04:00
|
|
|
// StartSession starts an interactive Systems Manager session with a remote instance via the AWS session-manager-plugin
|
2020-05-13 10:10:55 -04:00
|
|
|
// This ssm.StartSessionOutput returned by this function can be used for terminating the session manually. If you do
|
|
|
|
// not wish to manage the session manually calling StopSession on a instance of this driver will terminate the active session
|
|
|
|
// created from calling StartSession.
|
|
|
|
func (d *SSMDriver) StartSession(ctx context.Context, input ssm.StartSessionInput) (*ssm.StartSessionOutput, error) {
|
2020-08-26 13:53:08 -04:00
|
|
|
log.Printf("Starting PortForwarding session to instance %q", aws.StringValue(input.Target))
|
2020-05-13 10:10:55 -04:00
|
|
|
|
|
|
|
var output *ssm.StartSessionOutput
|
|
|
|
err := retry.Config{
|
2020-07-15 12:47:07 -04:00
|
|
|
ShouldRetry: func(err error) bool { return IsAWSErr(err, "TargetNotConnected", "") },
|
2020-05-13 10:10:55 -04:00
|
|
|
RetryDelay: (&retry.Backoff{InitialBackoff: 200 * time.Millisecond, MaxBackoff: 60 * time.Second, Multiplier: 2}).Linear,
|
|
|
|
}.Run(ctx, func(ctx context.Context) (err error) {
|
|
|
|
output, err = d.SvcClient.StartSessionWithContext(ctx, &input)
|
|
|
|
return err
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error encountered in starting session for instance %q: %s", aws.StringValue(input.Target), err)
|
|
|
|
}
|
|
|
|
|
|
|
|
d.session = output
|
|
|
|
d.sessionParams = input
|
|
|
|
|
|
|
|
if d.pluginCmdFunc == nil {
|
|
|
|
d.pluginCmdFunc = d.openTunnelForSession
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := d.pluginCmdFunc(ctx); err != nil {
|
|
|
|
return nil, fmt.Errorf("error encountered in starting session for instance %q: %s", aws.StringValue(input.Target), err)
|
2020-04-22 07:52:47 -04:00
|
|
|
}
|
|
|
|
|
2020-05-13 10:10:55 -04:00
|
|
|
return d.session, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *SSMDriver) openTunnelForSession(ctx context.Context) error {
|
2020-05-06 15:21:08 -04:00
|
|
|
args, err := d.Args()
|
2020-04-29 14:58:36 -04:00
|
|
|
if err != nil {
|
2020-05-13 10:10:55 -04:00
|
|
|
return fmt.Errorf("error encountered validating session details: %s", err)
|
2020-04-22 07:52:47 -04:00
|
|
|
}
|
|
|
|
|
2020-05-13 10:10:55 -04:00
|
|
|
cmd := exec.CommandContext(ctx, sessionManagerPluginName, args...)
|
2020-03-12 22:26:38 -04:00
|
|
|
|
2020-05-06 15:21:08 -04:00
|
|
|
// Let's build up our logging
|
|
|
|
stdout, err := cmd.StdoutPipe()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
stderr, err := cmd.StderrPipe()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create the channels we'll use for data
|
|
|
|
stdoutCh := iochan.DelimReader(stdout, '\n')
|
|
|
|
stderrCh := iochan.DelimReader(stderr, '\n')
|
|
|
|
|
2020-05-08 06:36:41 -04:00
|
|
|
/* Loop and get all our output
|
|
|
|
This particular logger will continue to run through an entire Packer run.
|
|
|
|
The decision to continue logging is due to the fact that session-manager-plugin
|
|
|
|
doesn't give a good way of knowing if the command failed or was successful other
|
|
|
|
than looking at the logs. Seeing as the plugin is updated frequently and that the
|
|
|
|
log information is a bit sparse this logger will indefinitely relying on other
|
|
|
|
steps to fail if the tunnel is unable to be created. If successful then the user
|
|
|
|
will get more information on the tunnel connection when running in a debug mode.
|
|
|
|
*/
|
2020-05-06 15:21:08 -04:00
|
|
|
go func(ctx context.Context, prefix string) {
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return
|
2020-08-26 13:53:08 -04:00
|
|
|
case output, ok := <-stderrCh:
|
|
|
|
if !ok {
|
|
|
|
stderrCh = nil
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2020-05-06 15:21:08 -04:00
|
|
|
if output != "" {
|
|
|
|
log.Printf("[ERROR] %s: %s", prefix, output)
|
|
|
|
}
|
2020-08-26 13:53:08 -04:00
|
|
|
case output, ok := <-stdoutCh:
|
|
|
|
if !ok {
|
|
|
|
stdoutCh = nil
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2020-05-06 15:21:08 -04:00
|
|
|
if output != "" {
|
|
|
|
log.Printf("[DEBUG] %s: %s", prefix, output)
|
|
|
|
}
|
|
|
|
}
|
2020-08-26 13:53:08 -04:00
|
|
|
|
|
|
|
if stdoutCh == nil && stderrCh == nil {
|
|
|
|
log.Printf("[DEBUG] %s: %s", prefix, "active session has been terminated; stopping all log polling processes.")
|
|
|
|
return
|
|
|
|
}
|
2020-05-06 15:21:08 -04:00
|
|
|
}
|
2020-05-13 10:10:55 -04:00
|
|
|
}(ctx, sessionManagerPluginName)
|
|
|
|
|
|
|
|
log.Printf("[DEBUG %s] opening session tunnel to instance %q for session %q", sessionManagerPluginName,
|
|
|
|
aws.StringValue(d.sessionParams.Target),
|
|
|
|
aws.StringValue(d.session.SessionId),
|
|
|
|
)
|
2020-05-06 15:21:08 -04:00
|
|
|
|
2020-03-12 22:26:38 -04:00
|
|
|
if err := cmd.Start(); err != nil {
|
2020-05-13 10:10:55 -04:00
|
|
|
err = fmt.Errorf("error encountered when calling %s: %s\n", sessionManagerPluginName, err)
|
2020-03-12 22:26:38 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2020-05-06 15:21:08 -04:00
|
|
|
|
2020-05-13 10:10:55 -04:00
|
|
|
// StopSession terminates an active Session Manager session
|
|
|
|
func (d *SSMDriver) StopSession() error {
|
|
|
|
|
|
|
|
if d.session == nil || d.session.SessionId == nil {
|
|
|
|
return fmt.Errorf("Unable to find a valid session to instance %q; skipping the termination step",
|
|
|
|
aws.StringValue(d.sessionParams.Target))
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err := d.SvcClient.TerminateSession(&ssm.TerminateSessionInput{SessionId: d.session.SessionId})
|
|
|
|
if err != nil {
|
|
|
|
err = fmt.Errorf("Error terminating SSM Session %q. Please terminate the session manually: %s", aws.StringValue(d.session.SessionId), err)
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-05-08 06:36:41 -04:00
|
|
|
// Args validates the driver inputs before returning an ordered set of arguments to pass to the driver command.
|
2020-05-06 15:21:08 -04:00
|
|
|
func (d *SSMDriver) Args() ([]string, error) {
|
2020-05-13 10:10:55 -04:00
|
|
|
if d.session == nil {
|
2020-04-29 14:58:36 -04:00
|
|
|
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.
|
2020-05-13 10:10:55 -04:00
|
|
|
sessionDetails, err := json.Marshal(d.session)
|
2020-04-29 14:58:36 -04:00
|
|
|
if err != nil {
|
|
|
|
return nil, 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.
|
2020-05-13 10:10:55 -04:00
|
|
|
sessionParameters, err := json.Marshal(d.sessionParams)
|
2020-04-29 14:58:36 -04:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error encountered in reading session parameter details %s", err)
|
|
|
|
}
|
|
|
|
|
2020-05-06 15:21:08 -04:00
|
|
|
// Args must be in this order
|
2020-04-29 14:58:36 -04:00
|
|
|
args := []string{
|
|
|
|
string(sessionDetails),
|
2020-05-06 15:21:08 -04:00
|
|
|
d.Region,
|
2020-04-29 14:58:36 -04:00
|
|
|
sessionCommand,
|
2020-05-06 15:21:08 -04:00
|
|
|
d.ProfileName,
|
2020-04-29 14:58:36 -04:00
|
|
|
string(sessionParameters),
|
2020-05-13 10:10:55 -04:00
|
|
|
d.SvcEndpoint,
|
2020-04-29 14:58:36 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return args, nil
|
|
|
|
}
|