Skip to content

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

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

Add mutually_exclusive_identifiers config for resources with no single primary key#736
knottnt wants to merge 4 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:
Some AWS resources have no single mandatory identifier and are instead identified by exactly one of several mutually-exclusive fields (e.g. CloudWatch Logs ResourcePolicy: account-scoped by PolicyName, resource-scoped by ResourceArn). Today the generated adoption code requires one primary key, which blocks adopting the alternately-keyed variant.

This adds a mutually_exclusive_identifiers resource config listing those fields. From that single declaration the generator now emits:

  1. PopulateResourceFromAnnotation — an exactly-one guard that returns a terminal error unless exactly one identifier is present, and populates whichever the user supplied. This closes the mis-adoption hole where an empty or misspelled annotation silently adopted an arbitrary resource under adoptionPolicy: adopt.
  2. requiredFieldsMissingFromReadManyInput / ReadOne check — treats the read input as incomplete (so sdkFind returns NotFound) unless at least one identifier is set, replacing the per-service custom_check_required_fields_missing_method.

ValidateConfig rejects a single-element list or combining with is_arn_primary_key.

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

Previous Implementation
Previous iteration of this PR used a is_primarykey_optional config that only relaxed the requirement that a single field considered the primary key exists and is provided. However, this approach was dropped as it didn't sufficiently provide users feedback when adoption-fields was misconfigured or empty.

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 Outdated
// 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 thread pkg/generate/code/set_resource.go Outdated
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 thread pkg/generate/code/set_resource.go Outdated
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.

…better enforcement of Adoption field requirement

- Replace is_primary_key_optional with mutually_exclusive_identifiers
- Replace optional primary key logic with mutually exclusive identifier check when generating PopulateResourceFromAnnotation
- Add cloudwatchlogs api model to testdata
@knottnt knottnt changed the title Add is_primary_key_optional config option for resources with no single primary key Add mutually_exclusive_identifiers config for resources with no single primary key Aug 27, 2026
@knottnt

knottnt commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

/test ec2-controller-test

@ack-prow

ack-prow Bot commented Aug 27, 2026

Copy link
Copy Markdown

@knottnt: The following tests failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
acm-controller-test 8111453 link true /test acm-controller-test
ec2-controller-test 8111453 link true /test ec2-controller-test

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@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.

Re-reviewed after the rename and the generated check. This addresses the earlier concern well — the exactly-one terminal error plus the ReadMany check gives two independent layers, and the config now fails validation for the obvious bad combinations. Old name and fixtures are fully cleaned up, build and unit tests are green.

Nothing here blocks. Three inline notes; the one on check.go is a two-line fix I would suggest taking before merge, since it is the only remaining path that fails open. The other two are follow-up material.

Comment on lines +1471 to +1481
if r.HasMutuallyExclusiveIdentifiers() {
identifierFields, meErr := r.GetMutuallyExclusiveIdentifierFields()
if meErr != nil {
return "", meErr
}
identifierKeys := make([]string, 0, len(identifierFields))
for _, identifierField := range identifierFields {
identifierKeys = append(identifierKeys, identifierField.Names.CamelLower)
}
out += mutuallyExclusiveIdentifierGuardConstructor(identifierKeys, sourceVarName, indentLevel)
}

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.

Nice — generating the check is the right call.

One issue: the guard's key list comes from the config, but the code that fills those keys in runs in the member loop over the read op's input shape, and the loop skips fields in several places the guard doesn't know about. So the guard can pass while nothing gets written.

Most likely case: IsPrimaryARNField is true for a field named <ResourceName>Arn (pkg/model/crd.go:403-415), and the loop skips those at line 1565 — exactly the "adopt by name or ARN" case this is for. I generated a config like that: the guard counted resourceARN, nothing populated it. Supplying only the ARN passes the guard, writes nothing, and then the ReadMany check reports incomplete, so adoption returns NotFound forever with nothing pointing at the config.

Same skip for: identifier not in the read op's input (or ignored out), a renames declared on only one op, is_secret, and non-string types — the "must be a scalar type" error comes after the type check at line 1554.

