Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ vendor
dist
/yace
*.tar.gz
.idea
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* [FEATURE] ...
* [ENHANCEMENT] ...
* [BUGFIX] ...
* [FEATURE] Add `exactMatch` option to `searchTags` for server-side tag filtering by @njo

## 0.63.0 / 2025-09-25

Expand Down
11 changes: 8 additions & 3 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ roles:
[ - <role_config> ... ]

# List of Key/Value pairs to use for tag filtering (all must match).
# The key is the AWS Tag key and is case-sensitive
# The value will be treated as a regex
# The key is the AWS Tag key and is case-sensitive.
# The value will be treated as a regex and filtered client side unless exactMatch is set.
searchTags:
[ - <search_tags_config> ... ]

Expand Down Expand Up @@ -393,7 +393,12 @@ This is an example of the `search_tags_config` block:
```yaml
searchTags:
- key: env
value: production
# Value is a regex pattern. Keys are filtered server-side. Value filtering happens client-side.
value: "(prod|staging)"
- key: team
value: accounting
# When exactMatch is `true`, the value is no longer treated as regex and filtering happens server-side.
exactMatch: true
```

### `custom_tags_config`
Expand Down
12 changes: 8 additions & 4 deletions pkg/clients/tagging/v1/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,16 @@ func (c client) GetResources(ctx context.Context, job model.DiscoveryJob, region
// We can't take a pointer to any fields from loop variable or the pointer will always be the same and this logic will be broken.
st := job.SearchTags[i]

// AWS's GetResources has a TagFilter option which matches the semantics of our SearchTags where all filters must match
// Their value matching implementation is different though so instead of mapping the Key and Value we only map the Keys.
// When we call AWS's GetResources and specify a TagFilter the value is optional.
// Their API docs say, "If you don't specify a value for a key, the response returns all resources that are tagged with that key, with any or no value."
// which makes this a safe way to reduce the amount of data we need to filter out.
// So in the default case we just specify the key and do client side filtering on the results to allow for regex matching.
// If the ExactMatch flag is set we do server side filtering to reduce the volume of data being returned.
// https://docs.aws.amazon.com/resourcegroupstagging/latest/APIReference/API_GetResources.html#resourcegrouptagging-GetResources-request-TagFilters
tagFilters = append(tagFilters, &resourcegroupstaggingapi.TagFilter{Key: &st.Key})
tf := &resourcegroupstaggingapi.TagFilter{Key: &st.Key}
if st.ExactMatch {
tf.Values = []*string{&st.ExactValue}
}
tagFilters = append(tagFilters, tf)
}
}

