diff --git a/apis/v1alpha1/ack-generate-metadata.yaml b/apis/v1alpha1/ack-generate-metadata.yaml index 7997350..72228a1 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-03-28T01:43:12Z" + build_date: "2025-04-01T02:35:33Z" build_hash: 980cb1e4734f673d16101cf55206b84ca639ec01 go_version: go1.24.1 version: v0.44.0 -api_directory_checksum: cb49386ebd7bb50e2521072a76262c72b9dbd285 +api_directory_checksum: 2f94829f12ad90f9c36c48823459c161c1670093 api_version: v1alpha1 aws_sdk_go_version: v1.32.6 generator_config_info: - file_checksum: 4533fa8aca3b134b5895ad6ce9a093c3446d99da + file_checksum: e7e79c3c7c21273967ca24947fda0f26b344c8f0 original_file_name: generator.yaml last_modification: reason: API generation diff --git a/apis/v1alpha1/generator.yaml b/apis/v1alpha1/generator.yaml index 4ed38b6..3dd2453 100644 --- a/apis/v1alpha1/generator.yaml +++ b/apis/v1alpha1/generator.yaml @@ -70,6 +70,10 @@ resources: service_name: kms resource: Key path: Status.ACKResourceMetadata.ARN + ContributorInsights: + from: + operation: UpdateContributorInsights + path: ContributorInsightsAction exceptions: errors: 404: @@ -86,6 +90,8 @@ resources: template_path: hooks/table/sdk_create_post_set_output.go.tpl sdk_read_one_post_set_output: template_path: hooks/table/sdk_read_one_post_set_output.go.tpl + sdk_update_pre_build_request: + template_path: hooks/table/sdk_update_pre_build_request.go.tpl sdk_delete_pre_build_request: template_path: hooks/table/sdk_delete_pre_build_request.go.tpl synced: diff --git a/apis/v1alpha1/table.go b/apis/v1alpha1/table.go index 4865df0..ae40556 100644 --- a/apis/v1alpha1/table.go +++ b/apis/v1alpha1/table.go @@ -38,6 +38,8 @@ type TableSpec struct { BillingMode *string `json:"billingMode,omitempty"` // Represents the settings used to enable point in time recovery. ContinuousBackups *PointInTimeRecoverySpecification `json:"continuousBackups,omitempty"` + // Represents the contributor insights action. + ContributorInsights *string `json:"contributorInsights,omitempty"` // Indicates whether deletion protection is to be enabled (true) or disabled // (false) on the table. DeletionProtectionEnabled *bool `json:"deletionProtectionEnabled,omitempty"` diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index 46bd371..3fed78f 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -2696,6 +2696,11 @@ func (in *TableSpec) DeepCopyInto(out *TableSpec) { *out = new(PointInTimeRecoverySpecification) (*in).DeepCopyInto(*out) } + if in.ContributorInsights != nil { + in, out := &in.ContributorInsights, &out.ContributorInsights + *out = new(string) + **out = **in + } if in.DeletionProtectionEnabled != nil { in, out := &in.DeletionProtectionEnabled, &out.DeletionProtectionEnabled *out = new(bool) diff --git a/config/crd/bases/dynamodb.services.k8s.aws_tables.yaml b/config/crd/bases/dynamodb.services.k8s.aws_tables.yaml index 196263e..634bac7 100644 --- a/config/crd/bases/dynamodb.services.k8s.aws_tables.yaml +++ b/config/crd/bases/dynamodb.services.k8s.aws_tables.yaml @@ -88,6 +88,9 @@ spec: pointInTimeRecoveryEnabled: type: boolean type: object + contributorInsights: + description: Represents the contributor insights action. + type: string deletionProtectionEnabled: description: |- Indicates whether deletion protection is to be enabled (true) or disabled diff --git a/generator.yaml b/generator.yaml index 4ed38b6..3dd2453 100644 --- a/generator.yaml +++ b/generator.yaml @@ -70,6 +70,10 @@ resources: service_name: kms resource: Key path: Status.ACKResourceMetadata.ARN + ContributorInsights: + from: + operation: UpdateContributorInsights + path: ContributorInsightsAction exceptions: errors: 404: @@ -86,6 +90,8 @@ resources: template_path: hooks/table/sdk_create_post_set_output.go.tpl sdk_read_one_post_set_output: template_path: hooks/table/sdk_read_one_post_set_output.go.tpl + sdk_update_pre_build_request: + template_path: hooks/table/sdk_update_pre_build_request.go.tpl sdk_delete_pre_build_request: template_path: hooks/table/sdk_delete_pre_build_request.go.tpl synced: diff --git a/helm/crds/dynamodb.services.k8s.aws_tables.yaml b/helm/crds/dynamodb.services.k8s.aws_tables.yaml index feaa235..cd2c243 100644 --- a/helm/crds/dynamodb.services.k8s.aws_tables.yaml +++ b/helm/crds/dynamodb.services.k8s.aws_tables.yaml @@ -88,6 +88,9 @@ spec: pointInTimeRecoveryEnabled: type: boolean type: object + contributorInsights: + description: Represents the contributor insights action. + type: string deletionProtectionEnabled: description: |- Indicates whether deletion protection is to be enabled (true) or disabled diff --git a/pkg/resource/table/delta.go b/pkg/resource/table/delta.go index 520d86a..954881e 100644 --- a/pkg/resource/table/delta.go +++ b/pkg/resource/table/delta.go @@ -62,6 +62,13 @@ func newResourceDelta( } } } + if ackcompare.HasNilDifference(a.ko.Spec.ContributorInsights, b.ko.Spec.ContributorInsights) { + delta.Add("Spec.ContributorInsights", a.ko.Spec.ContributorInsights, b.ko.Spec.ContributorInsights) + } else if a.ko.Spec.ContributorInsights != nil && b.ko.Spec.ContributorInsights != nil { + if *a.ko.Spec.ContributorInsights != *b.ko.Spec.ContributorInsights { + delta.Add("Spec.ContributorInsights", a.ko.Spec.ContributorInsights, b.ko.Spec.ContributorInsights) + } + } if ackcompare.HasNilDifference(a.ko.Spec.DeletionProtectionEnabled, b.ko.Spec.DeletionProtectionEnabled) { delta.Add("Spec.DeletionProtectionEnabled", a.ko.Spec.DeletionProtectionEnabled, b.ko.Spec.DeletionProtectionEnabled) } else if a.ko.Spec.DeletionProtectionEnabled != nil && b.ko.Spec.DeletionProtectionEnabled != nil { diff --git a/pkg/resource/table/hooks.go b/pkg/resource/table/hooks.go index 42f2bf0..a092102 100644 --- a/pkg/resource/table/hooks.go +++ b/pkg/resource/table/hooks.go @@ -31,6 +31,7 @@ import ( corev1 "k8s.io/api/core/v1" "github.com/aws-controllers-k8s/dynamodb-controller/apis/v1alpha1" + svcapitypes "github.com/aws-controllers-k8s/dynamodb-controller/apis/v1alpha1" ) var ( @@ -136,6 +137,15 @@ func isTableUpdating(r *resource) bool { return dbis == string(v1alpha1.TableStatus_SDK_UPDATING) } +func isTableContributorInsightsUpdating(r *resource) bool { + if r.ko.Spec.ContributorInsights == nil { + return false + } + insightStatus := *r.ko.Spec.ContributorInsights + return insightStatus == string(svcsdktypes.ContributorInsightsStatusEnabling) || + insightStatus == string(svcsdktypes.ContributorInsightsStatusDisabling) +} + func (rm *resourceManager) customUpdateTable( ctx context.Context, desired *resource, @@ -212,6 +222,13 @@ func (rm *resourceManager) customUpdateTable( } } + if delta.DifferentAt("Spec.ContributorInsights") { + err = rm.updateContributorInsights(ctx, desired) + if err != nil { + return &resource{ko}, err + } + } + // We want to update fast fields first // Then attributes // then GSI @@ -455,6 +472,9 @@ func (rm *resourceManager) setResourceAdditionalFields( ko.Spec.ContinuousBackups = pitrSpec } + if err = rm.setContributorInsights(ctx, ko); err != nil { + return err + } return nil } @@ -590,6 +610,11 @@ func customPreCompare( if a.ko.Spec.DeletionProtectionEnabled == nil { a.ko.Spec.DeletionProtectionEnabled = aws.Bool(false) } + + if a.ko.Spec.ContributorInsights == nil && b.ko.Spec.ContributorInsights != nil && + *b.ko.Spec.ContributorInsights == string(svcsdktypes.ContributorInsightsActionDisable) { + a.ko.Spec.ContributorInsights = b.ko.Spec.ContributorInsights + } } // equalAttributeDefinitions return whether two AttributeDefinition arrays are equal or not. @@ -724,3 +749,68 @@ func equalLocalSecondaryIndexes( } return true } + +// setContributorInsights retrieves the table's cloudformationInsights +// configuration +func (rm *resourceManager) setContributorInsights( + ctx context.Context, + ko *svcapitypes.Table, +) (err error) { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("rm.setCloudformationInsights") + defer func() { + exit(err) + }() + + resp, err := rm.sdkapi.DescribeContributorInsights( + ctx, + &svcsdk.DescribeContributorInsightsInput{ + TableName: ko.Spec.TableName, + }, + ) + rm.metrics.RecordAPICall("READ_ONE", "DescribeContributorInsights", err) + if err != nil { + return err + } + + switch resp.ContributorInsightsStatus { + case svcsdktypes.ContributorInsightsStatusEnabled: + ko.Spec.ContributorInsights = aws.String(string(svcsdktypes.ContributorInsightsActionEnable)) + case svcsdktypes.ContributorInsightsStatusDisabled: + ko.Spec.ContributorInsights = aws.String(string(svcsdktypes.ContributorInsightsActionDisable)) + default: + ko.Spec.ContributorInsights = aws.String(string(resp.ContributorInsightsStatus)) + + } + + return nil +} + +func (rm *resourceManager) updateContributorInsights( + ctx context.Context, + r *resource, +) (err error) { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("rm.updateCloudformationInsights") + defer func() { + exit(err) + }() + insight := svcsdktypes.ContributorInsightsActionDisable + if r.ko.Spec.ContributorInsights != nil { + insight = svcsdktypes.ContributorInsightsAction(*r.ko.Spec.ContributorInsights) + } + + _, err = rm.sdkapi.UpdateContributorInsights( + ctx, + &svcsdk.UpdateContributorInsightsInput{ + TableName: r.ko.Spec.TableName, + ContributorInsightsAction: insight, + }, + ) + rm.metrics.RecordAPICall("READ_ONE", "UpdateContributorInsights", err) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/resource/table/sdk.go b/pkg/resource/table/sdk.go index e9fc221..da0cc68 100644 --- a/pkg/resource/table/sdk.go +++ b/pkg/resource/table/sdk.go @@ -444,15 +444,16 @@ func (rm *resourceManager) sdkFind( if isTableCreating(&resource{ko}) { return &resource{ko}, requeueWaitWhileCreating } - if isTableUpdating(&resource{ko}) { - return &resource{ko}, requeueWaitWhileUpdating - } if !canUpdateTableGSIs(&resource{ko}) { return &resource{ko}, requeueWaitGSIReady } if err := rm.setResourceAdditionalFields(ctx, ko); err != nil { return nil, err } + if isTableUpdating(&resource{ko}) || isTableContributorInsightsUpdating(&resource{ko}) { + return &resource{ko}, requeueWaitWhileUpdating + } + return &resource{ko}, nil } @@ -808,6 +809,13 @@ func (rm *resourceManager) sdkCreate( return nil, err } } + + if desired.ko.Spec.ContributorInsights != nil { + if err := rm.updateContributorInsights(ctx, desired); err != nil { + return nil, err + } + } + return &resource{ko}, nil } diff --git a/templates/hooks/table/sdk_create_post_set_output.go.tpl b/templates/hooks/table/sdk_create_post_set_output.go.tpl index 49d67a2..8b777b5 100644 --- a/templates/hooks/table/sdk_create_post_set_output.go.tpl +++ b/templates/hooks/table/sdk_create_post_set_output.go.tpl @@ -2,4 +2,10 @@ if err := rm.syncTTL(ctx, desired, &resource{ko}); err != nil { return nil, err } - } \ No newline at end of file + } + + if desired.ko.Spec.ContributorInsights != nil { + if err := rm.updateContributorInsights(ctx, desired); err != nil { + return nil, err + } + } diff --git a/templates/hooks/table/sdk_read_one_post_set_output.go.tpl b/templates/hooks/table/sdk_read_one_post_set_output.go.tpl index 004a0d7..8ed5041 100644 --- a/templates/hooks/table/sdk_read_one_post_set_output.go.tpl +++ b/templates/hooks/table/sdk_read_one_post_set_output.go.tpl @@ -57,12 +57,12 @@ if isTableCreating(&resource{ko}) { return &resource{ko}, requeueWaitWhileCreating } - if isTableUpdating(&resource{ko}) { - return &resource{ko}, requeueWaitWhileUpdating - } if !canUpdateTableGSIs(&resource{ko}) { return &resource{ko}, requeueWaitGSIReady } if err := rm.setResourceAdditionalFields(ctx, ko); err != nil { return nil, err - } \ No newline at end of file + } + if isTableUpdating(&resource{ko}) || isTableContributorInsightsUpdating(&resource{ko}) { + return &resource{ko}, requeueWaitWhileUpdating + } diff --git a/test/e2e/resources/table_insights.yaml b/test/e2e/resources/table_insights.yaml new file mode 100644 index 0000000..9ee4a1e --- /dev/null +++ b/test/e2e/resources/table_insights.yaml @@ -0,0 +1,20 @@ +# Table used to test multiple interfering updates at once +apiVersion: dynamodb.services.k8s.aws/v1alpha1 +kind: Table +metadata: + name: $TABLE_NAME +spec: + tableName: $TABLE_NAME + billingMode: PAY_PER_REQUEST + tableClass: STANDARD + contributorInsights: ENABLE + attributeDefinitions: + - attributeName: Bill + attributeType: S + - attributeName: Total + attributeType: S + keySchema: + - attributeName: Bill + keyType: HASH + - attributeName: Total + keyType: RANGE diff --git a/test/e2e/table.py b/test/e2e/table.py index fff5b00..c85a0ae 100644 --- a/test/e2e/table.py +++ b/test/e2e/table.py @@ -236,6 +236,18 @@ def get(table_name): except c.exceptions.ResourceNotFoundException: return None +def get_insights(table_name): + """Returns a dict containing the Role record from the IAM API. + + If no such Table exists, returns None. + """ + c = boto3.client('dynamodb', region_name=get_region()) + try: + resp = c.describe_contributor_insights(TableName=table_name) + return resp['ContributorInsightsStatus'] + except c.exceptions.ResourceNotFoundException: + return None + def get_time_to_live(table_name): """Returns the TTL specification for the table with a supplied name. diff --git a/test/e2e/tests/test_table.py b/test/e2e/tests/test_table.py index f0f7536..8fa8ca5 100644 --- a/test/e2e/tests/test_table.py +++ b/test/e2e/tests/test_table.py @@ -136,6 +136,18 @@ def table_basic(): except: pass +@pytest.fixture(scope="function") +def table_insights(): + resource_name = random_suffix_name("table-insights", 32) + (ref, cr) = create_table(resource_name, "table_insights") + + yield ref, cr + try: + _, deleted = k8s.delete_custom_resource(ref, wait_periods=3, period_length=10) + assert deleted + except: + pass + @pytest.fixture(scope="module") def table_basic_pay_per_request(): resource_name = random_suffix_name("table-basic-pay-per-request", 32) @@ -153,6 +165,9 @@ def table_basic_pay_per_request(): class TestTable: def table_exists(self, table_name: str) -> bool: return table.get(table_name) is not None + + def table_insight_status(self, table_name: str, status: str) -> bool: + return table.get(table_name) is not None def test_create_delete(self, table_lsi): (ref, res) = table_lsi @@ -452,6 +467,33 @@ def test_update_provisioned_throughput(self, table_lsi): timeout_seconds=MODIFY_WAIT_AFTER_SECONDS, interval_seconds=3, ) + + def test_update_insights(self, table_insights): + (ref, res) = table_insights + + table_name = res["spec"]["tableName"] + assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "True", wait_periods=5) + + cr = k8s.get_resource(ref) + + # Check DynamoDB Table exists + assert self.table_exists(table_name) + assert cr['spec']['contributorInsights'] == "ENABLE" + assert self.table_insight_status(table_name, "ENABLED") + + # Set provisionedThroughput + updates = { + "spec": { + "contributorInsights": "DISABLE" + } + } + # Patch k8s resource + k8s.patch_custom_resource(ref, updates) + assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "True", wait_periods=5) + cr = k8s.get_resource(ref) + assert cr['spec']['contributorInsights'] == "DISABLE" + assert self.table_insight_status(table_name, "DISABLED") + def test_enable_sse_specification(self, table_lsi): (ref, res) = table_lsi