Update logging constructs for Session Manger driver

This commit is contained in:
Wilken Rivera 2020-05-06 15:21:08 -04:00
parent 81c40b8d08
commit 0d13c634b7
2 changed files with 58 additions and 37 deletions

View File

@ -1,14 +1,15 @@
package common package common
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
"os/exec" "os/exec"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ssm" "github.com/aws/aws-sdk-go/service/ssm"
"github.com/mitchellh/iochan"
) )
const sessionManagerPluginName string = "session-manager-plugin" const sessionManagerPluginName string = "session-manager-plugin"
@ -22,62 +23,89 @@ type SSMDriver struct {
Session *ssm.StartSessionOutput Session *ssm.StartSessionOutput
SessionParams ssm.StartSessionInput SessionParams ssm.StartSessionInput
SessionEndpoint string 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 // 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 { func (d *SSMDriver) StartSession(ctx context.Context) error {
var stdout bytes.Buffer if d.PluginName == "" {
var stderr bytes.Buffer d.PluginName = sessionManagerPluginName
if sd.PluginName == "" {
sd.PluginName = sessionManagerPluginName
} }
args, err := sd.Args() args, err := d.Args()
if err != nil { if err != nil {
err = fmt.Errorf("error encountered validating session details: %s", err) err = fmt.Errorf("error encountered validating session details: %s", err)
return err return err
} }
cmd := exec.CommandContext(ctx, sd.PluginName, args...) cmd := exec.CommandContext(ctx, d.PluginName, args...)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Start(); err != nil { // Let's build up our logging
err = fmt.Errorf("error encountered when calling %s: %s\nStderr: %s", sd.PluginName, err, stderr.String()) 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 return err
} }
// TODO capture logging for testing
log.Println(stdout.String())
return nil 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") 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. // 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 { if err != nil {
return nil, fmt.Errorf("error encountered in reading session details %s", err) 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. // 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 { if err != nil {
return nil, fmt.Errorf("error encountered in reading session parameter details %s", err) return nil, fmt.Errorf("error encountered in reading session parameter details %s", err)
} }
// Args must be in this order
args := []string{ args := []string{
string(sessionDetails), string(sessionDetails),
sd.Region, d.Region,
sessionCommand, sessionCommand,
sd.ProfileName, d.ProfileName,
string(sessionParameters), string(sessionParameters),
sd.SessionEndpoint, d.SessionEndpoint,
} }
return args, nil return args, nil

View File

@ -50,15 +50,14 @@ func (s *StepCreateSSMTunnel) Run(ctx context.Context, state multistep.StateBag)
} }
s.instanceId = aws.StringValue(instance.InstanceId) 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) input := s.BuildTunnelInputForInstance(s.instanceId)
ssmconn := ssm.New(s.AWSSession) ssmconn := ssm.New(s.AWSSession)
var output *ssm.StartSessionOutput
err := retry.Config{ err := retry.Config{
ShouldRetry: func(err error) bool { return isAWSErr(err, "TargetNotConnected", "") }, ShouldRetry: func(err error) bool { return isAWSErr(err, "TargetNotConnected", "") },
RetryDelay: (&retry.Backoff{InitialBackoff: 200 * time.Millisecond, MaxBackoff: 60 * time.Second, Multiplier: 2}).Linear, RetryDelay: (&retry.Backoff{InitialBackoff: 200 * time.Millisecond, MaxBackoff: 60 * time.Second, Multiplier: 2}).Linear,
}.Run(ctx, func(ctx context.Context) (err error) { }.Run(ctx, func(ctx context.Context) (err error) {
output, err = ssmconn.StartSessionWithContext(ctx, &input) s.session, err = ssmconn.StartSessionWithContext(ctx, &input)
return err return err
}) })
@ -71,7 +70,7 @@ func (s *StepCreateSSMTunnel) Run(ctx context.Context, state multistep.StateBag)
driver := SSMDriver{ driver := SSMDriver{
Region: s.Region, Region: s.Region,
Session: output, Session: s.session,
SessionParams: input, SessionParams: input,
SessionEndpoint: ssmconn.Endpoint, SessionEndpoint: ssmconn.Endpoint,
} }
@ -83,27 +82,21 @@ func (s *StepCreateSSMTunnel) Run(ctx context.Context, state multistep.StateBag)
return multistep.ActionHalt 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) state.Put("sessionPort", s.LocalPortNumber)
return multistep.ActionContinue return multistep.ActionContinue
} }
// Cleanup terminates an active session on AWS, which in turn terminates the associated tunnel process running on the local machine. // 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) { func (s *StepCreateSSMTunnel) Cleanup(state multistep.StateBag) {
if s.session == nil {
return
}
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
ssmconn := ssm.New(s.AWSSession) ssmconn := ssm.New(s.AWSSession)
_, err := ssmconn.TerminateSession(&ssm.TerminateSessionInput{SessionId: s.session.SessionId}) _, err := ssmconn.TerminateSession(&ssm.TerminateSessionInput{SessionId: s.session.SessionId})
if err != nil { if err != nil {
msg := fmt.Sprintf("Error terminating SSM Session %q. Please terminate the session manually: %s", msg := fmt.Sprintf("Error terminating SSM Session %q. Please terminate the session manually: %s", aws.StringValue(s.session.SessionId), err)
aws.StringValue(s.session.SessionId), err)
ui.Error(msg) ui.Error(msg)
} }
} }
// ConfigureLocalHostPort finds an available port on the localhost that can be used for the remote tunnel. // ConfigureLocalHostPort finds an available port on the localhost that can be used for the remote tunnel.