Skip to content

Commit 2e86b4c

Browse files
author
itsdalmo
committed
Add support for filtering list by tag values.
1 parent 2f730cc commit 2e86b4c

File tree

5 files changed

+134
-33
lines changed

5 files changed

+134
-33
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ $ ssm-sh list --help
6565

6666
...
6767
[list command options]
68+
-f, --filter= Filter the produced list by tag (key=value,..)
6869
-l, --limit= Limit the number of instances printed (default: 50)
6970
-o, --output= Path to a file where the list of instances will be written as YAML.[list command options]
7071
```
@@ -84,11 +85,11 @@ $ ssm-sh run --help
8485
### Example
8586

8687
```bash
87-
$ vaulted -n lab-admin -- ssm-sh list -o example.json
88+
$ vaulted -n lab-admin -- ssm-sh list --filter Name="*itsdalmo" -o example.json
8889

8990
Instance ID | Name | State | Image ID | Platform | Version | IP | Status | Last pinged
90-
i-03762678c45546813 | ssm-manager-manual-test-kristian | running | ami-db1688a2 | Amazon Linux | 2.0 | 172.53.17.163 | Online | 2018-02-09 12:37
91-
i-0d04464ff18b5db7d | ssm-manager-manual-test-kristian | running | ami-db1688a2 | Amazon Linux | 2.0 | 172.53.20.172 | Online | 2018-02-09 12:39
91+
i-03762678c45546813 | ssm-manager-manual-test-itsdalmo | running | ami-db1688a2 | Amazon Linux | 2.0 | 172.53.17.163 | Online | 2018-02-09 12:37
92+
i-0d04464ff18b5db7d | ssm-manager-manual-test-itsdalmo | running | ami-db1688a2 | Amazon Linux | 2.0 | 172.53.20.172 | Online | 2018-02-09 12:39
9293

9394
$ vaulted -n lab-admin -- ssm-sh shell --target-file example.json
9495
Initialized with targets: [i-03762678c45546813 i-0d04464ff18b5db7d]

command/list.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import (
66
"github.com/pkg/errors"
77
"io/ioutil"
88
"os"
9+
"strings"
910
)
1011

1112
type ListCommand struct {
13+
Tags []*tag `short:"f" long:"filter" description:"Filter the produced list by tag (key=value,..)"`
1214
Limit int64 `short:"l" long:"limit" description:"Limit the number of instances printed" default:"50"`
1315
Output string `short:"o" long:"output" description:"Path to a file where the list of instances will be written as JSON."`
1416
}
@@ -20,7 +22,14 @@ func (command *ListCommand) Execute([]string) error {
2022
}
2123
m := manager.NewManager(sess, Command.AwsOpts.Region)
2224

23-
instances, err := m.ListInstances(command.Limit)
25+
var filters []*manager.TagFilter
26+
for _, tag := range command.Tags {
27+
filters = append(filters, &manager.TagFilter{
28+
Key: tag.Key,
29+
Values: tag.Values,
30+
})
31+
}
32+
instances, err := m.ListInstances(command.Limit, filters)
2433
if err != nil {
2534
return errors.Wrap(err, "failed to list instances")
2635
}
@@ -41,3 +50,22 @@ func (command *ListCommand) Execute([]string) error {
4150

4251
return nil
4352
}
53+
54+
type tag manager.TagFilter
55+
56+
func (t *tag) UnmarshalFlag(value string) error {
57+
parts := strings.Split(value, "=")
58+
if len(parts) != 2 {
59+
return errors.New("expected a key and a value separated by =")
60+
}
61+
62+
values := strings.Split(parts[1], ",")
63+
if len(values) < 1 {
64+
return errors.New("expected one or more values separated by ,")
65+
}
66+
67+
t.Key = parts[0]
68+
t.Values = values
69+
70+
return nil
71+
}

manager/manager.go

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,28 @@ import (
1818
"time"
1919
)
2020

