Skip to content

Commit 9e6d8e5

Browse files
akerl-unprivakerl
authored andcommitted
Feature/xargs (#23)
* initial attempt at xargs support * simplify account lookup logic * simplify xargs using new speculate * remove unused exports * fix cred caching * add multi implementation * finish multi initial design * fix linting * add changelog
1 parent 2a13ba5 commit 9e6d8e5

File tree

10 files changed

+305
-16
lines changed

10 files changed

+305
-16
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# 2.1.0 / 2019-08-19
2+
3+
* [FEATURE] Add xargs subcommand
4+
* [FEATURE] Mutex for thread-safe credential lookups
5+
16
# 2.0.9 / 2019-08-15
27

38
* [FEATURE] Update speculate to handle platform-specific env vars

cmd/travel.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,15 @@ func travelRunner(cmd *cobra.Command, args []string) error {
8484
}}
8585
}
8686

87-
creds, err := path.TraverseWithOptions(opts)
87+
c, err := path.TraverseWithOptions(opts)
8888
if err != nil {
8989
return err
9090
}
9191

92-
for _, line := range creds.ToEnvVars() {
92+
for _, line := range c.ToEnvVars() {
9393
fmt.Println(line)
9494
}
95-
url, err := creds.ToCustomConsoleURL(servicePath)
95+
url, err := c.ToCustomConsoleURL(servicePath)
9696
if err != nil {
9797
return err
9898
}

cmd/xargs.go

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"github.com/akerl/voyager/v2/cartogram"
8+
"github.com/akerl/voyager/v2/multi"
9+
"github.com/akerl/voyager/v2/travel"
10+
"github.com/akerl/voyager/v2/yubikey"
11+
12+
"github.com/akerl/input/list"
13+
"github.com/akerl/speculate/v2/creds"
14+
"github.com/spf13/cobra"
15+
)
16+
17+
var xargsCmd = &cobra.Command{
18+
Use: "xargs",
19+
Short: "Run a command across many AWS accounts",
20+
RunE: xargsRunner,
21+
}
22+
23+
func init() {
24+
rootCmd.AddCommand(xargsCmd)
25+
xargsCmd.Flags().StringP("role", "r", "", "Choose target role to use")
26+
xargsCmd.Flags().String("profile", "", "Choose source profile to use")
27+
xargsCmd.Flags().StringP("prompt", "p", "", "Choose prompt to use")
28+
xargsCmd.Flags().BoolP("yubikey", "y", false, "Use Yubikey for MFA")
29+
xargsCmd.Flags().StringP("command", "c", "", "Command to execute")
30+
}
31+
32+
// revive:disable-next-line:cyclomatic
33+
func xargsRunner(cmd *cobra.Command, args []string) error {
34+
flags := cmd.Flags()
35+
36+
flagRole, err := flags.GetString("role")
37+
if err != nil {
38+
return err
39+
}
40+
41+
flagProfile, err := flags.GetString("profile")
42+
if err != nil {
43+
return err
44+
}
45+
46+
promptFlag, err := flags.GetString("prompt")
47+
if err != nil {
48+
return err
49+
}
50+
promptGenerator, ok := list.Types[promptFlag]
51+
if !ok {
52+
return fmt.Errorf("prompt type not found: %s", promptFlag)
53+
}
54+
prompt := promptGenerator()
55+
56+
useYubikey, err := flags.GetBool("yubikey")
57+
if err != nil {
58+
return err
59+
}
60+
61+
commandStr, err := flags.GetString("command")
62+
if err != nil {
63+
return err
64+
}
65+
if commandStr == "" {
66+
return fmt.Errorf("command must be provided via --command / -c")
67+
}
68+
69+
pack := cartogram.Pack{}
70+
if err := pack.Load(); err != nil {
71+
return err
72+
}
73+
74+
grapher := travel.Grapher{
75+
Prompt: prompt,
76+
Pack: pack,
77+
}
78+
79+
opts := travel.DefaultTraverseOptions()
80+
if useYubikey {
81+
opts.MfaPrompt = &creds.MultiMfaPrompt{Backends: []creds.MfaPrompt{
82+
yubikey.NewPrompt(),
83+
&creds.DefaultMfaPrompt{},
84+
}}
85+
}
86+
87+
processor := multi.Processor{
88+
Grapher: grapher,
89+
Options: opts,
90+
Args: args,
91+
RoleNames: []string{flagRole},
92+
ProfileNames: []string{flagProfile},
93+
}
94+
95+
results, err := processor.ExecString(commandStr)
96+
if err != nil {
97+
return err
98+
}
99+
100+
buffer, err := json.MarshalIndent(results, "", " ")
101+
if err != nil {
102+
return err
103+
}
104+
fmt.Println(string(buffer))
105+
106+
return nil
107+
}

