Last active
November 30, 2021 20:10
-
-
Save kwilczynski/bb0c147bcd6f7669822f2cb94c249bde to your computer and use it in GitHub Desktop.
A small parser for a grammar used to filter EC2 instances based on their tags. Inspired by Ansible.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"crypto/sha1" | |
"fmt" | |
"regexp" | |
"strings" | |
"github.com/aws/aws-sdk-go/aws" | |
"github.com/aws/aws-sdk-go/service/ec2" | |
) | |
type FilterMode int | |
func (fm FilterMode) String() string { | |
if fm == ReverseMode { | |
return "ReverseMode" | |
} | |
return "DefaultMode" | |
} | |
const ( | |
DefaultMode FilterMode = iota | |
ReverseMode | |
) | |
var normalizeStringRegex = regexp.MustCompile(`[^A-Za-z0-9_]`) | |
type Filter struct { | |
s string | |
mode FilterMode | |
include []map[string]string | |
exclude []map[string]string | |
} | |
func NewFilter(s string) *Filter { | |
f := &Filter{ | |
s: s, | |
mode: DefaultMode, | |
include: []map[string]string{}, | |
exclude: []map[string]string{}, | |
} | |
return f | |
} | |
func (f *Filter) Hash() string { | |
return fmt.Sprintf("%x", sha1.Sum([]byte(f.s))) | |
} | |
func (f *Filter) Parse() *Filter { | |
asterisk := []map[string]string{{"*": ""}} | |
if f.s == "" { | |
f.include = asterisk | |
return f | |
} | |
var ( | |
k, v string | |
p *[]map[string]string | |
) | |
splitter := func(s *string) (k, v string) { | |
ss := strings.Split(*s, "=") | |
v = "" | |
if len(ss) > 1 { | |
v = ss[1] | |
} | |
k = ss[0] | |
return | |
} | |
selector := func(s *string, pp *[]map[string]string) { | |
if strings.HasPrefix(*s, "!") { | |
*s = strings.TrimPrefix(*s, "!") | |
pp = &f.exclude | |
} | |
p = pp | |
} | |
filters := strings.Split(f.s, ",") | |
for _, filter := range filters { | |
switch filter { | |
case "!", "*!": | |
panic("filter must include a value to exclude") | |
case "*": | |
f.mode = DefaultMode | |
case "!*": | |
f.mode = ReverseMode | |
} | |
selector(&filter, &f.include) | |
if strings.Contains(filter, "&") { | |
m := make(map[string]string) | |
for _, ss := range strings.Split(filter, "&") { | |
selector(&ss, p) | |
k, v = splitter(&ss) | |
m[k] = v | |
} | |
*p = append(*p, m) | |
continue | |
} | |
k, v = splitter(&filter) | |
*p = append(*p, map[string]string{ | |
k: v, | |
}) | |
} | |
if len(f.include) < 1 && f.mode == DefaultMode { | |
f.include = asterisk | |
} | |
return f | |
} | |
func (f *Filter) Filter(instances []*ec2.Instance) (results []*ec2.Instance) { | |
includeIndex := f.filter(&instances, &f.include) | |
excludeIndex := f.filter(&instances, &f.exclude) | |
for k, v := range includeIndex { | |
_, ok := excludeIndex[k] | |
if ok == (f.mode == ReverseMode) { | |
results = append(results, v) | |
} | |
} | |
return | |
} | |
func (f *Filter) wildcardMatch(pattern, s string) bool { | |
var ( | |
p int | |
n int | |
nextP int | |
nextN int | |
) | |
if pattern == "*" { | |
return true | |
} | |
for n < len(s) || p < len(pattern) { | |
if p < len(pattern) { | |
c := pattern[p] | |
switch c { | |
case '?': | |
if n < len(s) { | |
p++ | |
n++ | |
continue | |
} | |
case '*': | |
nextP = p | |
nextN = n + 1 | |
p++ | |
continue | |
default: | |
if n < len(s) && s[n] == c { | |
p++ | |
n++ | |
continue | |
} | |
} | |
} | |
if 0 < nextN && nextN <= len(s) { | |
p = nextP | |
n = nextN | |
continue | |
} | |
return false | |
} | |
return true | |
} | |
func (f *Filter) normalizeString(s string) string { | |
return normalizeStringRegex.ReplaceAllString(s, "_") | |
} | |
func (f *Filter) flattenTags(ec2Tags *[]*ec2.Tag) (tags map[string]string) { | |
tags = make(map[string]string) | |
const awsTagPrefix = "aws:" | |
if len(*ec2Tags) > 0 { | |
var k, v string | |
for _, tag := range *ec2Tags { | |
k = aws.StringValue(tag.Key) | |
v = aws.StringValue(tag.Value) | |
if !strings.HasPrefix(k, awsTagPrefix) { | |
k = f.normalizeString(k) | |
v = f.normalizeString(v) | |
tags[k] = v | |
} | |
} | |
} | |
return | |
} | |
func (f *Filter) filter(instances *[]*ec2.Instance, tags *[]map[string]string) map[string]*ec2.Instance { | |
index := make(map[string]*ec2.Instance) | |
for _, instance := range *instances { | |
instanceTags := f.flattenTags(&instance.Tags) | |
for _, tag := range *tags { | |
var ( | |
score int | |
seen int | |
match bool | |
) | |
for tk, tv := range tag { | |
if tk == "*" && (tv == "*" || tv == "") { | |
match = true | |
break | |
} | |
for ik, iv := range instanceTags { | |
ok := f.wildcardMatch(tk, ik) | |
if ok && (f.wildcardMatch(tv, iv) || tv == "") { | |
score++ | |
} | |
} | |
seen++ | |
} | |
if seen > 1 { | |
if score == seen { | |
match = true | |
} | |
} else { | |
if score >= seen { | |
match = true | |
} | |
} | |
if match { | |
index[*instance.InstanceId] = instance | |
} | |
} | |
} | |
return index | |
} | |
func main() { | |
mocks := []*ec2.Instance{ | |
&ec2.Instance{ | |
InstanceId: aws.String("i-0"), | |
}, | |
&ec2.Instance{ | |
InstanceId: aws.String("i-1"), | |
Tags: []*ec2.Tag{ | |
&ec2.Tag{Key: aws.String("test1")}, | |
}, | |
}, | |
&ec2.Instance{ | |
InstanceId: aws.String("i-2"), | |
Tags: []*ec2.Tag{ | |
&ec2.Tag{ | |
Key: aws.String("test2"), | |
Value: aws.String("test2"), | |
}, | |
}, | |
}, | |
&ec2.Instance{ | |
InstanceId: aws.String("i-3"), | |
Tags: []*ec2.Tag{ | |
&ec2.Tag{ | |
Key: aws.String("test3"), | |
//Value: aws.String(""), | |
Value: aws.String("test3"), | |
}, | |
&ec2.Tag{ | |
Key: aws.String("test4"), | |
Value: aws.String("test8"), | |
}, | |
&ec2.Tag{ | |
Key: aws.String("test5"), | |
Value: aws.String("test5"), | |
}, | |
}, | |
}, | |
&ec2.Instance{ | |
InstanceId: aws.String("i-4"), | |
Tags: []*ec2.Tag{ | |
&ec2.Tag{ | |
Key: aws.String("test3"), | |
//Value: aws.String(""), | |
}, | |
}, | |
}, | |
&ec2.Instance{ | |
InstanceId: aws.String("i-5"), | |
Tags: []*ec2.Tag{ | |
&ec2.Tag{ | |
Key: aws.String("xyz"), | |
Value: aws.String("xyz"), | |
}, | |
&ec2.Tag{ | |
Key: aws.String("test5"), | |
Value: aws.String("test5"), | |
}, | |
}, | |
}, | |
&ec2.Instance{ | |
InstanceId: aws.String("i-6"), | |
Tags: []*ec2.Tag{ | |
&ec2.Tag{ | |
Key: aws.String("foo bar"), | |
Value: aws.String("baz-quux"), | |
}, | |
&ec2.Tag{ | |
Key: aws.String("test123,test456"), | |
Value: aws.String("test123,test456"), | |
}, | |
}, | |
}, | |
&ec2.Instance{ | |
InstanceId: aws.String("i-7"), | |
Tags: []*ec2.Tag{ | |
&ec2.Tag{ | |
Key: aws.String("test"), | |
Value: aws.String("42"), | |
}, | |
}, | |
}, | |
&ec2.Instance{ | |
InstanceId: aws.String("i-8"), | |
Tags: []*ec2.Tag{ | |
&ec2.Tag{ | |
Key: aws.String("test"), | |
Value: aws.String("252525"), | |
}, | |
}, | |
}, | |
} | |
//s := "" | |
//s := "*" | |
//s := "!" | |
//s := "*!" | |
//s := "!*" | |
//s := "!*,test5" | |
//s := "test5,!*" | |
//s := "!test5,*" | |
//s := "!test5" | |
//s := "!test5&xy?,*" | |
//s := "*,!xyz" | |
//s := "foo?bar" | |
//s := "foo_bar,test5" | |
//s := "foo-bar,test5" | |
//s := "!*,foo bar,test5" | |
//s := "test123_test456" | |
//s := "test=!test123 | |
s := "xyz,test,!test=252525" | |
f := NewFilter(s) | |
//fmt.Println(f.Hash()) | |
fmt.Println(f.Parse().Filter(mocks)) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[{ | |
InstanceId: "i-5", | |
Tags: [{ | |
Key: "xyz", | |
Value: "xyz" | |
},{ | |
Key: "test5", | |
Value: "test5" | |
}] | |
} { | |
InstanceId: "i-7", | |
Tags: [{ | |
Key: "test", | |
Value: "42" | |
}] | |
}] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment