Skip to content

Commit 1e703f4

Browse files
author
Kristian
authored
Merge pull request #12 from mumoshu/optional-s3-output
Support S3 bucket and key for run/shell output
2 parents 2b7a9d2 + 30f4aa0 commit 1e703f4

File tree

8 files changed

+157
-35
lines changed

8 files changed

+157
-35
lines changed

command/list.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ package command
22

33
import (
44
"encoding/json"
5-
"github.com/itsdalmo/ssm-sh/manager"
6-
"github.com/pkg/errors"
75
"io/ioutil"
86
"os"
97
"strings"
8+
9+
"github.com/itsdalmo/ssm-sh/manager"
10+
"github.com/pkg/errors"
1011
)
1112

1213
type ListCommand struct {
@@ -20,7 +21,7 @@ func (command *ListCommand) Execute([]string) error {
2021
if err != nil {
2122
return errors.Wrap(err, "failed to create new session")
2223
}
23-
m := manager.NewManager(sess, Command.AwsOpts.Region)
24+
m := manager.NewManager(sess, Command.AwsOpts.Region, manager.Opts{})
2425

2526
var filters []*manager.TagFilter
2627
for _, tag := range command.Tags {

command/run.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@ package command
33
import (
44
"context"
55
"fmt"
6-
"github.com/itsdalmo/ssm-sh/manager"
7-
"github.com/pkg/errors"
86
"os"
97
"strings"
108
"time"
9+
10+
"github.com/itsdalmo/ssm-sh/manager"
11+
"github.com/pkg/errors"
1112
)
1213

1314
type RunCommand struct {
14-
Timeout int `short:"i" long:"timeout" description:"Seconds to wait for command result before timing out." default:"30"`
15+
Timeout int `short:"i" long:"timeout" description:"Seconds to wait for command result before timing out." default:"30"`
16+
SSMOpts SSMOptions `group:"SSM options"`
1517
TargetOpts TargetOptions
1618
}
1719

@@ -21,7 +23,11 @@ func (command *RunCommand) Execute(args []string) error {
2123
return errors.Wrap(err, "failed to create new aws session")
2224
}
2325

24-
m := manager.NewManager(sess, Command.AwsOpts.Region)
26+
opts, err := command.SSMOpts.Parse()
27+
if err != nil {
28+
return err
29+
}
30+
m := manager.NewManager(sess, Command.AwsOpts.Region, *opts)
2531
targets, err := setTargets(command.TargetOpts)
2632
if err != nil {
2733
return errors.Wrap(err, "failed to set targets")

command/shell.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@ package command
33
import (
44
"context"
55
"fmt"
6-
"github.com/chzyer/readline"
7-
"github.com/itsdalmo/ssm-sh/manager"
8-
"github.com/pkg/errors"
96
"io"
107
"os"
118
"strings"
9+
10+
"github.com/chzyer/readline"
11+
"github.com/itsdalmo/ssm-sh/manager"
12+
"github.com/pkg/errors"
1213
)
1314

1415
type ShellCommand struct {
16+
SSMOpts SSMOptions `group:"SSM options"`
1517
TargetOpts TargetOptions
1618
}
1719

@@ -21,7 +23,11 @@ func (command *ShellCommand) Execute([]string) error {
2123
return errors.Wrap(err, "failed to create new aws session")
2224
}
2325

24-
m := manager.NewManager(sess, Command.AwsOpts.Region)
26+
opts, err := command.SSMOpts.Parse()
27+
if err != nil {
28+
return err
29+
}
30+
m := manager.NewManager(sess, Command.AwsOpts.Region, *opts)
2531
targets, err := setTargets(command.TargetOpts)
2632
if err != nil {
2733
return errors.Wrap(err, "failed to set targets")

command/ssm_opts.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package command
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/itsdalmo/ssm-sh/manager"
7+
)
8+
9+
type SSMOptions struct {
10+
ExtendOutput bool `short:"x" long:"extend-output" description:"Extend truncated command outputs by fetching S3 objects containing full ones"`
11+
S3Bucket string `short:"b" long:"s3-bucket" description:"S3 bucket in which S3 objects containing full command outputs are stored. Required when --extend-output is provided." default:""`
12+
S3KeyPrefix string `short:"k" long:"s3-key-prefix" description:"Key prefix of S3 objects containing full command outputs." default:""`
13+
}
14+
15+
func (o SSMOptions) Validate() error {
16+
if o.ExtendOutput && o.S3Bucket == "" {
17+
return fmt.Errorf("--s3-bucket must be a non-empty string when --extend-output is provided")
18+
}
19+
return nil
20+
}
21+
22+
func (o SSMOptions) Parse() (*manager.Opts, error) {
23+
err := o.Validate()
24+
if err != nil {
25+
return nil, err
26+
}
27+
return &manager.Opts{
28+
ExtendOutput: o.ExtendOutput,
29+
S3Bucket: o.S3Bucket,
30+
S3KeyPrefix: o.S3KeyPrefix,
31+
}, nil
32+
}

command/utils.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@ import (
44
"encoding/json"
55
"errors"
66
"fmt"
7-
"github.com/aws/aws-sdk-go/aws/session"
8-
"github.com/fatih/color"
9-
"github.com/itsdalmo/ssm-sh/manager"
107
"io"
118
"io/ioutil"
129
"os"
1310
"os/signal"
1411
"strings"
1512
"text/tabwriter"
1613
"time"
14+
15+
"github.com/aws/aws-sdk-go/aws/session"
16+
"github.com/fatih/color"
17+
"github.com/itsdalmo/ssm-sh/manager"
1718
)
1819

1920
// Create a new AWS session
@@ -103,6 +104,11 @@ func PrintCommandOutput(wrt io.Writer, output *manager.CommandOutput) error {
103104
if _, err := fmt.Fprintf(wrt, "%s\n", output.Output); err != nil {
104105
return err
105106
}
107+
if output.OutputUrl != "" {
108+
if _, err := fmt.Fprintf(wrt, "(Output URL: %s)\n", output.OutputUrl); err != nil {
109+
return err
110+
}
111+
}
106112
return nil
107113
}
108114

command/utils_test.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ package command_test
33
import (
44
"bytes"
55
"errors"
6-
"github.com/itsdalmo/ssm-sh/command"
7-
"github.com/itsdalmo/ssm-sh/manager"
8-
"github.com/stretchr/testify/assert"
96
"strings"
107
"testing"
118
"time"
9+
10+
"github.com/itsdalmo/ssm-sh/command"
11+
"github.com/itsdalmo/ssm-sh/manager"
12+
"github.com/stretchr/testify/assert"
1213
)
1314

1415
func TestPrintInstances(t *testing.T) {
@@ -61,6 +62,13 @@ func TestPrintCommandOutput(t *testing.T) {
6162
Output: "Standard output",
6263
Error: nil,
6364
},
65+
{
66+
InstanceID: "i-00000000000000001",
67+
Status: "Success",
68+
Output: "Extended standard output",
69+
OutputUrl: "https://s3-ap-northeast-1.amazonaws.com/mybucket/foobar/c0896747-af2b-4359-bc34-0f951ce02007/i-00000000000000001/awsrunShellScript/0.awsrunShellScript/stdout",
70+
Error: nil,
71+
},
6472
{
6573
InstanceID: "i-00000000000000002",
6674
Status: "Failed",
@@ -80,6 +88,10 @@ func TestPrintCommandOutput(t *testing.T) {
8088
i-00000000000000001 - Success:
8189
Standard output
8290
91+
i-00000000000000001 - Success:
92+
Extended standard output
93+
(Output URL: https://s3-ap-northeast-1.amazonaws.com/mybucket/foobar/c0896747-af2b-4359-bc34-0f951ce02007/i-00000000000000001/awsrunShellScript/0.awsrunShellScript/stdout)
94+
8395
i-00000000000000002 - Failed:
8496
Standard error
8597

main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package main
22

33
import (
4+
"os"
5+
46
"github.com/itsdalmo/ssm-sh/command"
57
"github.com/jessevdk/go-flags"
6-
"os"
78
)
89

910
// Version is set on build by the Git release tag.

manager/manager.go

Lines changed: 75 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ import (
44
"bytes"
55
"context"
66
"fmt"
7+
"io"
8+
"regexp"
9+
"strings"
10+
"sync"
11+
"time"
12+
713
"github.com/aws/aws-sdk-go/aws"
814
"github.com/aws/aws-sdk-go/aws/session"
915
"github.com/aws/aws-sdk-go/service/ec2"
@@ -13,9 +19,6 @@ import (
1319
"github.com/aws/aws-sdk-go/service/ssm"
1420
"github.com/aws/aws-sdk-go/service/ssm/ssmiface"
1521
"github.com/pkg/errors"
16-
"io"
17-
"sync"
18-
"time"
1922
)
2023

2124
// TagFilter represents a key=value pair for AWS EC2 tags.
@@ -37,26 +40,40 @@ type CommandOutput struct {
3740
InstanceID string
3841
Status string
3942
Output string
43+
OutputUrl string
4044
Error error
4145
}
4246

4347
// Manager handles the clients interfacing with AWS.
4448
type Manager struct {
45-
ssmClient ssmiface.SSMAPI
46-
s3Client s3iface.S3API
47-
ec2Client ec2iface.EC2API
48-
region string
49+
ssmClient ssmiface.SSMAPI
50+
s3Client s3iface.S3API
51+
ec2Client ec2iface.EC2API
52+
extendOutput bool
53+
region string
54+
s3Bucket string
55+
s3KeyPrefix string
56+
}
57+
58+
type Opts struct {
59+
ExtendOutput bool
60+
S3Bucket string
61+
S3KeyPrefix string
4962
}
5063

5164
// NewManager creates a new Manager from an AWS session and region.
52-
func NewManager(sess *session.Session, region string) *Manager {
53-
config := &aws.Config{Region: aws.String(region)}
54-
return &Manager{
55-
ssmClient: ssm.New(sess, config),
56-
s3Client: s3.New(sess, config),
57-
ec2Client: ec2.New(sess, config),
65+
func NewManager(sess *session.Session, region string, opts Opts) *Manager {
66+
awsCfg := &aws.Config{Region: aws.String(region)}
67+
m := &Manager{
68+
ssmClient: ssm.New(sess, awsCfg),
69+
s3Client: s3.New(sess, awsCfg),
70+
ec2Client: ec2.New(sess, awsCfg),
5871
region: region,
5972
}
73+
m.extendOutput = opts.ExtendOutput
74+
m.s3Bucket = opts.S3Bucket
75+
m.s3KeyPrefix = opts.S3KeyPrefix
76+
return m
6077
}
6178

6279
// NewTestManager creates a new manager for testing purposes.
@@ -154,7 +171,12 @@ func (m *Manager) RunCommand(instanceIds []string, command string) (string, erro
154171
Comment: aws.String("Interactive command."),
155172
Parameters: map[string][]*string{"commands": {aws.String(command)}},
156173
}
157-
174+
if m.s3Bucket != "" {
175+
input.OutputS3BucketName = aws.String(m.s3Bucket)
176+
}
177+
if m.s3KeyPrefix != "" {
178+
input.OutputS3KeyPrefix = aws.String(m.s3KeyPrefix)
179+
}
158180
res, err := m.ssmClient.SendCommand(input)
159181
if err != nil {
160182
return "", err
@@ -206,15 +228,15 @@ func (m *Manager) pollInstanceOutput(ctx context.Context, instanceID string, com
206228
CommandId: aws.String(commandID),
207229
InstanceId: aws.String(instanceID),
208230
})
209-
if out, ok := newCommandOutput(result, err); ok {
231+
if out, ok := m.newCommandOutput(result, err); ok {
210232
c <- out
211233
return
212234
}
213235
}
214236
}
215237
}
216238

217-
func newCommandOutput(result *ssm.GetCommandInvocationOutput, err error) (*CommandOutput, bool) {
239+
func (m *Manager) newCommandOutput(result *ssm.GetCommandInvocationOutput, err error) (*CommandOutput, bool) {
218240
out := &CommandOutput{
219241
InstanceID: aws.StringValue(result.InstanceId),
220242
Status: aws.StringValue(result.StatusDetails),
@@ -234,17 +256,53 @@ func newCommandOutput(result *ssm.GetCommandInvocationOutput, err error) (*Comma
234256
return out, true
235257
case "Success":
236258
out.Output = aws.StringValue(result.StandardOutputContent)
259+
out.OutputUrl = aws.StringValue(result.StandardOutputUrl)
260+
if m.extendOutput {
261+
return m.extendTruncatedOutput(*out), true
262+
}
237263
return out, true
238264
case "Failed":
239265
out.Output = aws.StringValue(result.StandardErrorContent)
266+
out.OutputUrl = aws.StringValue(result.StandardErrorUrl)
267+
if m.extendOutput {
268+
return m.extendTruncatedOutput(*out), true
269+
}
240270
return out, true
241271
default:
242272
out.Error = fmt.Errorf("Unrecoverable status: %s", out.Status)
243273
return out, true
244274
}
245275
}
246276

247-
func (m *Manager) readS3Output(bucket, key string) (string, error) {
277+
func (m *Manager) extendTruncatedOutput(out CommandOutput) *CommandOutput {
278+
const truncationMarker = "--output truncated--"
279+
if strings.Contains(out.Output, truncationMarker) {
280+
s3out, err := m.readOutput(out.OutputUrl)
281+
if err != nil {
282+
out.Error = errors.Wrap(err, "failed to fetch extended output")
283+
}
284+
out.Output = s3out
285+
return &out
286+
}
287+
return &out
288+
}
289+
290+
func (m *Manager) readOutput(url string) (string, error) {
291+
regex := regexp.MustCompile(`://s3[\-a-z0-9]*\.amazonaws.com/([^/]+)/(.+)|://([^.]+)\.s3\.amazonaws\.com/(.+)`)
292+
matches := regex.FindStringSubmatch(url)
293+
if len(matches) == 0 {
294+
return "", errors.Errorf("failed due to unexpected s3 url pattern: %s", url)
295+
}
296+
bucket := matches[1]
297+
key := matches[2]
298+
out, err := m.readS3Object(bucket, key)
299+
if err != nil {
300+
return "", errors.Wrapf(err, "failed to fetch s3 object: %s", url)
301+
}
302+
return out, nil
303+
}
304+
305+
func (m *Manager) readS3Object(bucket, key string) (string, error) {
248306
output, err := m.s3Client.GetObject(&s3.GetObjectInput{
249307
Bucket: aws.String(bucket),
250308
Key: aws.String(key),

0 commit comments

Comments
 (0)