Skip to content

Instantly share code, notes, and snippets.

@kwilczynski
Last active November 30, 2021 20:10
Show Gist options
  • Save kwilczynski/bb0c147bcd6f7669822f2cb94c249bde to your computer and use it in GitHub Desktop.
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.
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))
}
[{
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