@@ -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.
4448type 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