Skip to content
Merged
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
38 changes: 38 additions & 0 deletions docs/guides/additional-tags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Additional Tags

The AWS Gateway API Controller automatically applies some tags to resources it creates. In addition, you can use annotations to specify additional tags.

The `application-networking.k8s.aws/tags` annotation specifies additional tags that will be applied to AWS resources created.

## Supported Resources

- **HTTPRoute** - Tags applied to VPC Lattice Services, Listeners, Rules, Target Groups, and Service Network Service Associations
- **ServiceExport** - Tags applied to VPC Lattice Target Groups
- **AccessLogPolicy** - Tags applied to VPC Lattice Access Log Subscriptions
- **VpcAssociationPolicy** - Tags applied to VPC Lattice Service Network VPC Associations

## Usage

Add comma separated key=value pairs to the annotation:

```yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: inventory-route
annotations:
application-networking.k8s.aws/tags: "Environment=Production,Team=Backend"
spec:
# ... rest of spec
```

```yaml
apiVersion: application-networking.k8s.aws/v1alpha1
kind: ServiceExport
metadata:
name: payment-service
annotations:
application-networking.k8s.aws/tags: "Environment=Production,Service=Payment"
spec:
# ... rest of spec
```
15 changes: 15 additions & 0 deletions pkg/aws/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ type Cloud interface {
// check ownership and acquire if it is not owned by anyone.
TryOwn(ctx context.Context, arn string) (bool, error)
TryOwnFromTags(ctx context.Context, arn string, tags services.Tags) (bool, error)

// MergeTags creates a new tag map by merging baseTags and additionalTags.
// BaseTags will override additionalTags for any duplicate keys.
MergeTags(baseTags services.Tags, additionalTags services.Tags) services.Tags
}

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

func (c *defaultCloud) MergeTags(baseTags services.Tags, additionalTags services.Tags) services.Tags {
result := make(services.Tags)
if additionalTags != nil {
maps.Copy(result, additionalTags)
}
if baseTags != nil {
maps.Copy(result, baseTags)
}
return result
}

func (c *defaultCloud) getTags(ctx context.Context, arn string) (services.Tags, error) {
tagsReq := &vpclattice.ListTagsForResourceInput{ResourceArn: &arn}
resp, err := c.lattice.ListTagsForResourceWithContext(ctx, tagsReq)
Expand Down
14 changes: 14 additions & 0 deletions pkg/aws/cloud_mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

73 changes: 73 additions & 0 deletions pkg/aws/services/tagging.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"

"github.com/aws/aws-application-networking-k8s/pkg/k8s"
"github.com/aws/aws-application-networking-k8s/pkg/utils"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
Expand Down Expand Up @@ -34,6 +35,9 @@ type Tagging interface {

// Finds one resource that matches the given set of tags.
FindResourcesByTags(ctx context.Context, resourceType ResourceType, tags Tags) ([]string, error)

// Updates tags for a given resource ARN
UpdateTags(ctx context.Context, resourceArn string, newTags Tags) error
}

type defaultTagging struct {
Expand Down Expand Up @@ -165,3 +169,72 @@ func convertTagsToFilter(tags Tags) []*taggingapi.TagFilter {
}
return filters
}

func (t *defaultTagging) UpdateTags(ctx context.Context, resourceArn string, newTags Tags) error {
existingTags, err := t.GetTagsForArns(ctx, []string{resourceArn})
if err != nil {
return fmt.Errorf("failed to get existing tags: %w", err)
}

currentTags := k8s.GetNonAWSManagedTags(existingTags[resourceArn])
filteredNewTags := k8s.GetNonAWSManagedTags(newTags)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be useful to do some pre-validation if we can based on general AWS tagging guidelines

Copy link
Contributor Author

@SinghVikram97 SinghVikram97 Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added validation for:

  1. Key length (128 characters)
  2. Key can't start with prefix aws:
  3. Value length (256 characters)
  4. Maximum number of tags (50)
  5. Tag pattern: ^([\p{L}\p{Z}\p{N}_.:\/=+\-@]*)$

Taken from: https://docs.aws.amazon.com/resourcegroupstagging/latest/APIReference/API_Tag.html
Changes: 34889a4


tagsToAdd, tagsToRemove := k8s.CalculateTagDifference(currentTags, filteredNewTags)

if len(tagsToRemove) > 0 {
_, err := t.UntagResourcesWithContext(ctx, &taggingapi.UntagResourcesInput{
ResourceARNList: []*string{aws.String(resourceArn)},
TagKeys: tagsToRemove,
})
if err != nil {
return fmt.Errorf("failed to remove tags: %w", err)
}
}

if len(tagsToAdd) > 0 {
_, err := t.TagResourcesWithContext(ctx, &taggingapi.TagResourcesInput{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do the recent exponential throttling improvements apply here as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incase of an error, it would retry with an exponential backoff.

ResourceARNList: []*string{aws.String(resourceArn)},
Tags: tagsToAdd,
})
if err != nil {
return fmt.Errorf("failed to add/update tags: %w", err)
}
}

return nil
}

func (t *latticeTagging) UpdateTags(ctx context.Context, resourceArn string, newTags Tags) error {
existingTags, err := t.ListTagsForResourceWithContext(ctx, &vpclattice.ListTagsForResourceInput{
ResourceArn: aws.String(resourceArn),
})
if err != nil {
return fmt.Errorf("failed to get existing tags: %w", err)
}

currentTags := k8s.GetNonAWSManagedTags(existingTags.Tags)
filteredNewTags := k8s.GetNonAWSManagedTags(newTags)

tagsToAdd, tagsToRemove := k8s.CalculateTagDifference(currentTags, filteredNewTags)

if len(tagsToRemove) > 0 {
_, err := t.UntagResourceWithContext(ctx, &vpclattice.UntagResourceInput{
ResourceArn: aws.String(resourceArn),
TagKeys: tagsToRemove,
})
if err != nil {
return fmt.Errorf("failed to remove tags: %w", err)
}
}

if len(tagsToAdd) > 0 {
_, err := t.TagResourceWithContext(ctx, &vpclattice.TagResourceInput{
ResourceArn: aws.String(resourceArn),
Tags: tagsToAdd,
})
if err != nil {
return fmt.Errorf("failed to add/update tags: %w", err)
}
}
return nil
}
14 changes: 14 additions & 0 deletions pkg/aws/services/tagging_mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

123 changes: 123 additions & 0 deletions pkg/aws/services/tagging_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -429,3 +429,126 @@ func Test_latticeTagging_FindResourcesByTags(t *testing.T) {
})
}
}

