Skip to content

Commit 53e0b02

Browse files
SinghVikram97vbedi
andauthored
Add support for additional tags on vpc lattice resources (#829)
* Add support for additional tags on vpc lattice resources * add documentation for additional tags * add validation for tags --------- Co-authored-by: vbedi <vbedi@amazon.com>
1 parent 0a905f5 commit 53e0b02

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+3551
-55
lines changed

docs/guides/additional-tags.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Additional Tags
2+
3+
The AWS Gateway API Controller automatically applies some tags to resources it creates. In addition, you can use annotations to specify additional tags.
4+
5+
The `application-networking.k8s.aws/tags` annotation specifies additional tags that will be applied to AWS resources created.
6+
7+
## Supported Resources
8+
9+
- **HTTPRoute** - Tags applied to VPC Lattice Services, Listeners, Rules, Target Groups, and Service Network Service Associations
10+
- **ServiceExport** - Tags applied to VPC Lattice Target Groups
11+
- **AccessLogPolicy** - Tags applied to VPC Lattice Access Log Subscriptions
12+
- **VpcAssociationPolicy** - Tags applied to VPC Lattice Service Network VPC Associations
13+
14+
## Usage
15+
16+
Add comma separated key=value pairs to the annotation:
17+
18+
```yaml
19+
apiVersion: gateway.networking.k8s.io/v1
20+
kind: HTTPRoute
21+
metadata:
22+
name: inventory-route
23+
annotations:
24+
application-networking.k8s.aws/tags: "Environment=Production,Team=Backend"
25+
spec:
26+
# ... rest of spec
27+
```
28+
29+
```yaml
30+
apiVersion: application-networking.k8s.aws/v1alpha1
31+
kind: ServiceExport
32+
metadata:
33+
name: payment-service
34+
annotations:
35+
application-networking.k8s.aws/tags: "Environment=Production,Service=Payment"
36+
spec:
37+
# ... rest of spec
38+
```

pkg/aws/cloud.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ type Cloud interface {
4848
// check ownership and acquire if it is not owned by anyone.
4949
TryOwn(ctx context.Context, arn string) (bool, error)
5050
TryOwnFromTags(ctx context.Context, arn string, tags services.Tags) (bool, error)
51+
52+
// MergeTags creates a new tag map by merging baseTags and additionalTags.
53+
// BaseTags will override additionalTags for any duplicate keys.
54+
MergeTags(baseTags services.Tags, additionalTags services.Tags) services.Tags
5155
}
5256

5357
// NewCloud constructs new Cloud implementation.
@@ -144,6 +148,17 @@ func (c *defaultCloud) DefaultTagsMergedWith(tags services.Tags) services.Tags {
144148
return newTags
145149
}
146150

151+
func (c *defaultCloud) MergeTags(baseTags services.Tags, additionalTags services.Tags) services.Tags {
152+
result := make(services.Tags)
153+
if additionalTags != nil {
154+
maps.Copy(result, additionalTags)
155+
}
156+
if baseTags != nil {
157+
maps.Copy(result, baseTags)
158+
}
159+
return result
160+
}
161+
147162
func (c *defaultCloud) getTags(ctx context.Context, arn string) (services.Tags, error) {
148163
tagsReq := &vpclattice.ListTagsForResourceInput{ResourceArn: &arn}
149164
resp, err := c.lattice.ListTagsForResourceWithContext(ctx, tagsReq)

pkg/aws/cloud_mocks.go

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/aws/services/tagging.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66

7+
"github.com/aws/aws-application-networking-k8s/pkg/k8s"
78
"github.com/aws/aws-application-networking-k8s/pkg/utils"
89
"github.com/aws/aws-sdk-go/aws"
910
"github.com/aws/aws-sdk-go/aws/session"
@@ -34,6 +35,9 @@ type Tagging interface {
3435

3536
// Finds one resource that matches the given set of tags.
3637
FindResourcesByTags(ctx context.Context, resourceType ResourceType, tags Tags) ([]string, error)
38+
39+
// Updates tags for a given resource ARN
40+
UpdateTags(ctx context.Context, resourceArn string, newTags Tags) error
3741
}
3842

3943
type defaultTagging struct {
@@ -165,3 +169,72 @@ func convertTagsToFilter(tags Tags) []*taggingapi.TagFilter {
165169
}
166170
return filters
167171
}
172+
173+
func (t *defaultTagging) UpdateTags(ctx context.Context, resourceArn string, newTags Tags) error {
174+
existingTags, err := t.GetTagsForArns(ctx, []string{resourceArn})
175+
if err != nil {
176+
return fmt.Errorf("failed to get existing tags: %w", err)
177+
}
178+
179+
currentTags := k8s.GetNonAWSManagedTags(existingTags[resourceArn])
180+
filteredNewTags := k8s.GetNonAWSManagedTags(newTags)
181+
182+
tagsToAdd, tagsToRemove := k8s.CalculateTagDifference(currentTags, filteredNewTags)
183+
184+
if len(tagsToRemove) > 0 {
185+
_, err := t.UntagResourcesWithContext(ctx, &taggingapi.UntagResourcesInput{
186+
ResourceARNList: []*string{aws.String(resourceArn)},
187+
TagKeys: tagsToRemove,
188+
})
189+
if err != nil {
190+
return fmt.Errorf("failed to remove tags: %w", err)
191+
}
192+
}
193+
194+
if len(tagsToAdd) > 0 {
195+
_, err := t.TagResourcesWithContext(ctx, &taggingapi.TagResourcesInput{
196+
ResourceARNList: []*string{aws.String(resourceArn)},
197+
Tags: tagsToAdd,
198+
})
199+
if err != nil {
200+
return fmt.Errorf("failed to add/update tags: %w", err)
201+
}
202+
}
203+
204+
return nil
205+
}
206+
207+
func (t *latticeTagging) UpdateTags(ctx context.Context, resourceArn string, newTags Tags) error {
208+
existingTags, err := t.ListTagsForResourceWithContext(ctx, &vpclattice.ListTagsForResourceInput{
209+
ResourceArn: aws.String(resourceArn),
210+
})
211+
if err != nil {
212+
return fmt.Errorf("failed to get existing tags: %w", err)
213+
}
214+
215+
currentTags := k8s.GetNonAWSManagedTags(existingTags.Tags)
216+
filteredNewTags := k8s.GetNonAWSManagedTags(newTags)
217+
218+
tagsToAdd, tagsToRemove := k8s.CalculateTagDifference(currentTags, filteredNewTags)
219+
220+
if len(tagsToRemove) > 0 {
221+
_, err := t.UntagResourceWithContext(ctx, &vpclattice.UntagResourceInput{
222+
ResourceArn: aws.String(resourceArn),
223+
TagKeys: tagsToRemove,
224+
})
225+
if err != nil {
226+
return fmt.Errorf("failed to remove tags: %w", err)
227+
}
228+
}
229+
230+
if len(tagsToAdd) > 0 {
231+
_, err := t.TagResourceWithContext(ctx, &vpclattice.TagResourceInput{
232+
ResourceArn: aws.String(resourceArn),
233+
Tags: tagsToAdd,
234+
})
235+
if err != nil {
236+
return fmt.Errorf("failed to add/update tags: %w", err)
237+
}
238+
}
239+
return nil
240+
}

pkg/aws/services/tagging_mocks.go

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/aws/services/tagging_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,3 +429,126 @@ func Test_latticeTagging_FindResourcesByTags(t *testing.T) {
429429
})
430430
}
431431
}
432+
433+
func TestLatticeTagging_UpdateTags(t *testing.T) {
434+
ctx := context.TODO()
435+
tests := []struct {
436+
name string
437+
resourceArn string
438+
existingTags Tags
439+
newTags Tags
440+
expectedTagCalls int
441+
expectedUntagCalls int
442+
expectError bool
443+
description string
444+
}{
445+
{
446+
name: "nil new tags removes all existing additional tags",
447+
resourceArn: "arn:aws:vpc-lattice:us-west-2:123456789:service/svc-123",
448+
existingTags: Tags{
449+
"Environment": aws.String("Dev"),
450+
"Project": aws.String("MyApp"),
451+
"application-networking.k8s.aws/ManagedBy": aws.String("123456789/cluster/vpc-123"),
452+
},
453+
newTags: nil,
454+
expectedTagCalls: 0,
455+
expectedUntagCalls: 1,
456+
expectError: false,
457+
description: "should remove all additional tags when newTags is nil",
458+
},
459+
{
460+
name: "add new tags when no existing additional tags",
461+
resourceArn: "arn:aws:vpc-lattice:us-west-2:123456789:service/svc-123",
462+
existingTags: Tags{
463+
"application-networking.k8s.aws/ManagedBy": aws.String("123456789/cluster/vpc-123"),
464+
},
465+
newTags: Tags{
466+
"Environment": aws.String("Dev"),
467+
"Project": aws.String("MyApp"),
468+
},
469+
expectedTagCalls: 1,
470+
expectedUntagCalls: 0,
471+
expectError: false,
472+
description: "should add new tags when no existing additional tags",
473+
},
474+
{
475+
name: "update existing additional tags",
476+
resourceArn: "arn:aws:vpc-lattice:us-west-2:123456789:service/svc-123",
477+
existingTags: Tags{
478+
"Environment": aws.String("Dev"),
479+
"Project": aws.String("OldApp"),
480+
"application-networking.k8s.aws/ManagedBy": aws.String("123456789/cluster/vpc-123"),
481+
},
482+
newTags: Tags{
483+
"Environment": aws.String("Prod"),
484+
"Project": aws.String("NewApp"),
485+
},
486+
expectedTagCalls: 1,
487+
expectedUntagCalls: 0,
488+
expectError: false,
489+
description: "should update changed additional tag values",
490+
},
491+
{
492+
name: "no changes needed",
493+
resourceArn: "arn:aws:vpc-lattice:us-west-2:123456789:service/svc-123",
494+
existingTags: Tags{
495+
"Environment": aws.String("Dev"),
496+
"Project": aws.String("MyApp"),
497+
"application-networking.k8s.aws/ManagedBy": aws.String("123456789/cluster/vpc-123"),
498+
},
499+
newTags: Tags{
500+
"Environment": aws.String("Dev"),
501+
"Project": aws.String("MyApp"),
502+
},
503+
expectedTagCalls: 0,
504+
expectedUntagCalls: 0,
505+
expectError: false,
506+
description: "should not make API calls when no changes needed",
507+
},
508+
{
509+
name: "filters out AWS managed tags from new tags",
510+
resourceArn: "arn:aws:vpc-lattice:us-west-2:123456789:service/svc-123",
511+
existingTags: Tags{},
512+
newTags: Tags{
513+
"application-networking.k8s.aws/ManagedBy": aws.String("test-override"),
514+
"application-networking.k8s.aws/RouteType": aws.String("http"),
515+
},
516+
expectedTagCalls: 0,
517+
expectedUntagCalls: 0,
518+
expectError: false,
519+
description: "should filter out AWS managed tags from new tags, resulting in no API calls",
520+
},
521+
}
522+
523+
for _, tt := range tests {
524+
t.Run(tt.name, func(t *testing.T) {
525+
c := gomock.NewController(t)
526+
mockLattice := NewMockLattice(c)
527+
528+
lt := &latticeTagging{
529+
Lattice: mockLattice,
530+
}
531+
532+
mockLattice.EXPECT().ListTagsForResourceWithContext(ctx, gomock.Any()).
533+
Return(&vpclattice.ListTagsForResourceOutput{Tags: tt.existingTags}, nil).Times(1)
534+
535+
if tt.expectedUntagCalls > 0 {
536+
mockLattice.EXPECT().UntagResourceWithContext(ctx, gomock.Any()).
537+
Return(nil, nil).Times(tt.expectedUntagCalls)
538+
}
539+
540+
if tt.expectedTagCalls > 0 {
541+
mockLattice.EXPECT().TagResourceWithContext(ctx, gomock.Any()).
542+
Return(nil, nil).Times(tt.expectedTagCalls)
543+
}
544+
545+
err := lt.UpdateTags(ctx, tt.resourceArn, tt.newTags)
546+
547+
if tt.expectError {
548+
assert.Error(t, err, tt.description)
549+
} else {
550+
assert.NoError(t, err, tt.description)
551+
}
552+
})
553+
}
554+
}