go.mod

+4-2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ go 1.12
44

55
require (
66
github.com/99designs/keyring v1.1.1
7+
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69
78
github.com/akerl/input v0.0.3
8-
github.com/akerl/speculate/v2 v2.0.8
9+
github.com/akerl/speculate/v2 v2.1.0
910
github.com/akerl/timber/v2 v2.0.1
10-
github.com/aws/aws-sdk-go v1.23.1
11+
github.com/aws/aws-sdk-go v1.23.3
1112
github.com/spf13/cobra v0.0.5
13+
github.com/vbauerster/mpb/v4 v4.9.2
1214
github.com/yawn/ykoath v1.0.2
1315
)

go.sum

+13-7
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
github.com/99designs/keyring v1.1.1 h1:3XSIPsh/MH5tHYT7ub1LoOkpGQuW/TdK//c04TnU2j4=
22
github.com/99designs/keyring v1.1.1/go.mod h1:657DQuMrBZRtuL/voxVyiyb6zpMehlm5vLB9Qwrv904=
3+
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU=
4+
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg=
35
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
6+
github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM=
7+
github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA=
48
github.com/akerl/input v0.0.3 h1:gy9u/L8WJWIJ9OB/5HSdOBIjrFfVkrRxmRlWd3I9+Co=
59
github.com/akerl/input v0.0.3/go.mod h1:AVqHnOGmoQDEzX4jMX/ZZOC20+7MfodkmNbXhzqIJ2U=
6-
github.com/akerl/speculate/v2 v2.0.8 h1:NJ8xQGopA3OlFFOir7duni2K3aN3h75eolVC+NVK8nY=
7-
github.com/akerl/speculate/v2 v2.0.8/go.mod h1:qkhyP7kYtsPqTNpuE+6S4/gx5q2X1AmTcuDqp7Jmegw=
10+
github.com/akerl/speculate/v2 v2.1.0 h1:pmz1d5iIaGESJ/zh05P0eAURfx6o4+yy0Z99qnBWwwM=
11+
github.com/akerl/speculate/v2 v2.1.0/go.mod h1:qkhyP7kYtsPqTNpuE+6S4/gx5q2X1AmTcuDqp7Jmegw=
812
github.com/akerl/timber/v2 v2.0.1 h1:hY4VCOJns7KsxwxP/ifSt3Rz9GZCfKewapaimObnA2E=
913
github.com/akerl/timber/v2 v2.0.1/go.mod h1:jBjRGI2CWuvbZlrZkp1JO/X51pMlbg72NFy+Vnd59oI=
1014
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
1115
github.com/aws/aws-sdk-go v1.22.3/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
12-
github.com/aws/aws-sdk-go v1.23.1 h1:MXfB75PkuWJJmZMCQ40haFUuOIIGt1FuiWtPBXy8URA=
13-
github.com/aws/aws-sdk-go v1.23.1/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
16+
github.com/aws/aws-sdk-go v1.23.3 h1:Ty/4P6tOFJkDnKDrFJWnveznvESblf8QOheD1CwQPDU=
17+
github.com/aws/aws-sdk-go v1.23.3/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
1418
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
1519
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
1620
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@@ -27,15 +31,14 @@ github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a/go.mod h1:7Bv
2731
github.com/ebfe/scard v0.0.0-20190212122703-c3d1b1916a95 h1:OM0MnUcXBysj7ZtXvThVWHMoahuKQ8FuwIdeSLcNdP4=
2832
github.com/ebfe/scard v0.0.0-20190212122703-c3d1b1916a95/go.mod h1:8hHvF8DlEq5kE3KWOsZQezdWq1OTOVxZArZMscS954E=
2933
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
30-
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0=
3134
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
3235
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
33-
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU=
3436
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0=
3537
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
3638
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
3739
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
3840
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
41+
github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d h1:Z+RDyXzjKE0i2sTjZ/b1uxiGtPhFy34Ou/Tk0qwN0kM=
3942
github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d/go.mod h1:JJNrCn9otv/2QP4D7SMJBgaleKpOf66PnW6F5WGNRIc=
4043
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
4144
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -71,6 +74,8 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH
7174
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
7275
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
7376
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
77+
github.com/vbauerster/mpb/v4 v4.9.2 h1:/niWWc4MsgvOaCXJCdrWq2vOyN5inuQIq/Spn4Fi1XI=
78+
github.com/vbauerster/mpb/v4 v4.9.2/go.mod h1:svMK4M+/dWn+xLRlw1Hq4MzYq+5yX2moH6tktD+lIk0=
7479
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
7580
github.com/yawn/ykoath v1.0.2 h1:BYrUYmzu7nRjiqx5olhVhVZqh5NEUUMJf49ZqgrlEnc=
7681
github.com/yawn/ykoath v1.0.2/go.mod h1:dcXMmLrvt6WFkySkG2k8ZEqxiTbu/TWSI4+/Cb54+Lg=
@@ -83,8 +88,9 @@ golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5h
8388
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
8489
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
8590
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
86-
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI=
8791
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
92+
golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa h1:KIDDMLT1O0Nr7TSxp8xM5tJcdn8tgyAONntO829og1M=
93+
golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
8894
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
8995
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
9096
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

