From 505979cfa1863794fd0dac13db7cbf29a247d278 Mon Sep 17 00:00:00 2001 From: Pablo Barsotti Date: Fri, 24 Oct 2025 16:39:59 +0200 Subject: [PATCH] feat: Add annotation to tweak providedThroughput control for tables --- apis/v1alpha1/ack-generate-metadata.yaml | 10 +-- apis/v1alpha1/annotation.go | 50 ++++++++++++ apis/v1alpha1/generator.yaml | 2 + generator.yaml | 2 + pkg/resource/table/delta.go | 1 + pkg/resource/table/hooks.go | 41 ++++++++++ pkg/resource/table/hooks_test.go | 96 ++++++++++++++++++++++++ 7 files changed, 197 insertions(+), 5 deletions(-) create mode 100644 apis/v1alpha1/annotation.go diff --git a/apis/v1alpha1/ack-generate-metadata.yaml b/apis/v1alpha1/ack-generate-metadata.yaml index 994533a..3ae33f1 100755 --- a/apis/v1alpha1/ack-generate-metadata.yaml +++ b/apis/v1alpha1/ack-generate-metadata.yaml @@ -1,13 +1,13 @@ ack_generate_info: - build_date: "2025-10-21T04:38:02Z" - build_hash: 6b4211163dcc34776b01da9a18217bac0f4103fd + build_date: "2025-10-24T13:21:34Z" + build_hash: eaabefb6bd7b2be8a1baf4478f22b3310e6921c8 go_version: go1.24.6 - version: v0.52.0 -api_directory_checksum: d2887bf57c4e94a2687e17c41f74c875131c0beb + version: v0.52.0-6-geaabefb +api_directory_checksum: e4d87325b7f3dab8a75e052c7538bc803f871048 api_version: v1alpha1 aws_sdk_go_version: v1.32.6 generator_config_info: - file_checksum: 3c4832feff83bc9c29b40bc73bafc1d7e75ab1cd + file_checksum: 171412a113b31a5564a67d45ddb3face9ca90e15 original_file_name: generator.yaml last_modification: reason: API generation diff --git a/apis/v1alpha1/annotation.go b/apis/v1alpha1/annotation.go new file mode 100644 index 0000000..6c2beea --- /dev/null +++ b/apis/v1alpha1/annotation.go @@ -0,0 +1,50 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package v1alpha1 + +import "fmt" + +var ( + // TableProvisionedThroughputManagedByAnnotation is the annotation key used to set the management + // style for the provisioned throughput of a DynamoDB table. This annotation can only be set on a + // table custom resource. + // + // The value of this annotation must be one of the following: + // + // - 'external-autoscaler': The provisioned throughput is managed by an external entity. Causing + // the controller to completly ignore the fields `readCapacityUnits` + // and `writeCapacityUnits` of `provisionedThroughput` and not reconcile + // the provisioned throughput of a table. + // + // - 'ack-dynamodb-controller': The provisioned throughput is managed by the ACK controller. + // Causing the controller to reconcile the provisioned throughput of the + // table with the values of the `spec.provisionedThroughput` field + // (`readCapacityUnits` and `writeCapacityUnits`). + // + // By default the provisioned throughput is managed by the controller. If the annotation is not set, or + // the value is not one of the above, the controller will default to managing the provisioned throughput + // as if the annotation was set to "controller". + TableProvisionedThroughputManagedByAnnotation = fmt.Sprintf("%s/table-provisioned-throughput-managed-by", GroupVersion.Group) +) + +const ( + // TableProvisionedThroughputManagedByExternalAutoscaler is the value of the + // TableProvisionedThroughputManagedByAnnotation annotation that indicates that the provisioned + // throughput of a table is managed by an external autoscaler. + TableProvisionedThroughputManagedByExternalAutoscaler = "external-autoscaler" + // TableProvisionedThroughputManagedByACKController is the value of the + // TableProvisionedThroughputManagedByAnnotation annotation that indicates that the provisioned + // throughput of a table is managed by the ACK controller. + TableProvisionedThroughputManagedByACKController = "ack-dynamodb-controller" +) diff --git a/apis/v1alpha1/generator.yaml b/apis/v1alpha1/generator.yaml index 42bbe2a..24d2bba 100644 --- a/apis/v1alpha1/generator.yaml +++ b/apis/v1alpha1/generator.yaml @@ -93,6 +93,8 @@ resources: hooks: delta_pre_compare: code: customPreCompare(delta, a, b) + delta_post_compare: + code: customPostCompare(delta, a, b) sdk_create_post_set_output: template_path: hooks/table/sdk_create_post_set_output.go.tpl sdk_read_one_post_set_output: diff --git a/generator.yaml b/generator.yaml index 42bbe2a..24d2bba 100644 --- a/generator.yaml +++ b/generator.yaml @@ -93,6 +93,8 @@ resources: hooks: delta_pre_compare: code: customPreCompare(delta, a, b) + delta_post_compare: + code: customPostCompare(delta, a, b) sdk_create_post_set_output: template_path: hooks/table/sdk_create_post_set_output.go.tpl sdk_read_one_post_set_output: diff --git a/pkg/resource/table/delta.go b/pkg/resource/table/delta.go index 520d86a..7c1ce2a 100644 --- a/pkg/resource/table/delta.go +++ b/pkg/resource/table/delta.go @@ -138,5 +138,6 @@ func newResourceDelta( } } + customPostCompare(delta, a, b) return delta } diff --git a/pkg/resource/table/hooks.go b/pkg/resource/table/hooks.go index f380fad..7ecd781 100644 --- a/pkg/resource/table/hooks.go +++ b/pkg/resource/table/hooks.go @@ -910,3 +910,44 @@ func (rm *resourceManager) updateContributorInsights( return nil } + +func getTableProvisionedThroughputManagedByAnnotation(table *svcapitypes.Table) (string, bool) { + if len(table.Annotations) == 0 { + return "", false + } + managedBy, ok := table.Annotations[svcapitypes.TableProvisionedThroughputManagedByAnnotation] + return managedBy, ok +} + +func isTableProvisionedThroughputManagedByExternalAutoscaler(table *svcapitypes.Table) bool { + managedBy, ok := getTableProvisionedThroughputManagedByAnnotation(table) + if !ok { + return false + } + return managedBy == svcapitypes.TableProvisionedThroughputManagedByExternalAutoscaler +} + +func customPostCompare( + delta *ackcompare.Delta, + a *resource, + b *resource, +) { + // We only want to compare the ProvisionedThroughput field if and only if the + // ProvisionedThroughput is managed by the controller, meaning that in the case + // where the ProvisionedThroughput is managed by an external entity, we do not + // want to compare the ProvisionedThroughput field. + // When managed by an external entity, an annotation is set on the + // table resource to indicate that the ProvisionedThroughput is managed + // externally. + if isTableProvisionedThroughputManagedByExternalAutoscaler(a.ko) && delta.DifferentAt("Spec.ProvisionedThroughput") { + // We need to unset the ProvisionedThroughput field in the delta so that the + // controller does not attempt to reconcile it. + newDiffs := make([]*ackcompare.Difference, 0) + for _, d := range delta.Differences { + if !d.Path.Contains("Spec.ProvisionedThroughput") { + newDiffs = append(newDiffs, d) + } + } + delta.Differences = newDiffs + } +} diff --git a/pkg/resource/table/hooks_test.go b/pkg/resource/table/hooks_test.go index faaf429..d5a317e 100644 --- a/pkg/resource/table/hooks_test.go +++ b/pkg/resource/table/hooks_test.go @@ -22,6 +22,7 @@ import ( "github.com/stretchr/testify/require" "github.com/aws-controllers-k8s/dynamodb-controller/apis/v1alpha1" + svcapitypes "github.com/aws-controllers-k8s/dynamodb-controller/apis/v1alpha1" ) var ( @@ -505,3 +506,98 @@ func Test_newResourceDelta_customDeltaFunction_AttributeDefinitions(t *testing.T }) } } + +func Test_compareProvisionedThroughput(t *testing.T) { + type managementType int + const ( + managedByDefault managementType = iota + managedByACKController + managedByExternalAutoscaler + ) + + // Helper to create table with given throughput parameters + createTable := func(rpu, wpu *int64) *v1alpha1.Table { + if rpu == nil && wpu == nil { + return &v1alpha1.Table{ + Spec: v1alpha1.TableSpec{ + ProvisionedThroughput: nil, + }, + } + } + return &v1alpha1.Table{ + Spec: v1alpha1.TableSpec{ + ProvisionedThroughput: &v1alpha1.ProvisionedThroughput{ + ReadCapacityUnits: rpu, + WriteCapacityUnits: wpu, + }, + }, + } + } + + // Helper function to apply management type to a table + applyManagement := func(table *v1alpha1.Table, mgmt managementType) *v1alpha1.Table { + switch mgmt { + case managedByACKController: + if table.Annotations == nil { + table.Annotations = make(map[string]string) + } + table.Annotations[svcapitypes.TableProvisionedThroughputManagedByAnnotation] = svcapitypes.TableProvisionedThroughputManagedByACKController + return table + case managedByExternalAutoscaler: + if table.Annotations == nil { + table.Annotations = make(map[string]string) + } + table.Annotations[svcapitypes.TableProvisionedThroughputManagedByAnnotation] = svcapitypes.TableProvisionedThroughputManagedByExternalAutoscaler + return table + default: + return table // managedByDefault + } + } + + tests := []struct { + name string + mgmt managementType + aRpu, aWpu *int64 // Table A provisioned throughput (nil means no throughput spec) + bRpu, bWpu *int64 // Table B provisioned throughput (nil means no throughput spec) + expectDelta bool + }{ + // ManagedByDefault scenarios + {"default: both nil ProvisionedThroughput", managedByDefault, nil, nil, nil, nil, false}, + {"default: (a) nil ProvisionedThroughput", managedByDefault, nil, nil, aws.Int64(5), aws.Int64(5), true}, + {"default: (b) nil ProvisionedThroughput", managedByDefault, aws.Int64(5), aws.Int64(5), nil, nil, true}, + {"default: equal ProvisionedThroughput", managedByDefault, aws.Int64(5), aws.Int64(5), aws.Int64(5), aws.Int64(5), false}, + {"default: different ProvisionedThroughput", managedByDefault, aws.Int64(5), aws.Int64(5), aws.Int64(10), aws.Int64(5), true}, + + // ManagedByACKController scenarios + {"ack: both nil ProvisionedThroughput", managedByACKController, nil, nil, nil, nil, false}, + {"ack: (a) nil ProvisionedThroughput", managedByACKController, nil, nil, aws.Int64(5), aws.Int64(5), true}, + {"ack: (b) nil ProvisionedThroughput", managedByACKController, aws.Int64(5), aws.Int64(5), nil, nil, true}, + {"ack: equal ProvisionedThroughput", managedByACKController, aws.Int64(5), aws.Int64(5), aws.Int64(5), aws.Int64(5), false}, + {"ack: different ProvisionedThroughput", managedByACKController, aws.Int64(5), aws.Int64(5), aws.Int64(10), aws.Int64(5), true}, + + // ManagedByExternalAutoscaler scenarios (delta should be false for changes) + {"external: both nil ProvisionedThroughput", managedByExternalAutoscaler, nil, nil, nil, nil, false}, + {"external: (a) nil ProvisionedThroughput", managedByExternalAutoscaler, nil, nil, aws.Int64(5), aws.Int64(5), false}, + {"external: (b) nil ProvisionedThroughput", managedByExternalAutoscaler, aws.Int64(5), aws.Int64(5), nil, nil, false}, + {"external: equal ProvisionedThroughput", managedByExternalAutoscaler, aws.Int64(5), aws.Int64(5), aws.Int64(5), aws.Int64(5), false}, + {"external: different ProvisionedThroughput", managedByExternalAutoscaler, aws.Int64(5), aws.Int64(5), aws.Int64(10), aws.Int64(5), false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create tables + tableA := createTable(tt.aRpu, tt.aWpu) + tableB := createTable(tt.bRpu, tt.bWpu) + + // Apply management type + tableA = applyManagement(tableA, tt.mgmt) + tableB = applyManagement(tableB, tt.mgmt) + + // Test comparison + delta := newResourceDelta(&resource{tableA}, &resource{tableB}) + if tt.expectDelta != delta.DifferentAt("Spec.ProvisionedThroughput") { + t.Errorf("customPostCompare() has delta at ProvisionedThroughput = %v, want %v", delta.DifferentAt("Spec.ProvisionedThroughput"), tt.expectDelta) + } + }) + } +}