Skip to content

Commit e137f15

Browse files
committed
MGDAPI-5690 CRO Redis snapshots tagging logics update
1 parent 5450c62 commit e137f15

File tree

7 files changed

+243
-10
lines changed

7 files changed

+243
-10
lines changed

pkg/providers/aws/aws_interfaces.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ type ElastiCacheAPI interface {
7474
CreateReplicationGroup(ctx context.Context, input *elasticache.CreateReplicationGroupInput, optFns ...func(*elasticache.Options)) (*elasticache.CreateReplicationGroupOutput, error)
7575
CreateCacheSubnetGroup(ctx context.Context, input *elasticache.CreateCacheSubnetGroupInput, optFns ...func(*elasticache.Options)) (*elasticache.CreateCacheSubnetGroupOutput, error)
7676
DeleteReplicationGroup(ctx context.Context, input *elasticache.DeleteReplicationGroupInput, optFns ...func(*elasticache.Options)) (*elasticache.DeleteReplicationGroupOutput, error)
77+
ListTagsForResource(ctx context.Context, input *elasticache.ListTagsForResourceInput, optFns ...func(*elasticache.Options)) (*elasticache.ListTagsForResourceOutput, error)
7778
}
7879

7980
type S3API interface {

pkg/providers/aws/aws_mock.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,11 @@ func (m *mock_ElasticacheClient) CreateReplicationGroup(ctx context.Context, inp
465465
return args.Get(0).(*elasticache.CreateReplicationGroupOutput), args.Error(1)
466466
}
467467

468+
func (m *mock_ElasticacheClient) ListTagsForResource(ctx context.Context, input *elasticache.ListTagsForResourceInput, optFns ...func(*elasticache.Options)) (*elasticache.ListTagsForResourceOutput, error) {
469+
args := m.Called(ctx, input, optFns)
470+
return args.Get(0).(*elasticache.ListTagsForResourceOutput), args.Error(1)
471+
}
472+
468473
// S3 Mock
469474
type mock_S3Client struct {
470475
mock.Mock

pkg/providers/aws/aws_wrapper.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,10 @@ func (r *RealElasticacheClient) CreateReplicationGroup(ctx context.Context, inpu
274274
return r.Client.CreateReplicationGroup(ctx, input, optFns...)
275275
}
276276

277+
func (r *RealElasticacheClient) ListTagsForResource(ctx context.Context, input *elasticache.ListTagsForResourceInput, optFns ...func(*elasticache.Options)) (*elasticache.ListTagsForResourceOutput, error) {
278+
return r.Client.ListTagsForResource(ctx, input, optFns...)
279+
}
280+
277281
// ---------- S3 ----------
278282
func NewS3Client(cfg aws.Config) S3API {
279283
return &RealS3Client{

pkg/providers/aws/cluster_network_provider_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,7 @@ type mockElasticacheClient struct {
538538
batchApplyUpdateActionFn func(ctx context.Context, input *elasticache.BatchApplyUpdateActionInput, opts ...func(*elasticache.Options)) (*elasticache.BatchApplyUpdateActionOutput, error)
539539
addTagsToResourceFn func(ctx context.Context, input *elasticache.AddTagsToResourceInput, opts ...func(*elasticache.Options)) (*elasticache.AddTagsToResourceOutput, error)
540540
createReplicationGroupFn func(ctx context.Context, input *elasticache.CreateReplicationGroupInput, opts ...func(*elasticache.Options)) (*elasticache.CreateReplicationGroupOutput, error)
541+
listTagsForResourceFn func(ctx context.Context, input *elasticache.ListTagsForResourceInput, opts ...func(*elasticache.Options)) (*elasticache.ListTagsForResourceOutput, error)
541542
calls struct {
542543
DescribeSnapshots []struct {
543544
In1 *elasticache.DescribeSnapshotsInput

pkg/providers/aws/credentials.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ var (
8181
"elasticache:ModifyCacheSubnetGroup",
8282
"elasticache:DeleteCacheSubnetGroup",
8383
"elasticache:ModifyReplicationGroup",
84+
"elasticache:ListTagsForResource",
8485
"rds:DescribeDBInstances",
8586
"rds:CreateDBInstance",
8687
"rds:DeleteDBInstance",

pkg/providers/aws/provider_redis.go

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -455,20 +455,33 @@ func (p *RedisProvider) TagElasticacheNode(ctx context.Context, elasticacheClien
455455
// need arn in the following format arn:aws:elasticache:us-east-1:1234567890:cluster:my-mem-cluster
456456
arn := fmt.Sprintf("arn:aws:elasticache:%s:%s:cluster:%s", region, *id.Account, *cache.CacheClusterId)
457457

458+
// Get default tags
458459
cacheTags, clusterID, err := p.getDefaultElasticacheTags(ctx, r)
459460
if err != nil {
460461
msg := "Failed to build default tags"
461462
return croType.StatusMessage(msg), errorUtil.Wrapf(err, msg)
462463
}
463464

464-
// add tags
465-
_, err = elasticacheClient.AddTagsToResource(ctx, &elasticache.AddTagsToResourceInput{
466-
ResourceName: aws.String(arn),
467-
Tags: cacheTags,
468-
})
465+
// Filter out already applied tags
466+
filteredTags, err := filterAlreadyAppliedTags(ctx, elasticacheClient, arn, cacheTags)
469467
if err != nil {
470-
msg := "failed to add tags to aws elasticache :"
471-
return croType.StatusMessage(msg), err
468+
msg := "Failed to filter already applied tags"
469+
return croType.StatusMessage(msg), errorUtil.Wrapf(err, msg)
470+
}
471+
472+
// Add only new/changed tags (if any)
473+
if len(filteredTags) > 0 {
474+
_, err = elasticacheClient.AddTagsToResource(ctx, &elasticache.AddTagsToResourceInput{
475+
ResourceName: aws.String(arn),
476+
Tags: filteredTags,
477+
})
478+
if err != nil {
479+
msg := "failed to add tags to aws elasticache :"
480+
return croType.StatusMessage(msg), err
481+
}
482+
logrus.Infof("Successfully applied %d new/updated tags to cluster %s", len(filteredTags), arn)
483+
} else {
484+
logrus.Infof("Redis cluster %s: no tag changes required", arn)
472485
}
473486

474487
// if snapshots exist add tags to them
@@ -478,7 +491,7 @@ func (p *RedisProvider) TagElasticacheNode(ctx context.Context, elasticacheClien
478491

479492
// loop snapshots adding tags per found snapshot
480493
snapshotList, _ := elasticacheClient.DescribeSnapshots(ctx, inputDescribe)
481-
if len(snapshotList.Snapshots) > 0 {
494+
if len(snapshotList.Snapshots) > 0 && (len(filteredTags) > 0) {
482495
metricName := getMetricName(r.Name)
483496
// We need to reset before recreating so that metrics for deleted snapshots are not orphaned
484497
resources.ResetMetric(metricName)
@@ -1247,7 +1260,6 @@ func (p *RedisProvider) applySpecifiedSecurityUpdates(ctx context.Context, elast
12471260
}
12481261
return nil
12491262
}
1250-
12511263
func (p *RedisProvider) applyServiceUpdate(ctx context.Context, elasticacheClient ElastiCacheAPI, replicationgroupid, serviceupdateName *string) error {
12521264
logger := p.Logger.WithField("action", "applyServiceUpdate")
12531265

@@ -1278,3 +1290,34 @@ func validServiceUpdateStates(status string) bool {
12781290
}
12791291
return false
12801292
}
1293+
1294+
// filterAlreadyAppliedTags removes tags from `desired` that already exist (same key and value) on the resource.
1295+
func filterAlreadyAppliedTags(ctx context.Context, client ElastiCacheAPI, resourceARN string, desired []elasticachetypes.Tag) ([]elasticachetypes.Tag, error) {
1296+
// List current tags on the resource
1297+
resp, err := client.ListTagsForResource(ctx, &elasticache.ListTagsForResourceInput{
1298+
ResourceName: aws.String(resourceARN),
1299+
})
1300+
if err != nil {
1301+
// If we can't list tags (permission issue), fall back to applying all tags
1302+
logrus.Warnf("Could not list existing tags for %s: %v. Will attempt to apply all tags (may result in unnecessary API calls for already-applied tags).", resourceARN, err)
1303+
return desired, nil
1304+
}
1305+
1306+
// Build a map of current tags
1307+
currentTags := make(map[string]string)
1308+
for _, tag := range resp.TagList {
1309+
currentTags[*tag.Key] = *tag.Value
1310+
}
1311+
1312+
// Filter out tags that are already applied with same value
1313+
var filtered []elasticachetypes.Tag
1314+
for _, tag := range desired {
1315+
val, exists := currentTags[*tag.Key]
1316+
if !exists || val != *tag.Value {
1317+
// Either tag doesn't exist or value is different — keep it
1318+
filtered = append(filtered, tag)
1319+
}
1320+
}
1321+
1322+
return filtered, nil
1323+
}

pkg/providers/aws/provider_redis_test.go

Lines changed: 179 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,15 @@ func (m *mockStsClient) GetCallerIdentity(*sts.GetCallerIdentityInput) (*sts.Get
246246
}, nil
247247
}
248248

249+
// mock elasticache ListTagsForResource output
250+
func (m *mockElasticacheClient) ListTagsForResource(ctx context.Context, input *elasticache.ListTagsForResourceInput) (*elasticache.ListTagsForResourceOutput, error) {
251+
if resources.SafeStringDereference(input.ResourceName) == "arn:aws:elasticache:tes:test:cluster:test" {
252+
return &elasticache.ListTagsForResourceOutput{}, nil
253+
} else {
254+
return m.listTagsForResourceFn(ctx, input)
255+
}
256+
}
257+
249258
func buildTestPrometheusRule() *monitoringv1.PrometheusRule {
250259
return &monitoringv1.PrometheusRule{
251260
ObjectMeta: controllerruntime.ObjectMeta{
@@ -2462,6 +2471,7 @@ func TestAWSRedisProvider_TagElasticache(t *testing.T) {
24622471
},
24632472
}, nil)
24642473
mockElasticache.On("AddTagsToResource", mock.Anything, mock.Anything, mock.Anything).Return(&elasticache.AddTagsToResourceOutput{}, nil)
2474+
mockElasticache.On("ListTagsForResource", mock.Anything, mock.Anything, mock.Anything).Return(&elasticache.ListTagsForResourceOutput{}, nil)
24652475
mockElasticache.On("DescribeCacheClusters", mock.Anything, mock.Anything, mock.Anything).Return(&elasticache.DescribeCacheClustersOutput{
24662476
CacheClusters: buildCacheClusterList(nil),
24672477
}, nil)
@@ -2507,6 +2517,7 @@ func TestAWSRedisProvider_TagElasticache(t *testing.T) {
25072517
},
25082518
}, &mockSnapshotNotFoundError{})
25092519
mockElasticache.On("AddTagsToResource", mock.Anything, mock.Anything, mock.Anything).Return((*elasticache.AddTagsToResourceOutput)(nil), nil)
2520+
mockElasticache.On("ListTagsForResource", mock.Anything, mock.Anything, mock.Anything).Return(&elasticache.ListTagsForResourceOutput{}, nil)
25102521
mockElasticache.On("DescribeCacheClusters", mock.Anything, mock.Anything, mock.Anything).Return(&elasticache.DescribeCacheClustersOutput{
25112522
CacheClusters: buildCacheClusterList(nil),
25122523
}, nil)
@@ -2541,7 +2552,9 @@ func TestAWSRedisProvider_TagElasticache(t *testing.T) {
25412552
r: buildTestRedisCR(),
25422553
elastiCacheClient: func() ElastiCacheAPI {
25432554
mockElasticache := new(mock_ElasticacheClient)
2544-
mockElasticache.On("DescribeReplicationGroups", mock.Anything, mock.Anything, mock.Anything).Return(&elasticache.DescribeReplicationGroupsOutput{}, nil)
2555+
mockElasticache.On("DescribeReplicationGroups", mock.Anything, mock.Anything, mock.Anything).Return(&elasticache.DescribeCacheClustersOutput{
2556+
CacheClusters: buildCacheClusterList(nil),
2557+
}, nil)
25452558
mockElasticache.On("DescribeSnapshots", mock.Anything, mock.Anything, mock.Anything).Return(&elasticache.DescribeSnapshotsOutput{
25462559
Snapshots: []elasticachetypes.Snapshot{
25472560
{
@@ -2551,6 +2564,7 @@ func TestAWSRedisProvider_TagElasticache(t *testing.T) {
25512564
}, nil)
25522565
mockElasticache.On("AddTagsToResource", mock.Anything, mock.Anything, mock.Anything).Return((*elasticache.AddTagsToResourceOutput)(nil), nil).Once()
25532566
mockElasticache.On("AddTagsToResource", mock.Anything, mock.Anything, mock.Anything).Return((*elasticache.AddTagsToResourceOutput)(nil), genericAWSError)
2567+
mockElasticache.On("ListTagsForResource", mock.Anything, mock.Anything, mock.Anything).Return(&elasticache.ListTagsForResourceOutput{}, nil)
25542568
mockElasticache.On("DescribeCacheClusters", mock.Anything, mock.Anything, mock.Anything).Return(&elasticache.DescribeCacheClustersOutput{
25552569
CacheClusters: buildCacheClusterList(nil),
25562570
}, nil)
@@ -2597,6 +2611,7 @@ func TestAWSRedisProvider_TagElasticache(t *testing.T) {
25972611
}, errors.New("SnapshotAlreadyExistsFault"))
25982612
mockElasticache.On("AddTagsToResource", mock.Anything, mock.Anything, mock.Anything).Return((*elasticache.AddTagsToResourceOutput)(nil), nil).Once()
25992613
mockElasticache.On("AddTagsToResource", mock.Anything, mock.Anything, mock.Anything).Return((*elasticache.AddTagsToResourceOutput)(nil), errors.New("SnapshotAlreadyExistsFault")).Once()
2614+
mockElasticache.On("ListTagsForResource", mock.Anything, mock.Anything, mock.Anything).Return(&elasticache.ListTagsForResourceOutput{}, nil)
26002615
mockElasticache.On("DescribeCacheClusters", mock.Anything, mock.Anything, mock.Anything).Return(&elasticache.DescribeCacheClustersOutput{
26012616
CacheClusters: buildCacheClusterList(nil),
26022617
}, nil)
@@ -3564,3 +3579,166 @@ func TestRedisProvider_getElasticacheConfig(t *testing.T) {
35643579
})
35653580
}
35663581
}
3582+
3583+
// Test_filterAlreadyAppliedTags was generated with the help of Cursor AI IDE.
3584+
func Test_filterAlreadyAppliedTags(t *testing.T) {
3585+
type args struct {
3586+
ctx context.Context
3587+
client ElastiCacheAPI
3588+
resourceARN string
3589+
desired []elasticachetypes.Tag
3590+
}
3591+
tests := []struct {
3592+
name string
3593+
args args
3594+
want []elasticachetypes.Tag
3595+
wantErr bool
3596+
}{
3597+
{
3598+
name: "error case - ListTagsForResource fails",
3599+
args: args{
3600+
ctx: context.TODO(),
3601+
client: func() ElastiCacheAPI {
3602+
mockClient := new(mock_ElasticacheClient)
3603+
mockClient.On("ListTagsForResource", mock.Anything, mock.Anything, mock.Anything).Return(
3604+
(*elasticache.ListTagsForResourceOutput)(nil),
3605+
errors.New("API error"),
3606+
)
3607+
return mockClient
3608+
}(),
3609+
resourceARN: "arn:aws:elasticache:us-east-1:123456789012:cluster:test-cluster",
3610+
desired: []elasticachetypes.Tag{
3611+
{Key: aws.String("Environment"), Value: aws.String("test")},
3612+
},
3613+
},
3614+
want: nil,
3615+
wantErr: true,
3616+
},
3617+
{
3618+
name: "success case - no existing tags, all desired tags should be kept",
3619+
args: args{
3620+
ctx: context.TODO(),
3621+
client: func() ElastiCacheAPI {
3622+
mockClient := new(mock_ElasticacheClient)
3623+
mockClient.On("ListTagsForResource", mock.Anything, mock.Anything, mock.Anything).Return(
3624+
&elasticache.ListTagsForResourceOutput{
3625+
TagList: []elasticachetypes.Tag{}, // No existing tags
3626+
},
3627+
nil,
3628+
)
3629+
return mockClient
3630+
}(),
3631+
resourceARN: "arn:aws:elasticache:us-east-1:123456789012:cluster:test-cluster",
3632+
desired: []elasticachetypes.Tag{
3633+
{Key: aws.String("Environment"), Value: aws.String("test")},
3634+
{Key: aws.String("Team"), Value: aws.String("backend")},
3635+
},
3636+
},
3637+
want: []elasticachetypes.Tag{
3638+
{Key: aws.String("Environment"), Value: aws.String("test")},
3639+
{Key: aws.String("Team"), Value: aws.String("backend")},
3640+
},
3641+
wantErr: false,
3642+
},
3643+
{
3644+
name: "success case - some tags already exist with same values, should be filtered out",
3645+
args: args{
3646+
ctx: context.TODO(),
3647+
client: func() ElastiCacheAPI {
3648+
mockClient := new(mock_ElasticacheClient)
3649+
mockClient.On("ListTagsForResource", mock.Anything, mock.Anything, mock.Anything).Return(
3650+
&elasticache.ListTagsForResourceOutput{
3651+
TagList: []elasticachetypes.Tag{
3652+
{Key: aws.String("Environment"), Value: aws.String("test")},
3653+
{Key: aws.String("Owner"), Value: aws.String("platform")},
3654+
},
3655+
},
3656+
nil,
3657+
)
3658+
return mockClient
3659+
}(),
3660+
resourceARN: "arn:aws:elasticache:us-east-1:123456789012:cluster:test-cluster",
3661+
desired: []elasticachetypes.Tag{
3662+
{Key: aws.String("Environment"), Value: aws.String("test")}, // Same value - should be filtered
3663+
{Key: aws.String("Team"), Value: aws.String("backend")}, // New tag - should be kept
3664+
{Key: aws.String("Owner"), Value: aws.String("engineering")}, // Different value - should be kept
3665+
},
3666+
},
3667+
want: []elasticachetypes.Tag{
3668+
{Key: aws.String("Team"), Value: aws.String("backend")},
3669+
{Key: aws.String("Owner"), Value: aws.String("engineering")},
3670+
},
3671+
wantErr: false,
3672+
},
3673+
{
3674+
name: "success case - all tags already exist with same values, empty result",
3675+
args: args{
3676+
ctx: context.TODO(),
3677+
client: func() ElastiCacheAPI {
3678+
mockClient := new(mock_ElasticacheClient)
3679+
mockClient.On("ListTagsForResource", mock.Anything, mock.Anything, mock.Anything).Return(
3680+
&elasticache.ListTagsForResourceOutput{
3681+
TagList: []elasticachetypes.Tag{
3682+
{Key: aws.String("Environment"), Value: aws.String("test")},
3683+
{Key: aws.String("Team"), Value: aws.String("backend")},
3684+
},
3685+
},
3686+
nil,
3687+
)
3688+
return mockClient
3689+
}(),
3690+
resourceARN: "arn:aws:elasticache:us-east-1:123456789012:cluster:test-cluster",
3691+
desired: []elasticachetypes.Tag{
3692+
{Key: aws.String("Environment"), Value: aws.String("test")},
3693+
{Key: aws.String("Team"), Value: aws.String("backend")},
3694+
},
3695+
},
3696+
want: []elasticachetypes.Tag{},
3697+
wantErr: false,
3698+
},
3699+
{
3700+
name: "success case - existing tags with nil values handled gracefully",
3701+
args: args{
3702+
ctx: context.TODO(),
3703+
client: func() ElastiCacheAPI {
3704+
mockClient := new(mock_ElasticacheClient)
3705+
// Create tags with valid pointers to test the dereferencing
3706+
existingKey := "Environment"
3707+
existingValue := "production"
3708+
mockClient.On("ListTagsForResource", mock.Anything, mock.Anything, mock.Anything).Return(
3709+
&elasticache.ListTagsForResourceOutput{
3710+
TagList: []elasticachetypes.Tag{
3711+
{Key: &existingKey, Value: &existingValue},
3712+
},
3713+
},
3714+
nil,
3715+
)
3716+
return mockClient
3717+
}(),
3718+
resourceARN: "arn:aws:elasticache:us-east-1:123456789012:cluster:test-cluster",
3719+
desired: []elasticachetypes.Tag{
3720+
{Key: aws.String("Environment"), Value: aws.String("test")}, // Different value - should be kept
3721+
},
3722+
},
3723+
want: []elasticachetypes.Tag{
3724+
{Key: aws.String("Environment"), Value: aws.String("test")},
3725+
},
3726+
wantErr: false,
3727+
},
3728+
}
3729+
for _, tt := range tests {
3730+
t.Run(tt.name, func(t *testing.T) {
3731+
got, err := filterAlreadyAppliedTags(tt.args.ctx, tt.args.client, tt.args.resourceARN, tt.args.desired)
3732+
if (err != nil) != tt.wantErr {
3733+
t.Errorf("filterAlreadyAppliedTags() error = %v, wantErr %v", err, tt.wantErr)
3734+
return
3735+
}
3736+
// Handle nil vs empty slice comparison
3737+
if (got == nil && len(tt.want) == 0) || (len(got) == 0 && len(tt.want) == 0) || reflect.DeepEqual(got, tt.want) {
3738+
// Test passes - nil slice and empty slice are equivalent for our purposes
3739+
} else {
3740+
t.Errorf("filterAlreadyAppliedTags() got = %v, want %v", got, tt.want)
3741+
}
3742+
})
3743+
}
3744+
}

0 commit comments

Comments
 (0)