main.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
package main
22

33
import (
4-
"fmt"
54
"os"
65

76
"github.com/akerl/voyager/v2/cmd"
7+
8+
"github.com/akerl/speculate/v2/helpers"
89
)
910

1011
func main() {
1112
if err := cmd.Execute(); err != nil {
12-
fmt.Fprintln(os.Stderr, err)
13+
helpers.PrintAwsError(err)
1314
os.Exit(1)
1415
}
1516
}

multi/main.go

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package multi
2+
3+
import (
4+
"os"
5+
"strings"
6+
"time"
7+
8+
"github.com/akerl/voyager/v2/travel"
9+
10+
"github.com/akerl/speculate/v2/creds"
11+
"github.com/akerl/timber/v2/log"
12+
"github.com/vbauerster/mpb/v4"
13+
"github.com/vbauerster/mpb/v4/decor"
14+
)
15+
16+
var logger = log.NewLogger("voyager")
17+
18+
// Processor defines the settings for parallel processing
19+
type Processor struct {
20+
Grapher travel.Grapher
21+
Options travel.TraverseOptions
22+
Args []string
23+
RoleNames []string
24+
ProfileNames []string
25+
}
26+
27+
// ExecString runs a command string against a set of accounts
28+
func (p Processor) ExecString(cmd string) (map[string]creds.ExecResult, error) {
29+
cmdSlice := strings.Split(cmd, " ")
30+
return p.Exec(cmdSlice)
31+
}
32+
33+
// Exec runs a command against a set of accounts
34+
func (p Processor) Exec(cmd []string) (map[string]creds.ExecResult, error) {
35+
logger.InfoMsgf("processing command: %v", cmd)
36+
37+
paths, err := p.Grapher.ResolveAll(p.Args, p.RoleNames, p.ProfileNames)
38+
if err != nil {
39+
return map[string]creds.ExecResult{}, err
40+
}
41+
42+
inputCh := make(chan workerInput, len(paths))
43+
outputCh := make(chan workerOutput, len(paths))
44+
refreshCh := make(chan time.Time)
45+
46+
for i := 1; i <= 10; i++ {
47+
go execWorker(inputCh, outputCh)
48+
}
49+
50+
for _, item := range paths {
51+
inputCh <- workerInput{
52+
Path: item,
53+
Options: p.Options,
54+
Command: cmd,
55+
}
56+
}
57+
close(inputCh)
58+
59+
progress := mpb.New(
60+
mpb.WithOutput(os.Stderr),
61+
mpb.WithManualRefresh(refreshCh),
62+
)
63+
bar := progress.AddBar(
64+
int64(len(paths)),
65+
mpb.AppendDecorators(
66+
decor.Percentage(),
67+
),
68+
mpb.PrependDecorators(
69+
decor.CountersNoUnit("%d / %d", decor.WCSyncWidth),
70+
),
71+
)
72+
73+
output := map[string]creds.ExecResult{}
74+
for i := 1; i <= len(paths); i++ {
75+
result := <-outputCh
76+
output[result.AccountID] = result.ExecResult
77+
bar.Increment()
78+
refreshCh <- time.Now()
79+
}
80+
progress.Wait()
81+
82+
return output, nil
83+
}
84+
85+
type workerInput struct {
86+
Path travel.Path
87+
Options travel.TraverseOptions
88+
Command []string
89+
}
90+
91+
type workerOutput struct {
92+
AccountID string
93+
ExecResult creds.ExecResult
94+
}
95+
96+
func execWorker(inputCh <-chan workerInput, outputCh chan<- workerOutput) {
97+
for item := range inputCh {
98+
c, err := item.Path.TraverseWithOptions(item.Options)
99+
if err != nil {
100+
outputCh <- workerOutput{ExecResult: creds.ExecResult{Error: err}}
101+
continue
102+
}
103+
accountID, err := c.AccountID()
104+
if err != nil {
105+
outputCh <- workerOutput{ExecResult: creds.ExecResult{Error: err}}
106+
continue
107+
}
108+
outputCh <- workerOutput{
109+
AccountID: accountID,
110+
ExecResult: c.Exec(item.Command),
111+
}
112+
}
113+
}