Expand Down
12 changes: 8 additions & 4 deletions pkg/clients/tagging/v2/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,16 @@ func (c client) GetResources(ctx context.Context, job model.DiscoveryJob, region
// We can't take a pointer to any fields from loop variable or the pointer will always be the same and this logic will be broken.
st := job.SearchTags[i]

// AWS's GetResources has a TagFilter option which matches the semantics of our SearchTags where all filters must match
// Their value matching implementation is different though so instead of mapping the Key and Value we only map the Keys.
// When we call AWS's GetResources and specify a TagFilter the value is optional.
// Their API docs say, "If you don't specify a value for a key, the response returns all resources that are tagged with that key, with any or no value."
// which makes this a safe way to reduce the amount of data we need to filter out.
// So in the default case we just specify the key and do client side filtering on the results to allow for regex matching.
// If the ExactMatch flag is set we do server side filtering to reduce the volume of data being returned.
// https://docs.aws.amazon.com/resourcegroupstagging/latest/APIReference/API_GetResources.html#resourcegrouptagging-GetResources-request-TagFilters
tagFilters = append(tagFilters, types.TagFilter{Key: &st.Key})
tf := types.TagFilter{Key: &st.Key}
if st.ExactMatch {
tf.Values = []string{st.ExactValue}
}
tagFilters = append(tagFilters, tf)
}
}
inputparams := &resourcegroupstaggingapi.GetResourcesInput{
Expand Down
28 changes: 18 additions & 10 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ type Discovery struct {
type ExportedTagsOnMetrics map[string][]string

type Tag struct {
Key string `yaml:"key"`
Value string `yaml:"value"`
Key string `yaml:"key"`
Value string `yaml:"value"`
ExactMatch bool `yaml:"exactMatch"`
}

type JobLevelMetricFields struct {
Expand Down Expand Up @@ -249,8 +250,10 @@ func (j *Job) validateDiscoveryJob(logger *slog.Logger, jobIdx int) error {
}

for _, st := range j.SearchTags {
if _, err := regexp.Compile(st.Value); err != nil {
return fmt.Errorf("Discovery job [%s/%d]: search tag value for %s has invalid regex value %s: %w", j.Type, jobIdx, st.Key, st.Value, err)
if !st.ExactMatch {
if _, err := regexp.Compile(st.Value); err != nil {
return fmt.Errorf("Discovery job [%s/%d]: search tag value for %s has invalid regex value %s: %w", j.Type, jobIdx, st.Key, st.Value, err)
}
}
}

Expand Down Expand Up @@ -492,12 +495,17 @@ func toModelTags(tags []Tag) []model.Tag {
func toModelSearchTags(tags []Tag) []model.SearchTag {
ret := make([]model.SearchTag, 0, len(tags))
for _, t := range tags {
// This should never panic as long as regex validation continues to happen before model mapping
r := regexp.MustCompile(t.Value)
ret = append(ret, model.SearchTag{
Key: t.Key,
Value: r,
})
st := model.SearchTag{
Key: t.Key,
ExactMatch: t.ExactMatch,
}
if t.ExactMatch {
st.ExactValue = t.Value
} else {
// This should never panic as long as regex validation continues to happen before model mapping
st.Value = regexp.MustCompile(t.Value)
}
ret = append(ret, st)
}
return ret
}
Expand Down
1 change: 1 addition & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func TestConfLoad(t *testing.T) {
{configFile: "sts_region.ok.yml"},
{configFile: "multiple_roles.ok.yml"},
{configFile: "custom_namespace.ok.yml"},
{configFile: "search_tags_exact_match.ok.yml"},
}
for _, tc := range testCases {
config := ScrapeConf{}
Expand Down
18 changes: 18 additions & 0 deletions pkg/config/testdata/search_tags_exact_match.ok.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
apiVersion: v1alpha1
discovery:
jobs:
- type: AWS/EC2
regions:
- us-east-1
searchTags:
- key: Environment
value: production
exactMatch: true
- key: Team
value: "platform-.*"
metrics:
- name: CPUUtilization
statistics:
- Average
period: 300
length: 300
14 changes: 11 additions & 3 deletions pkg/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,10 @@ type Tag struct {
}

type SearchTag struct {
Key string
Value *regexp.Regexp
Key string
Value *regexp.Regexp // Used for regex matching (when ExactMatch is false)
ExactValue string // Used for exact matching (when ExactMatch is true)
ExactMatch bool // When true, use ExactValue for exact string comparison
}

type Dimension struct {
Expand Down Expand Up @@ -239,7 +241,13 @@ func (r TaggedResource) FilterThroughTags(filterTags []SearchTag) bool {
for _, resourceTag := range r.Tags {
for _, filterTag := range filterTags {
if resourceTag.Key == filterTag.Key {
if !filterTag.Value.MatchString(resourceTag.Value) {
var matches bool
if filterTag.ExactMatch {
matches = resourceTag.Value == filterTag.ExactValue
} else {
matches = filterTag.Value.MatchString(resourceTag.Value)
}
if !matches {
return false
}
// A resource needs to match all SearchTags to be returned, so we track the number of tag filter
Expand Down
101 changes: 101 additions & 0 deletions pkg/model/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,107 @@ func Test_FilterThroughTags(t *testing.T) {
},
result: true,
},
{
testName: "exact match - matching value",
resourceTags: []Tag{
{
Key: "Environment",
Value: "production",
},
},
filterTags: []SearchTag{
{
Key: "Environment",
ExactValue: "production",
ExactMatch: true,
},
},
result: true,
},
{
testName: "exact match - non-matching value",
resourceTags: []Tag{
{
Key: "Environment",
Value: "production-us",
},
},
filterTags: []SearchTag{
{
Key: "Environment",
ExactValue: "production",
ExactMatch: true,
},
},
result: false,
},
{
testName: "exact match - value that would match as regex but not exact",
resourceTags: []Tag{
{
Key: "Environment",
Value: "prod-123",
},
},
filterTags: []SearchTag{
{
Key: "Environment",
ExactValue: "prod.*",
ExactMatch: true,
},
},
result: false,
},
{
testName: "mixed exact and regex filters - all match",
resourceTags: []Tag{
{
Key: "Environment",
Value: "production",
},
{
Key: "Team",
Value: "platform-infra",
},
},
filterTags: []SearchTag{
{
Key: "Environment",
ExactValue: "production",
ExactMatch: true,
},
{
Key: "Team",
Value: regexp.MustCompile("platform-.*"),
},
},
result: true,
},
{
testName: "mixed exact and regex filters - exact fails",
resourceTags: []Tag{
{
Key: "Environment",
Value: "staging",
},
{
Key: "Team",
Value: "platform-infra",
},
},
filterTags: []SearchTag{
{
Key: "Environment",
ExactValue: "production",
ExactMatch: true,
},
{
Key: "Team",
Value: regexp.MustCompile("platform-.*"),
},
},
result: false,
},
}

for _, tc := range testCases {
Expand Down