func TestLatticeTagging_UpdateTags(t *testing.T) {
ctx := context.TODO()
tests := []struct {
name string
resourceArn string
existingTags Tags
newTags Tags
expectedTagCalls int
expectedUntagCalls int
expectError bool
description string
}{
{
name: "nil new tags removes all existing additional tags",
resourceArn: "arn:aws:vpc-lattice:us-west-2:123456789:service/svc-123",
existingTags: Tags{
"Environment": aws.String("Dev"),
"Project": aws.String("MyApp"),
"application-networking.k8s.aws/ManagedBy": aws.String("123456789/cluster/vpc-123"),
},
newTags: nil,
expectedTagCalls: 0,
expectedUntagCalls: 1,
expectError: false,
description: "should remove all additional tags when newTags is nil",
},
{
name: "add new tags when no existing additional tags",
resourceArn: "arn:aws:vpc-lattice:us-west-2:123456789:service/svc-123",
existingTags: Tags{
"application-networking.k8s.aws/ManagedBy": aws.String("123456789/cluster/vpc-123"),
},
newTags: Tags{
"Environment": aws.String("Dev"),
"Project": aws.String("MyApp"),
},
expectedTagCalls: 1,
expectedUntagCalls: 0,
expectError: false,
description: "should add new tags when no existing additional tags",
},
{
name: "update existing additional tags",
resourceArn: "arn:aws:vpc-lattice:us-west-2:123456789:service/svc-123",
existingTags: Tags{
"Environment": aws.String("Dev"),
"Project": aws.String("OldApp"),
"application-networking.k8s.aws/ManagedBy": aws.String("123456789/cluster/vpc-123"),
},
newTags: Tags{
"Environment": aws.String("Prod"),
"Project": aws.String("NewApp"),
},
expectedTagCalls: 1,
expectedUntagCalls: 0,
expectError: false,
description: "should update changed additional tag values",
},
{
name: "no changes needed",
resourceArn: "arn:aws:vpc-lattice:us-west-2:123456789:service/svc-123",
existingTags: Tags{
"Environment": aws.String("Dev"),
"Project": aws.String("MyApp"),
"application-networking.k8s.aws/ManagedBy": aws.String("123456789/cluster/vpc-123"),
},
newTags: Tags{
"Environment": aws.String("Dev"),
"Project": aws.String("MyApp"),
},
expectedTagCalls: 0,
expectedUntagCalls: 0,
expectError: false,
description: "should not make API calls when no changes needed",
},
{
name: "filters out AWS managed tags from new tags",
resourceArn: "arn:aws:vpc-lattice:us-west-2:123456789:service/svc-123",
existingTags: Tags{},
newTags: Tags{
"application-networking.k8s.aws/ManagedBy": aws.String("test-override"),
"application-networking.k8s.aws/RouteType": aws.String("http"),
},
expectedTagCalls: 0,
expectedUntagCalls: 0,
expectError: false,
description: "should filter out AWS managed tags from new tags, resulting in no API calls",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := gomock.NewController(t)
mockLattice := NewMockLattice(c)

lt := &latticeTagging{
Lattice: mockLattice,
}

mockLattice.EXPECT().ListTagsForResourceWithContext(ctx, gomock.Any()).
Return(&vpclattice.ListTagsForResourceOutput{Tags: tt.existingTags}, nil).Times(1)

if tt.expectedUntagCalls > 0 {
mockLattice.EXPECT().UntagResourceWithContext(ctx, gomock.Any()).
Return(nil, nil).Times(tt.expectedUntagCalls)
}

if tt.expectedTagCalls > 0 {
mockLattice.EXPECT().TagResourceWithContext(ctx, gomock.Any()).
Return(nil, nil).Times(tt.expectedTagCalls)
}

err := lt.UpdateTags(ctx, tt.resourceArn, tt.newTags)

if tt.expectError {
assert.Error(t, err, tt.description)
} else {
assert.NoError(t, err, tt.description)
}
})
}
}
3 changes: 2 additions & 1 deletion pkg/controllers/accesslogpolicy_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import (
"github.com/aws/aws-application-networking-k8s/pkg/aws"
"github.com/aws/aws-application-networking-k8s/pkg/aws/services"
"github.com/aws/aws-application-networking-k8s/pkg/config"
"github.com/aws/aws-application-networking-k8s/pkg/controllers/predicates"
"github.com/aws/aws-application-networking-k8s/pkg/deploy"
"github.com/aws/aws-application-networking-k8s/pkg/gateway"
"github.com/aws/aws-application-networking-k8s/pkg/k8s"
Expand Down Expand Up @@ -95,7 +96,7 @@ func RegisterAccessLogPolicyController(
}

builder := ctrl.NewControllerManagedBy(mgr).
For(&anv1alpha1.AccessLogPolicy{}, pkg_builder.WithPredicates(predicate.GenerationChangedPredicate{})).
For(&anv1alpha1.AccessLogPolicy{}, pkg_builder.WithPredicates(predicate.Or(predicate.GenerationChangedPredicate{}, predicates.AdditionalTagsAnnotationChangedPredicate))).
Watches(&gwv1.Gateway{}, handler.EnqueueRequestsFromMapFunc(r.findImpactedAccessLogPolicies), pkg_builder.WithPredicates(predicate.GenerationChangedPredicate{})).
Watches(&gwv1.HTTPRoute{}, handler.EnqueueRequestsFromMapFunc(r.findImpactedAccessLogPolicies), pkg_builder.WithPredicates(predicate.GenerationChangedPredicate{})).
Watches(&gwv1.GRPCRoute{}, handler.EnqueueRequestsFromMapFunc(r.findImpactedAccessLogPolicies), pkg_builder.WithPredicates(predicate.GenerationChangedPredicate{})).
Expand Down
31 changes: 31 additions & 0 deletions pkg/controllers/predicates/additionaltags_predicate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package predicates

import (
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"

"github.com/aws/aws-application-networking-k8s/pkg/k8s"
)

var AdditionalTagsAnnotationChangedPredicate = predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
oldAnnotations := e.ObjectOld.GetAnnotations()
newAnnotations := e.ObjectNew.GetAnnotations()

oldAdditionalTags := getAdditionalTagsAnnotation(oldAnnotations)
newAdditionalTags := getAdditionalTagsAnnotation(newAnnotations)

return oldAdditionalTags != newAdditionalTags
},
CreateFunc: func(e event.CreateEvent) bool {
annotations := e.Object.GetAnnotations()
return getAdditionalTagsAnnotation(annotations) != ""
},
}

func getAdditionalTagsAnnotation(annotations map[string]string) string {
if annotations == nil {
return ""
}
return annotations[k8s.TagsAnnotationKey]
}
Loading
Loading