diff --git a/builder/amazon/common/ssm_driver.go b/builder/amazon/common/ssm_driver.go index 8fc69b90f..314af182f 100644 --- a/builder/amazon/common/ssm_driver.go +++ b/builder/amazon/common/ssm_driver.go @@ -1,14 +1,15 @@ package common import ( - "bytes" "context" "encoding/json" "fmt" "log" "os/exec" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ssm" + "github.com/mitchellh/iochan" ) const sessionManagerPluginName string = "session-manager-plugin" @@ -22,62 +23,89 @@ type SSMDriver struct { Session *ssm.StartSessionOutput SessionParams ssm.StartSessionInput SessionEndpoint string - // Provided for testing purposes; if not specified it defaults to sessionManagerPluginName - PluginName string + PluginName string } // StartSession starts an interactive Systems Manager session with a remote instance via the AWS session-manager-plugin -func (sd *SSMDriver) StartSession(ctx context.Context) error { - var stdout bytes.Buffer - var stderr bytes.Buffer - - if sd.PluginName == "" { - sd.PluginName = sessionManagerPluginName +func (d *SSMDriver) StartSession(ctx context.Context) error { + if d.PluginName == "" { + d.PluginName = sessionManagerPluginName } - args, err := sd.Args() + args, err := d.Args() if err != nil { err = fmt.Errorf("error encountered validating session details: %s", err) return err } - cmd := exec.CommandContext(ctx, sd.PluginName, args...) - cmd.Stdout = &stdout - cmd.Stderr = &stderr + cmd := exec.CommandContext(ctx, d.PluginName, args...) - if err := cmd.Start(); err != nil { - err = fmt.Errorf("error encountered when calling %s: %s\nStderr: %s", sd.PluginName, err, stderr.String()) + // 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') + + // Loop and get all our output + go func(ctx context.Context, prefix string) { + for { + select { + case <-ctx.Done(): + return + case output := <-stderrCh: + if output != "" { + log.Printf("[ERROR] %s: %s", prefix, output) + } + case output := <-stdoutCh: + if output != "" { + log.Printf("[DEBUG] %s: %s", prefix, output) + } + } + } + }(ctx, d.PluginName) + + log.Printf("[DEBUG %s] opening session tunnel to instance %q for session %q", d.PluginName, aws.StringValue(d.SessionParams.Target), aws.StringValue(d.Session.SessionId)) + if err := cmd.Start(); err != nil { + err = fmt.Errorf("error encountered when calling %s: %s\n", d.PluginName, err) return err } - // TODO capture logging for testing - log.Println(stdout.String()) return nil } -func (sd *SSMDriver) Args() ([]string, error) { - if sd.Session == nil { + +func (d *SSMDriver) Args() ([]string, error) { + if d.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(sd.Session) + sessionDetails, err := json.Marshal(d.Session) 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. - sessionParameters, err := json.Marshal(sd.SessionParams) + sessionParameters, err := json.Marshal(d.SessionParams) 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), - sd.Region, + d.Region, sessionCommand, - sd.ProfileName, + d.ProfileName, string(sessionParameters), - sd.SessionEndpoint, + d.SessionEndpoint, } return args, nil diff --git a/builder/amazon/common/step_create_ssm_tunnel.go b/builder/amazon/common/step_create_ssm_tunnel.go index ad1938a21..e62697c7c 100644 --- a/builder/amazon/common/step_create_ssm_tunnel.go +++ b/builder/amazon/common/step_create_ssm_tunnel.go @@ -50,15 +50,14 @@ func (s *StepCreateSSMTunnel) Run(ctx context.Context, state multistep.StateBag) } s.instanceId = aws.StringValue(instance.InstanceId) - log.Printf("Starting PortForwarding session to instance %q on local port %q to remote port %q", s.instanceId, s.LocalPortNumber, s.RemotePortNumber) + log.Printf("Starting PortForwarding session to instance %q on local port %d to remote port %d", s.instanceId, s.LocalPortNumber, s.RemotePortNumber) input := s.BuildTunnelInputForInstance(s.instanceId) ssmconn := ssm.New(s.AWSSession) - var output *ssm.StartSessionOutput err := retry.Config{ ShouldRetry: func(err error) bool { return isAWSErr(err, "TargetNotConnected", "") }, RetryDelay: (&retry.Backoff{InitialBackoff: 200 * time.Millisecond, MaxBackoff: 60 * time.Second, Multiplier: 2}).Linear, }.Run(ctx, func(ctx context.Context) (err error) { - output, err = ssmconn.StartSessionWithContext(ctx, &input) + s.session, err = ssmconn.StartSessionWithContext(ctx, &input) return err }) @@ -71,7 +70,7 @@ func (s *StepCreateSSMTunnel) Run(ctx context.Context, state multistep.StateBag) driver := SSMDriver{ Region: s.Region, - Session: output, + Session: s.session, SessionParams: input, SessionEndpoint: ssmconn.Endpoint, } @@ -83,27 +82,21 @@ func (s *StepCreateSSMTunnel) Run(ctx context.Context, state multistep.StateBag) return multistep.ActionHalt } - ui.Message(fmt.Sprintf("PortForwarding session tunnel to instance %q established!", s.instanceId)) + ui.Message(fmt.Sprintf("PortForwarding session %q to instance %q has been started", aws.StringValue(s.session.SessionId), s.instanceId)) state.Put("sessionPort", s.LocalPortNumber) - return multistep.ActionContinue } // Cleanup terminates an active session on AWS, which in turn terminates the associated tunnel process running on the local machine. func (s *StepCreateSSMTunnel) Cleanup(state multistep.StateBag) { - if s.session == nil { - return - } - ui := state.Get("ui").(packer.Ui) + ssmconn := ssm.New(s.AWSSession) _, err := ssmconn.TerminateSession(&ssm.TerminateSessionInput{SessionId: s.session.SessionId}) if err != nil { - msg := fmt.Sprintf("Error terminating SSM Session %q. Please terminate the session manually: %s", - aws.StringValue(s.session.SessionId), err) + msg := fmt.Sprintf("Error terminating SSM Session %q. Please terminate the session manually: %s", aws.StringValue(s.session.SessionId), err) ui.Error(msg) } - } // ConfigureLocalHostPort finds an available port on the localhost that can be used for the remote tunnel.