pkg/controllers/accesslogpolicy_controller.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
"github.com/aws/aws-application-networking-k8s/pkg/aws"
4343
"github.com/aws/aws-application-networking-k8s/pkg/aws/services"
4444
"github.com/aws/aws-application-networking-k8s/pkg/config"
45+
"github.com/aws/aws-application-networking-k8s/pkg/controllers/predicates"
4546
"github.com/aws/aws-application-networking-k8s/pkg/deploy"
4647
"github.com/aws/aws-application-networking-k8s/pkg/gateway"
4748
"github.com/aws/aws-application-networking-k8s/pkg/k8s"
@@ -95,7 +96,7 @@ func RegisterAccessLogPolicyController(
9596
}
9697

9798
builder := ctrl.NewControllerManagedBy(mgr).
98-
For(&anv1alpha1.AccessLogPolicy{}, pkg_builder.WithPredicates(predicate.GenerationChangedPredicate{})).
99+
For(&anv1alpha1.AccessLogPolicy{}, pkg_builder.WithPredicates(predicate.Or(predicate.GenerationChangedPredicate{}, predicates.AdditionalTagsAnnotationChangedPredicate))).
99100
Watches(&gwv1.Gateway{}, handler.EnqueueRequestsFromMapFunc(r.findImpactedAccessLogPolicies), pkg_builder.WithPredicates(predicate.GenerationChangedPredicate{})).
100101
Watches(&gwv1.HTTPRoute{}, handler.EnqueueRequestsFromMapFunc(r.findImpactedAccessLogPolicies), pkg_builder.WithPredicates(predicate.GenerationChangedPredicate{})).
101102
Watches(&gwv1.GRPCRoute{}, handler.EnqueueRequestsFromMapFunc(r.findImpactedAccessLogPolicies), pkg_builder.WithPredicates(predicate.GenerationChangedPredicate{})).
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package predicates
2+
3+
import (
4+
"sigs.k8s.io/controller-runtime/pkg/event"
5+
"sigs.k8s.io/controller-runtime/pkg/predicate"
6+
7+
"github.com/aws/aws-application-networking-k8s/pkg/k8s"
8+
)
9+
10+
var AdditionalTagsAnnotationChangedPredicate = predicate.Funcs{
11+
UpdateFunc: func(e event.UpdateEvent) bool {
12+
oldAnnotations := e.ObjectOld.GetAnnotations()
13+
newAnnotations := e.ObjectNew.GetAnnotations()
14+
15+
oldAdditionalTags := getAdditionalTagsAnnotation(oldAnnotations)
16+
newAdditionalTags := getAdditionalTagsAnnotation(newAnnotations)
17+
18+
return oldAdditionalTags != newAdditionalTags
19+
},
20+
CreateFunc: func(e event.CreateEvent) bool {
21+
annotations := e.Object.GetAnnotations()
22+
return getAdditionalTagsAnnotation(annotations) != ""
23+
},
24+
}
25+
26+
func getAdditionalTagsAnnotation(annotations map[string]string) string {
27+
if annotations == nil {
28+
return ""
29+
}
30+
return annotations[k8s.TagsAnnotationKey]
31+
}

0 commit comments

Comments
 (0)