It fails safe, so the feature just stops working rather than adopting the wrong thing. Not a blocker for this PR, since ResourcePolicy isn't affected (ResourceArn doesn't match IsPrimaryARNField and is a member of DescribeResourcePoliciesInput). Worth fixing in one place though: build the key list from the fields the loop actually writes, or error at codegen if a configured identifier was never written.

Two smaller ones:

  • Line 1534 returns primaryKeyConditionalOut + arnOut, dropping out — the guard. Validation catches is_arn_primary_key but not this path, and mutually_exclusive_identifiers doesn't require is_primary_key.
  • The guard checks key presence, not value, so {"policyName": ""} passes and sets a non-nil empty pointer, which also passes the ReadMany check. identifierNameOrIDGuardConstructor checks == "".

Comment on lines +251 to +256
if r.HasMutuallyExclusiveIdentifiers() {
exclusiveConditions, _, err := mutuallyExclusiveIdentifierNilConditions(r, koVarName)
if err != nil {
return result
}
return fmt.Sprintf("%sreturn %s\n", indent, strings.Join(exclusiveConditions, " && "))

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.

This drops the error and returns return false — the behavior the comment above calls out as letting "sdkFind list every resource and match an arbitrary one." The ReadOne version returns the error.

A typo'd config name makes mutuallyExclusiveIdentifierNilConditions error (pkg/model/crd.go:485); I confirmed this then emits return false with no error at the call site. It doesn't reach a real build today only because the same typo also fails PopulateResourceFromAnnotation and aborts generation. That's accidental, and this is the function meant to close the hole.

Of everything I found this round, this is the only path that fails open, and it's the cheapest to fix — so it's the one I'd suggest doing before merge. Widening the signature to return an error would be best; return true would at least make the fallback safe.

Also, ReadOne parenthesizes the grouped condition and this doesn't — fine today, but this branch ignores shape, so a future || term here would break quietly.

Comment thread pkg/config/validate.go
Comment on lines +57 to +77
func validateMutuallyExclusiveIdentifiers(cfg *Config) []error {
var errs []error
for resName, resCfg := range cfg.Resources {
identifiers := resCfg.MutuallyExclusiveIdentifiers
if len(identifiers) == 0 {
continue
}
if len(identifiers) < 2 {
errs = append(errs, fmt.Errorf(
"resources.%s.mutually_exclusive_identifiers: must list at least two fields, got %d",
resName, len(identifiers),
))
}
if resCfg.IsARNPrimaryKey {
errs = append(errs, fmt.Errorf(
"resources.%s.mutually_exclusive_identifiers: cannot be combined with is_arn_primary_key",
resName,
))
}
}
return errs

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.

A few config rules worth adding here, none urgent:

  1. [PolicyName, policyName] passes len < 2, since both collapse to one field later. The guard then counts the same key twice, so adoption always fails. Check for two distinct fields after normalizing.
  2. Field names aren't verified. A typo fails generation only via PopulateResourceFromAnnotation, without the resources.<R>.mutually_exclusive_identifiers: context. ValidateConfig has no model, but pkg/model/model.go has a post-processFields block doing this for resources.<R>.fields.<F> — likely the right home.
  3. custom_check_required_fields_missing_method makes the read templates skip the generated check entirely. cloudwatchlogs-controller#76 sets it today, so migrating without removing it silently disables the new check. Worth rejecting together.
  4. Declaring identifiers the read op marks required (deliberate, per the comment in set_resource.go) turns the ReadOne check from || into &&. With only Type set it passes, GetSecurityPolicy gets Name=nil, and fails with a ValidationException instead of NotFound → create. The only test for that branch is the opensearchserverless fixture, which says itself that Name/Type aren't really exclusive. Either allow it with a real fixture, or reject it here.
  5. This option alone doesn't make adoption safe — each identifier also needs a read-request filter or a list_operation.match_fields entry, or the List is unfiltered and sdkFind takes the first result. Patch the correct observed state after a rm.ReadOne call #76 sets match_fields: [PolicyName]; the fixture here doesn't, and nothing warns.

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