Skip to content

Add is_primary_key_optional config option for resources with no single primary key - #736

Open
knottnt wants to merge 2 commits into
aws-controllers-k8s:mainfrom
knottnt:feat/allow-adoption-with-no-primary-key
Open

Add is_primary_key_optional config option for resources with no single primary key#736
knottnt wants to merge 2 commits into
aws-controllers-k8s:mainfrom
knottnt:feat/allow-adoption-with-no-primary-key

Conversation

@knottnt

@knottnt knottnt commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Issue #, if available:

Description of changes:
In some cases an AWS resource may have multiple potential identifiers with no single one serving as the primary. Currently out generate code for PopulateResourceFromAnnotation requires at least one field be identified as a primary key and will throw an error during adoption if it is not provided. For such resources this will either add an unnecessary requirement or actually prevent adoption if they identifiers are mutually exclusive.

The motivating resource for this feature is ResourcePolicy for cloudwatch logs. This resource can identified by two mutually exclusive primary keys (policyName for account-wide and resourceARN for resource scoped). See aws-controllers-k8s/cloudwatchlogs-controller#76

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@gustavodiaz7722

Copy link
Copy Markdown
Member

/retest

1 similar comment
@gustavodiaz7722

Copy link
Copy Markdown
Member

/retest

Comment thread pkg/config/resource.go
// mutually-exclusive fields (for example, a policy keyed by name OR by
// resource ARN) so adoption succeeds with whichever field(s) the user
// supplies.
IsPrimaryKeyOptional bool `json:"is_primary_key_optional"`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: is_primary_key_optional: true is a silent no-op unless a field is also marked is_primary_key: true

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hmm that's a good call. This could also be applied to auto-discovered primary fields.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — addressed. is_primary_key_optional now also applies to auto-discovered primary keys, not just fields explicitly marked is_primary_key: true. The optional if ok guard is emitted in the FindPrimaryIdentifierFieldNames path too, so the flag is no longer a silent no-op when is_primary_key is unset. (It still has no effect for ARN primary keys, which have no alternate identifier to fall back to — noted in a code comment.) Added a dedicated test + testdata config for the auto-discovered case.

@gustavodiaz7722

Copy link
Copy Markdown
Member

Removing the required-field guard leaves nothing asserting that any identifier was supplied. PopulateResourceFromAnnotation returns nil having populated nothing, and on adoptionPolicy: adopt that does not fail — it adopts an arbitrary unrelated resource and reports success.

Observed

cloudwatchlogs-controller#76 built from source and deployed to an EKS cluster, with three ResourcePolicy CRs differing only in adoption-fields. The account held three account-scoped policies; DescribeResourcePolicies returned them in a stable order across repeated calls, with the AWS-managed AWSLogDeliveryWrite20150319 first:

$ aws logs describe-resource-policies --query 'resourcePolicies[].policyName' --output text
AWSLogDeliveryWrite20150319	ack736-app-log-policy	ack736-audit-log-policy

Every CR below intends to adopt ack736-app-log-policy:

NAME               ADOPTED_POLICY                SYNCED   FINALIZERS
control-correct    ack736-app-log-policy         True     [finalizers.cloudwatchlogs.services.k8s.aws/ResourcePolicy]
empty-annotation   AWSLogDeliveryWrite20150319   True     [finalizers.cloudwatchlogs.services.k8s.aws/ResourcePolicy]
misspelled-key     AWSLogDeliveryWrite20150319   True     [finalizers.cloudwatchlogs.services.k8s.aws/ResourcePolicy]
  • control-correct'{"policyName": "ack736-app-log-policy"}' — adopts the intended policy, so adoption works correctly whenever an identifier is present.
  • empty-annotation'{}'ResourceSynced=True, no error, bound to the AWS-managed policy.
  • misspelled-key'{"policyname": "ack736-app-log-policy"}' (lowercase n, so the key is never read) — identical outcome.

Both failing CRs had the ACK finalizer attached and the AWS-managed policy's full 10-statement document copied into their spec — statements governing live log delivery for unrelated workloads in the account:

$ kubectl get resourcepolicy empty-annotation -o jsonpath='{.spec.policyDocument}' | jq '.Statement | length'
10
  - test-c01-SummaryWorkflowExpressLogGroup:log-stream:*
  - fluent-bitEKSAddonRelease-AddonReleaseStateMachineLogGroup0D1FCFFD-9sz...
  - /aws/vendedlogs/states/waiter-state-machine-bristol-devstack-test-pdx-...

Note also that two CRs were simultaneously bound to the same AWS policy, each believing it owned it.

Why it does not fail safely

The generated requiredFieldsMissingFromReadManyInput is a constant return false (DescribeResourcePolicies has no required members), so the early NotFound bailout in sdkFind cannot fire and the list call returns unfiltered. The match_fields comparison is then nil-guarded:

if elem.PolicyName != nil {
	if ko.Spec.PolicyName != nil {          // nil -> comparison skipped entirely
		if *elem.PolicyName != *ko.Spec.PolicyName {
			continue
		}
	}
	ko.Spec.PolicyName = elem.PolicyName
}

With Spec.PolicyName nil the loop takes the first element and breaks. Resources whose read op has required members fail safely here as AdoptedResourceNotFound — but is_primary_key_optional exists for resources with no mandatory identifier, which are exactly the ones that cannot.

This is specific to adopt. Under adopt-or-create, reconciler.go:641-643 keeps only the populated status and discards the spec, so the annotation cannot misdirect the lookup there. Worth noting too that since the primary key is auto-discovered, is_primary_key_optional: true alone is enough to reach this — no is_primary_key needed.

Deletion

The controller ran with the default DELETION_POLICY=delete. Deleting a correctly-adopted CR deletes the underlying AWS policy:

BEFORE: AWSLogDeliveryWrite20150319	ack736-app-log-policy	ack736-audit-log-policy
$ kubectl delete resourcepolicy control-correct
AFTER:  AWSLogDeliveryWrite20150319	ack736-audit-log-policy

Combined with the mis-adoption above, deleting a mis-adopted CR would delete the AWS-managed log delivery policy. That last step was deliberately not run — the two mis-adopted CRs were annotated services.k8s.aws/deletion-policy: retain before removal, so the policy survived (AWS resource will not be deleted - deletion policy set to retain). Each link was verified independently rather than chained on a live resource.

@knottnt

knottnt commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Removing the required-field guard leaves nothing asserting that any identifier was supplied. PopulateResourceFromAnnotation returns nil having populated nothing, and on adoptionPolicy: adopt that does not fail — it adopts an arbitrary unrelated resource and reports success.

Observed

cloudwatchlogs-controller#76 built from source and deployed to an EKS cluster, with three ResourcePolicy CRs differing only in adoption-fields. The account held three account-scoped policies; DescribeResourcePolicies returned them in a stable order across repeated calls, with the AWS-managed AWSLogDeliveryWrite20150319 first:

$ aws logs describe-resource-policies --query 'resourcePolicies[].policyName' --output text
AWSLogDeliveryWrite20150319	ack736-app-log-policy	ack736-audit-log-policy

Every CR below intends to adopt ack736-app-log-policy:

NAME               ADOPTED_POLICY                SYNCED   FINALIZERS
control-correct    ack736-app-log-policy         True     [finalizers.cloudwatchlogs.services.k8s.aws/ResourcePolicy]
empty-annotation   AWSLogDeliveryWrite20150319   True     [finalizers.cloudwatchlogs.services.k8s.aws/ResourcePolicy]
misspelled-key     AWSLogDeliveryWrite20150319   True     [finalizers.cloudwatchlogs.services.k8s.aws/ResourcePolicy]
* `control-correct` — `'{"policyName": "ack736-app-log-policy"}'` — adopts the intended policy, so adoption works correctly whenever an identifier is present.

* `empty-annotation` — `'{}'` — `ResourceSynced=True`, no error, bound to the AWS-managed policy.

* `misspelled-key` — `'{"policyname": "ack736-app-log-policy"}'` (lowercase `n`, so the key is never read) — identical outcome.

Both failing CRs had the ACK finalizer attached and the AWS-managed policy's full 10-statement document copied into their spec — statements governing live log delivery for unrelated workloads in the account:

