Skip to content

Commit 6e0d020

Browse files
SinghVikram97vbedirlymbur
authored
Add allow-takeover-from annotation to enable automated VPC Lattice service takeover between clusters (#841)
* Add allow-takeover-from annotation to enable automated VPC Lattice service takeover between clusters * address review comments --------- Co-authored-by: vbedi <[email protected]> Co-authored-by: Ryan Lymburner <[email protected]>
1 parent e80a74f commit 6e0d020

25 files changed

+1098
-70
lines changed

docs/guides/advanced-configurations.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,42 @@ spec:
132132
port: 80
133133
targetPort: 8090
134134
```
135+
136+
### Blue/Green Multi-Cluster Migration with Service Takeover
137+
138+
For blue/green cluster migrations, the controller supports automated takeover of VPC Lattice services using the `application-networking.k8s.aws/allow-takeover-from` annotation. The annotation value must match the value of the `application-networking.k8s.aws/ManagedBy` tag on the VPC Lattice Service, which has the format `{AWS_ACCOUNT_ID}/{CLUSTER_NAME}/{VPC_ID}` (e.g., "123456789012/blue-cluster/vpc-0abc123def456789"). This eliminates the need for manual ManagedBy tag changes during cluster migrations.
139+
140+
#### Migration Workflow
141+
142+
1. Blue cluster creates HTTPRoute
143+
2. Blue cluster exports service using ServiceExport (creates standalone target group for cross-cluster access)
144+
3. Green cluster imports blue service using ServiceImport (references the exported target group from blue cluster)
145+
4. Green cluster creates HTTPRoute with takeover annotation to claim the existing VPC Lattice service:
146+
147+
```yaml
148+
apiVersion: gateway.networking.k8s.io/v1
149+
kind: HTTPRoute
150+
metadata:
151+
name: inventory-service
152+
annotations:
153+
application-networking.k8s.aws/allow-takeover-from: "123456789012/blue-cluster/vpc-0abc123def456789"
154+
spec:
155+
parentRefs:
156+
- name: my-gateway
157+
rules:
158+
- matches:
159+
- path:
160+
type: PathPrefix
161+
value: /inventory
162+
backendRefs:
163+
- name: inventory-ver1
164+
kind: ServiceImport
165+
weight: 90
166+
- name: inventory-ver2
167+
kind: Service
168+
port: 80
169+
weight: 10
170+
```
171+
172+
5. Controller takes over the VPC Lattice service and updates it to reflect traffic weights in green HTTPRoute
173+
6. Controller updates ManagedBy tag on service, service network service association, listeners, and rules to transfer ownership

pkg/aws/cloud.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ type Cloud interface {
5252
// MergeTags creates a new tag map by merging baseTags and additionalTags.
5353
// BaseTags will override additionalTags for any duplicate keys.
5454
MergeTags(baseTags services.Tags, additionalTags services.Tags) services.Tags
55+
56+
// GetManagedByFromTags extracts the ManagedBy tag value from a tags map
57+
GetManagedByFromTags(tags services.Tags) string
5558
}
5659

5760
// NewCloud constructs new Cloud implementation.
@@ -168,7 +171,7 @@ func (c *defaultCloud) getTags(ctx context.Context, arn string) (services.Tags,
168171
return resp.Tags, nil
169172
}
170173

171-
func (c *defaultCloud) getManagedByFromTags(tags services.Tags) string {
174+
func (c *defaultCloud) GetManagedByFromTags(tags services.Tags) string {
172175
tag, ok := tags[TagManagedBy]
173176
if !ok || tag == nil {
174177
return ""
@@ -181,7 +184,7 @@ func (c *defaultCloud) IsArnManaged(ctx context.Context, arn string) (bool, erro
181184
if err != nil {
182185
return false, err
183186
}
184-
return c.isOwner(c.getManagedByFromTags(tags)), nil
187+
return c.isOwner(c.GetManagedByFromTags(tags)), nil
185188
}
186189

187190
func (c *defaultCloud) TryOwn(ctx context.Context, arn string) (bool, error) {
@@ -195,7 +198,7 @@ func (c *defaultCloud) TryOwn(ctx context.Context, arn string) (bool, error) {
195198

196199
func (c *defaultCloud) TryOwnFromTags(ctx context.Context, arn string, tags services.Tags) (bool, error) {
197200
// For resources that need backwards compatibility - not having managedBy is considered as owned by controller.
198-
managedBy := c.getManagedByFromTags(tags)
201+
managedBy := c.GetManagedByFromTags(tags)
199202
if managedBy == "" {
200203
err := c.ownResource(ctx, arn)
201204
if err != nil {

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: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ type Tagging interface {
3737
FindResourcesByTags(ctx context.Context, resourceType ResourceType, tags Tags) ([]string, error)
3838

3939
// Updates tags for a given resource ARN
40-
UpdateTags(ctx context.Context, resourceArn string, newTags Tags) error
40+
UpdateTags(ctx context.Context, resourceArn string, additionalTags Tags, awsManagedTags Tags) error
4141
}
4242

4343
type defaultTagging struct {
@@ -170,14 +170,21 @@ func convertTagsToFilter(tags Tags) []*taggingapi.TagFilter {
170170
return filters
171171
}
172172

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

179179
currentTags := k8s.GetNonAWSManagedTags(existingTags[resourceArn])
180-
filteredNewTags := k8s.GetNonAWSManagedTags(newTags)
180+
filteredNewTags := k8s.GetNonAWSManagedTags(additionalTags)
181+
182+
for key, value := range awsManagedTags {
183+
if existingValue, exists := existingTags[resourceArn][key]; exists {
184+
currentTags[key] = existingValue
185+
}
186+
filteredNewTags[key] = value
187+
}
181188

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

@@ -204,7 +211,7 @@ func (t *defaultTagging) UpdateTags(ctx context.Context, resourceArn string, new
204211
return nil
205212
}
206213

207-
func (t *latticeTagging) UpdateTags(ctx context.Context, resourceArn string, newTags Tags) error {
214+
func (t *latticeTagging) UpdateTags(ctx context.Context, resourceArn string, additionalTags Tags, awsManagedTags Tags) error {
208215
existingTags, err := t.ListTagsForResourceWithContext(ctx, &vpclattice.ListTagsForResourceInput{
209216
ResourceArn: aws.String(resourceArn),
210217
})
@@ -213,7 +220,14 @@ func (t *latticeTagging) UpdateTags(ctx context.Context, resourceArn string, new
213220
}
214221

215222
currentTags := k8s.GetNonAWSManagedTags(existingTags.Tags)
216-
filteredNewTags := k8s.GetNonAWSManagedTags(newTags)
223+
filteredNewTags := k8s.GetNonAWSManagedTags(additionalTags)
224+
225+
for key, value := range awsManagedTags {
226+
if existingValue, exists := existingTags.Tags[key]; exists {
227+
currentTags[key] = existingValue
228+
}
229+
filteredNewTags[key] = value
230+
}
217231

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

pkg/aws/services/tagging_mocks.go

Lines changed: 4 additions & 4 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: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -436,87 +436,118 @@ func TestLatticeTagging_UpdateTags(t *testing.T) {
436436
name string
437437
resourceArn string
438438
existingTags Tags
439-
newTags Tags
439+
additionalTags Tags
440+
awsManagedTags Tags
440441
expectedTagCalls int
441442
expectedUntagCalls int
442443
expectError bool
443444
description string
444445
}{
445446
{
446-
name: "nil new tags removes all existing additional tags",
447+
name: "nil additional tags removes all existing additional tags",
447448
resourceArn: "arn:aws:vpc-lattice:us-west-2:123456789:service/svc-123",
448449
existingTags: Tags{
449450
"Environment": aws.String("Dev"),
450451
"Project": aws.String("MyApp"),
451452
"application-networking.k8s.aws/ManagedBy": aws.String("123456789/cluster/vpc-123"),
452453
},
453-
newTags: nil,
454+
additionalTags: nil,
455+
awsManagedTags: nil,
454456
expectedTagCalls: 0,
455457
expectedUntagCalls: 1,
456458
expectError: false,
457-
description: "should remove all additional tags when newTags is nil",
459+
description: "should remove all additional tags when additionalTags is nil",
458460
},
459461
{
460-
name: "add new tags when no existing additional tags",
462+
name: "add new additional tags when no existing additional tags",
461463
resourceArn: "arn:aws:vpc-lattice:us-west-2:123456789:service/svc-123",
462464
existingTags: Tags{
463465
"application-networking.k8s.aws/ManagedBy": aws.String("123456789/cluster/vpc-123"),
464466
},
465-
newTags: Tags{
467+
additionalTags: Tags{
466468
"Environment": aws.String("Dev"),
467469
"Project": aws.String("MyApp"),
468470
},
471+
awsManagedTags: nil,
469472
expectedTagCalls: 1,
470473
expectedUntagCalls: 0,
471474
expectError: false,
472-
description: "should add new tags when no existing additional tags",
475+
description: "should add new additional tags when no existing additional tags",
473476
},
474477
{
475-
name: "update existing additional tags",
478+
name: "update AWS managed tags only",
479+
resourceArn: "arn:aws:vpc-lattice:us-west-2:123456789:service/svc-123",
480+
existingTags: Tags{
481+
"Environment": aws.String("Dev"),
482+
"application-networking.k8s.aws/ManagedBy": aws.String("old-cluster/old-vpc"),
483+
},
484+
additionalTags: Tags{
485+
"Environment": aws.String("Dev"),
486+
},
487+
awsManagedTags: Tags{
488+
"application-networking.k8s.aws/ManagedBy": aws.String("new-cluster/new-vpc"),
489+
},
490+
expectedTagCalls: 1,
491+
expectedUntagCalls: 0,
492+
expectError: false,
493+
description: "should update AWS managed tags when provided",
494+
},
495+
{
496+
name: "update both additional and AWS managed tags",
476497
resourceArn: "arn:aws:vpc-lattice:us-west-2:123456789:service/svc-123",
477498
existingTags: Tags{
478499
"Environment": aws.String("Dev"),
479500
"Project": aws.String("OldApp"),
480-
"application-networking.k8s.aws/ManagedBy": aws.String("123456789/cluster/vpc-123"),
501+
"application-networking.k8s.aws/ManagedBy": aws.String("old-cluster/old-vpc"),
481502
},
482-
newTags: Tags{
503+
additionalTags: Tags{
483504
"Environment": aws.String("Prod"),
484505
"Project": aws.String("NewApp"),
485506
},
507+
awsManagedTags: Tags{
508+
"application-networking.k8s.aws/ManagedBy": aws.String("new-cluster/new-vpc"),
509+
},
486510
expectedTagCalls: 1,
487511
expectedUntagCalls: 0,
488512
expectError: false,
489-
description: "should update changed additional tag values",
513+
description: "should update both additional and AWS managed tags",
490514
},
491515
{
492-
name: "no changes needed",
516+
name: "no changes needed with AWS managed tags",
493517
resourceArn: "arn:aws:vpc-lattice:us-west-2:123456789:service/svc-123",
494518
existingTags: Tags{
495519
"Environment": aws.String("Dev"),
496520
"Project": aws.String("MyApp"),
497521
"application-networking.k8s.aws/ManagedBy": aws.String("123456789/cluster/vpc-123"),
498522
},
499-
newTags: Tags{
523+
additionalTags: Tags{
500524
"Environment": aws.String("Dev"),
501525
"Project": aws.String("MyApp"),
502526
},
527+
awsManagedTags: Tags{
528+
"application-networking.k8s.aws/ManagedBy": aws.String("123456789/cluster/vpc-123"),
529+
},
503530
expectedTagCalls: 0,
504531
expectedUntagCalls: 0,
505532
expectError: false,
506533
description: "should not make API calls when no changes needed",
507534
},
508535
{
509-
name: "filters out AWS managed tags from new tags",
536+
name: "filters out AWS managed tags from additional tags",
510537
resourceArn: "arn:aws:vpc-lattice:us-west-2:123456789:service/svc-123",
511538
existingTags: Tags{},
512-
newTags: Tags{
539+
additionalTags: Tags{
513540
"application-networking.k8s.aws/ManagedBy": aws.String("test-override"),
514541
"application-networking.k8s.aws/RouteType": aws.String("http"),
542+
"Environment": aws.String("Dev"),
515543
},
516-
expectedTagCalls: 0,
544+
awsManagedTags: Tags{
545+
"application-networking.k8s.aws/ManagedBy": aws.String("correct-value"),
546+
},
547+
expectedTagCalls: 1,
517548
expectedUntagCalls: 0,
518549
expectError: false,
519-
description: "should filter out AWS managed tags from new tags, resulting in no API calls",
550+
description: "should filter out AWS managed tags from additional tags but include them from awsManagedTags",
520551
},
521552
}
522553

@@ -542,7 +573,7 @@ func TestLatticeTagging_UpdateTags(t *testing.T) {
542573
Return(nil, nil).Times(tt.expectedTagCalls)
543574
}
544575

545-
err := lt.UpdateTags(ctx, tt.resourceArn, tt.newTags)
576+
err := lt.UpdateTags(ctx, tt.resourceArn, tt.additionalTags, tt.awsManagedTags)
546577

547578
if tt.expectError {
548579
assert.Error(t, err, tt.description)
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 AllowTakeoverFromAnnotationChangedPredicate = predicate.Funcs{
11+
UpdateFunc: func(e event.UpdateEvent) bool {
12+
oldAnnotations := e.ObjectOld.GetAnnotations()
13+
newAnnotations := e.ObjectNew.GetAnnotations()
14+
15+
oldAllowTakeoverFromAnnotation := getAllowTakeoverFromAnnotation(oldAnnotations)
16+
newAllowTakeoverFromAnnotation := getAllowTakeoverFromAnnotation(newAnnotations)
17+
18+
return oldAllowTakeoverFromAnnotation != newAllowTakeoverFromAnnotation
19+
},
20+
CreateFunc: func(e event.CreateEvent) bool {
21+
annotations := e.Object.GetAnnotations()
22+
return getAllowTakeoverFromAnnotation(annotations) != ""
23+
},
24+
}
25+
26+
func getAllowTakeoverFromAnnotation(annotations map[string]string) string {
27+
if annotations == nil {
28+
return ""
29+
}
30+
return annotations[k8s.AllowTakeoverFromAnnotation]
31+
}

0 commit comments

Comments
 (0)