21+
// TagFilter represents a key=value pair for AWS EC2 tags.
22+
type TagFilter struct {
23+
Key string
24+
Values []string
25+
}
26+
27+
// Filter returns the ec2.Filter representation of the TagFilter.
28+
func (t *TagFilter) Filter() *ec2.Filter {
29+
return &ec2.Filter{
30+
Name: aws.String(fmt.Sprintf("tag:%s", t.Key)),
31+
Values: aws.StringSlice(t.Values),
32+
}
33+
}
34+
35+
// CommandOutput is the return type transmitted over a channel when fetching output.
36+
type CommandOutput struct {
37+
InstanceID string
38+
Status string
39+
Output string
40+
Error error
41+
}
42+
2143
// Manager handles the clients interfacing with AWS.
2244
type Manager struct {
2345
ssmClient ssmiface.SSMAPI
@@ -48,7 +70,7 @@ func NewTestManager(ssm ssmiface.SSMAPI, s3 s3iface.S3API, ec2 ec2iface.EC2API)
4870
}
4971

5072
// ListInstances fetches a list of instances managed by SSM. Paginates until all responses have been collected.
51-
func (m *Manager) ListInstances(limit int64) ([]*Instance, error) {
73+
func (m *Manager) ListInstances(limit int64, tagFilters []*TagFilter) ([]*Instance, error) {
5274
var out []*Instance
5375

5476
input := &ssm.DescribeInstanceInformationInput{
@@ -60,7 +82,7 @@ func (m *Manager) ListInstances(limit int64) ([]*Instance, error) {
6082
if err != nil {
6183
return nil, errors.Wrap(err, "failed to describe instance information")
6284
}
63-
ssmInstances, ec2Instances, err := m.describeInstances(response.InstanceInformationList)
85+
ssmInstances, ec2Instances, err := m.describeInstances(response.InstanceInformationList, tagFilters)
6486
if err != nil {
6587
return nil, errors.Wrap(err, "failed to retrieve ec2 instance information")
6688
}
@@ -79,25 +101,29 @@ func (m *Manager) ListInstances(limit int64) ([]*Instance, error) {
79101
}
80102

81103
// describeInstances retrieves additional information about SSM managed instances from EC2.
82-
func (m *Manager) describeInstances(instances []*ssm.InstanceInformation) (map[string]*ssm.InstanceInformation, map[string]*ec2.Instance, error) {
104+
func (m *Manager) describeInstances(instances []*ssm.InstanceInformation, tagFilters []*TagFilter) (map[string]*ssm.InstanceInformation, map[string]*ec2.Instance, error) {
83105
var ids []*string
106+
var filters []*ec2.Filter
84107

85108
org := make(map[string]*ssm.InstanceInformation)
86109
out := make(map[string]*ec2.Instance)
87110

88111
for _, instance := range instances {
89-
id := aws.StringValue(instance.InstanceId)
90-
org[id] = instance
112+
org[aws.StringValue(instance.InstanceId)] = instance
91113
ids = append(ids, instance.InstanceId)
92114
}
93115

116+
filters = append(filters, &ec2.Filter{
117+
Name: aws.String("instance-id"),
118+
Values: ids,
119+
})
120+
121+
for _, f := range tagFilters {
122+
filters = append(filters, f.Filter())
123+
}
124+
94125
input := &ec2.DescribeInstancesInput{
95-
Filters: []*ec2.Filter{
96-
{
97-
Name: aws.String("instance-id"),
98-
Values: ids,
99-
},
100-
},
126+
Filters: filters,
101127
}
102128

103129
for {
@@ -149,14 +175,6 @@ func (m *Manager) AbortCommand(instanceIds []string, commandID string) error {
149175
return nil
150176
}
151177

152-
// CommandOutput is the return type transmitted over a channel when fetching output.
153-
type CommandOutput struct {
154-
InstanceID string
155-
Status string
156-
Output string
157-
Error error
158-
}
159-
160178
// GetCommandOutput fetches the results from a command invocation for all specified instanceIds and
161179
// closes the receiving channel before exiting.
162180
func (m *Manager) GetCommandOutput(ctx context.Context, instanceIds []string, commandID string, out chan<- *CommandOutput) {

manager/manager_test.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,15 +105,15 @@ func TestList(t *testing.T) {
105105

106106
t.Run("Get managed instances works", func(t *testing.T) {
107107
expected := outputInstances
108-
actual, err := m.ListInstances(50)
108+
actual, err := m.ListInstances(50, nil)
109109
assert.Nil(t, err)
110110
assert.NotNil(t, actual)
111111
assert.Equal(t, expected, actual)
112112
})
113113

114114
t.Run("Limit number of instances works", func(t *testing.T) {
115115
expected := outputInstances[:1]
116-
actual, err := m.ListInstances(1)
116+
actual, err := m.ListInstances(1, nil)
117117
assert.Nil(t, err)
118118
assert.NotNil(t, actual)
119119
assert.Equal(t, expected, actual)
@@ -126,7 +126,22 @@ func TestList(t *testing.T) {
126126
}()
127127

128128
expected := outputInstances
129-
actual, err := m.ListInstances(50)
129+
actual, err := m.ListInstances(50, nil)
130+
assert.Nil(t, err)
131+
assert.NotNil(t, actual)
132+
assert.Equal(t, expected, actual)
133+
})
134+
135+
t.Run("TagFilter works", func(t *testing.T) {
136+
expected := outputInstances[:1]
137+
actual, err := m.ListInstances(50, []*manager.TagFilter{
138+
{
139+
Key: "Name",
140+
Values: []string{
141+
"1",
142+
},
143+
},
144+
})
130145
assert.Nil(t, err)
131146
assert.NotNil(t, actual)
132147
assert.Equal(t, expected, actual)
@@ -138,7 +153,7 @@ func TestList(t *testing.T) {
138153
ssmMock.Error = false
139154
}()
140155

141-
actual, err := m.ListInstances(50)
156+
actual, err := m.ListInstances(50, nil)
142157
assert.NotNil(t, err)
143158
assert.EqualError(t, err, "failed to describe instance information: expected")
144159
assert.Nil(t, actual)

manager/testing.go

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,59 @@ func (mock *MockEC2) DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2.
2626
return nil, errors.New("expected")
2727
}
2828

29+
var out []*ec2.Instance
30+
var tmp []*ec2.Instance
2931
var ids []string
32+
var nameFilters []string
33+
3034
for _, filter := range input.Filters {
31-
if aws.StringValue(filter.Name) == "instance-id" {
35+
key := aws.StringValue(filter.Name)
36+
if key == "instance-id" {
3237
ids = aws.StringValueSlice(filter.Values)
38+
} else if key == "tag:Name" {
39+
nameFilters = aws.StringValueSlice(filter.Values)
3340
}
3441
}
3542

36-
if ids == nil {
37-
return nil, errors.New("missing instance ids in input")
43+
// Filter instance ids if a list was provided. If not, we
44+
// provide the entire list of instances.
45+
if ids != nil {
46+
for _, id := range ids {
47+
instance, ok := mock.Instances[id]
48+
if !ok {
49+
return nil, errors.New("instance id does not exist")
50+
}
51+
tmp = append(tmp, instance)
52+
}
53+
54+
} else {
55+
for _, instance := range mock.Instances {
56+
tmp = append(tmp, instance)
57+
}
3858
}
3959

40-
var out []*ec2.Instance
41-
for _, id := range ids {
42-
out = append(out, mock.Instances[id])
60+
// If a tag filter was supplied (only Name is supported for testing),
61+
// filter instances which don't match.
62+
if nameFilters != nil && len(nameFilters) > 0 {
63+
for _, instance := range tmp {
64+
for _, tag := range instance.Tags {
65+
// Look for Name tag.
66+
if aws.StringValue(tag.Key) != "Name" {
67+
continue
68+
}
69+
// Once it is found, check whether it contains
70+
// any of the name filters (simple contains).
71+
name := aws.StringValue(tag.Value)
72+
for _, filter := range nameFilters {
73+
if strings.Contains(name, filter) {
74+
out = append(out, instance)
75+
}
76+
}
77+
}
78+
}
79+
80+
} else {
81+
out = tmp
4382
}
4483

4584
// NOTE: It should not matter if we have multiple reservations

0 commit comments

Comments
 (0)