$ kubectl get resourcepolicy empty-annotation -o jsonpath='{.spec.policyDocument}' | jq '.Statement | length'
10
  - test-c01-SummaryWorkflowExpressLogGroup:log-stream:*
  - fluent-bitEKSAddonRelease-AddonReleaseStateMachineLogGroup0D1FCFFD-9sz...
  - /aws/vendedlogs/states/waiter-state-machine-bristol-devstack-test-pdx-...

Note also that two CRs were simultaneously bound to the same AWS policy, each believing it owned it.

Why it does not fail safely

The generated requiredFieldsMissingFromReadManyInput is a constant return false (DescribeResourcePolicies has no required members), so the early NotFound bailout in sdkFind cannot fire and the list call returns unfiltered. The match_fields comparison is then nil-guarded:

if elem.PolicyName != nil {
	if ko.Spec.PolicyName != nil {          // nil -> comparison skipped entirely
		if *elem.PolicyName != *ko.Spec.PolicyName {
			continue
		}
	}
	ko.Spec.PolicyName = elem.PolicyName
}

With Spec.PolicyName nil the loop takes the first element and breaks. Resources whose read op has required members fail safely here as AdoptedResourceNotFound — but is_primary_key_optional exists for resources with no mandatory identifier, which are exactly the ones that cannot.

This is specific to adopt. Under adopt-or-create, reconciler.go:641-643 keeps only the populated status and discards the spec, so the annotation cannot misdirect the lookup there. Worth noting too that since the primary key is auto-discovered, is_primary_key_optional: true alone is enough to reach this — no is_primary_key needed.

Deletion

The controller ran with the default DELETION_POLICY=delete. Deleting a correctly-adopted CR deletes the underlying AWS policy:

BEFORE: AWSLogDeliveryWrite20150319	ack736-app-log-policy	ack736-audit-log-policy
$ kubectl delete resourcepolicy control-correct
AFTER:  AWSLogDeliveryWrite20150319	ack736-audit-log-policy

Combined with the mis-adoption above, deleting a mis-adopted CR would delete the AWS-managed log delivery policy. That last step was deliberately not run — the two mis-adopted CRs were annotated services.k8s.aws/deletion-policy: retain before removal, so the policy survived (AWS resource will not be deleted - deletion policy set to retain). Each link was verified independently rather than chained on a live resource.

@gustavodiaz7722 This is a good catch. Will need to test this, but I think what might be happening is when neither PolicyName or ResourceARN are set the DescribeResourcePolicies API returns with the default ACCOUNT policy scope. To get around this I believe can add a check to sdkFind to ensure that at least on of PolicyName or ResourceARN are set and return a Terminal error if that is violated.

@gustavodiaz7722

Copy link
Copy Markdown
Member

/approve

@ack-prow

ack-prow Bot commented Aug 26, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: gustavodiaz7722, knottnt

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@sapphirew sapphirew left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the adoption path. One substantive concern, on the case where the annotation supplies neither identifier. Details inline.

Verified along the way that the premise holds: PutResourcePolicy rejects policyName + resourceArn together, and a resource-scoped policy carries no policyName, so the current hard requirement really does block adoption of resource-scoped policies.