travel/cache.go

+5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package travel
22

33
import (
44
"fmt"
5+
56
"github.com/akerl/speculate/v2/creds"
67
"github.com/aws/aws-sdk-go/service/sts"
78
)
@@ -16,6 +17,7 @@ type Cache interface {
1617
// CheckCache returns credentials if they exist in the cache and are still valid
1718
// If the credentials exist but are invalid/expired, it removes them from the cache
1819
func CheckCache(c Cache, h Hop) (creds.Creds, bool) {
20+
logger.DebugMsgf("checking cache for %+v", h)
1921
cachedCreds, ok := c.Get(h)
2022
if !ok {
2123
return creds.Creds{}, false
@@ -57,6 +59,7 @@ type MapCache struct {
5759
// Put stores the credentials in the map
5860
func (mc *MapCache) Put(h Hop, c creds.Creds) error {
5961
key := mc.hopToKey(h)
62+
logger.DebugMsgf("mapcache: caching %s", key)
6063
if mc.creds == nil {
6164
mc.creds = map[string]creds.Creds{}
6265
}
@@ -67,13 +70,15 @@ func (mc *MapCache) Put(h Hop, c creds.Creds) error {
6770
// Get returns credentials from the map, if they exist
6871
func (mc *MapCache) Get(h Hop) (creds.Creds, bool) {
6972
key := mc.hopToKey(h)
73+
logger.DebugMsgf("mapcache: getting %s", key)
7074
creds, ok := mc.creds[key]
7175
return creds, ok
7276
}
7377

7478
// Delete removes credentials from the cache
7579
func (mc *MapCache) Delete(h Hop) error {
7680
key := mc.hopToKey(h)
81+
logger.DebugMsgf("mapcache: deleting %s", key)
7782
delete(mc.creds, key)
7883
return nil
7984
}

0 commit comments

Comments
 (0)