Comment on lines +1450 to +1460
if r.IsPrimaryKeyOptional() {
// The primary key is optional for adoption: set it when the
// annotation supplies it, but do not require it.
primaryKeyOut += optionalFieldGuardConstructor("primaryKey", sourceVarName, primaryField.Names.CamelLower, indentLevel)
primaryKeyOut += setResourceIdentifierPrimaryIdentifierAnn(
"&primaryKey",
primaryField,
targetVarPath,
indentLevel+1,
)
primaryKeyOut += fmt.Sprintf("%s}\n", indent)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The feature is well motivated. I checked against the API and both halves hold up: a resource-scoped policy has no policyName at all (PutResourcePolicy with only resourceArn returns policyScope: RESOURCE with no name field), and passing both is rejected outright:

InvalidParameterException: Both policy name and resource arn cannot be specified at the same time.

So requiring policyName in the annotation really does block adoption of resource-scoped policies today. Worth noting that neither constraint appears in the API docs, which mark both fields Required: No.

My concern is the case where the annotation supplies neither identifier — an empty annotation, or a misspelled key like policyname. With adoption-policy: adopt that doesn't fail. It succeeds against an arbitrary resource:

  1. PopulateResourceFromAnnotation sets nothing and returns nil. handlePopulation only stops on an error, so the runtime treats this as success (runtime v0.62.0 reconciler.go:511-518).
  2. resolved = populated (reconciler.go:634-647), so the spec is now empty.
  3. sdkFind calls DescribeResourcePolicies, which has no policyName filter, so with an empty spec there is nothing to filter on and it lists everything.
  4. The generated matcher only compares a field when the spec value is non-nil. Nothing gets compared, so found is set on the first policy returned.

setResourceManagedAndAdopted then adds the finalizer and adopted: true, and the CR owns that policy. Nothing looks wrong. With deletionPolicy: delete, deleting the CR deletes a policy the user never meant to manage.

Before this change the if !ok { return terminal } guard made step 1 impossible. That's the property I'd like to keep.

cloudwatchlogs-controller#76 does handle this via customCheckRequiredFieldsMissing, but that makes sdkFind return NotFound. It stops the mis-adoption, but the user gets an indefinite requeue instead of being told an identifier is missing, so the actionable terminal error from #613 is gone either way. And nothing in code-generator requires that pairing, so the next service to set this flag gets the mis-adoption with no protection at all.

Could we generate the check instead? Since the two fields are mutually exclusive, the real constraint is exactly one:

policyName, hasPolicyName := fields["policyName"]
resourceARN, hasResourceARN := fields["resourceARN"]
if hasPolicyName == hasResourceARN {
    return ackerrors.NewTerminalError(fmt.Errorf(
        "adoption requires exactly one of: policyName, resourceARN"))
}

A len(fields) == 0 check would not be enough, since a misspelled key still gives a non-empty map. To emit the above, the generator needs to know which fields are the alternatives, and a resource-level bool can't tell it. Something like mutually_exclusive_identifiers: [PolicyName, ResourceARN] would, and it describes the resource more accurately than "the primary key is optional."

If you'd rather keep the check in the controller, that's reasonable, but then ValidateConfig should reject is_primary_key_optional when custom_check_required_fields_missing_method is absent, so it isn't left to each service to remember.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hmm, I think this is reasonable. I can look into implementing a mutually_exclusive_identifiers to provide a better generated adoption check. Might be able to generate the required_field_missing check from this as well.

Comment on lines +1548 to +1561
if isPrimaryIdentifier && r.IsPrimaryKeyOptional() {
// The auto-discovered primary key is optional for adoption: set
// it when the annotation supplies it, but do not require it.
// This mirrors the explicit is_primary_key handling above.
// (Note: is_primary_key_optional has no effect for ARN primary
// keys, which return early and always require the ARN.)
primaryKeyOut += optionalFieldGuardConstructor(requiredFieldVarName, sourceVarName, targetField.Names.CamelLower, indentLevel)
primaryKeyOut += setResourceIdentifierPrimaryIdentifierAnn(
fmt.Sprintf("&%s", requiredFieldVarName),
targetField,
sourceVarPath,
indentLevel+1,
)
primaryKeyOut += fmt.Sprintf("%s}\n", indent)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same request as above for this branch — whatever check we add should cover the auto-discovered case too.

One thing specific to here: the condition only looks at isPrimaryIdentifier and ignores the inputShape.IsRequired(memberName) half of the if on line 1546. So when a field is both the primary identifier and marked required by the read operation's input shape, the guard is dropped for a field the API cannot work without.

The new test is that case. name is required on DescribeClusterRequest in pkg/testdata/codegen/sdk-codegen/aws-models/eks.json, so TestSetResource_EKS_Cluster_OptionalAutoDiscoveredPrimaryKey_PopulateResourceFromAnnotation asserts the relaxed behavior for an identifier that isn't really optional.

I don't think this branch has to change if we add the "exactly one identifier" check, since that would catch the empty case first. But it's worth deciding whether the flag should apply to fields the read op marks required — if not, adding !inputShape.IsRequired(memberName) here would say so. Either way, a test against a resource whose identifiers are genuinely optional would show the intent better; DescribeResourcePolicies has no required members at all, which is exactly the shape this flag is for.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants