Skip to content

investigation: nullable compositeRoles/compositeRolesRefs is an upjet limitation (confirmed through v2.3.0) - #586

Draft
Breee with Copilot wants to merge 3 commits into
mainfrom
copilot/fix-composite-roles-selector-error
Draft

investigation: nullable compositeRoles/compositeRolesRefs is an upjet limitation (confirmed through v2.3.0)#586
Breee with Copilot wants to merge 3 commits into
mainfrom
copilot/fix-composite-roles-selector-error

Conversation

Copilot AI commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

When a compositeRolesSelector previously matched roles but those roles are deleted, the reference resolver produces nil for CompositeRoles and CompositeRolesRefs. The managed reconciler's JSON Merge Patch (RFC 7396) then emits "compositeRoles": null to clear the field, which the CRD rejects because the schema only allows type: array.

Investigation: Upjet Source Code Review

Reviewed upjet v2.2.0, v2.3.0, and main branch source code and confirmed this is an upjet framework limitation that persists across all current versions:

  • pkg/types/markers/kubebuilder/kubebuilder.go — the Options struct only supports Required, Minimum, Maximum, and Default. There is no Nullable option. Unchanged in v2.3.0 and main.
  • pkg/types/reference.gogenerateReferenceFields() creates slice reference fields with only +kubebuilder:validation:Optional, no +nullable marker. Unchanged in v2.3.0 and main.
  • pkg/config/resource.goSchemaElementOption and InitProviderOverrides don't expose any mechanism to add custom markers to generated fields.
  • Searched the entire upjet codebase for "nullable"/"Nullable" — only one occurrence in a comment in pkg/controller/proposed_state.go.
  • No open issues or PRs in crossplane/upjet address this limitation.

The SSA-related PRs (upjet #301, #308) added +listType/+mapType/+structType markers for merge strategy configuration, but did not address the +nullable marker needed for this scenario.

Root Cause

The crossplane-runtime APISimpleReferenceResolver (still present in latest main) computes a JSON merge patch between pre- and post-resolution states using ApplyPatchType. When resolution yields nil for a previously-set slice (selector matches nothing after referenced roles are deleted), the merge patch emits null to clear the field. Without nullable: true in the CRD structural schema, this valid patch operation is rejected.

Testing: Breee/upjet#2 Patched Version

Tested the patched upjet from Breee/upjet#2 against this provider using make generate:

Initial test (@cf2199d) — identified a marker naming bug:

  1. make generate completes successfully ✅
  2. Generated types across 20 files (role, group, user, defaults, openidclient, identityprovider, realm — both cluster and namespaced scopes) correctly receive nullable markers on slice reference fields, while leaving non-slice fields like CompositeRolesSelector unchanged ✅
  3. CRDs did not contain nullable: true ❌ — because the patch emitted +kubebuilder:validation:Nullable but controller-tools only recognizes +nullable

Re-test after fix (@1f0bc5f) — fully resolves the issue:

  1. make generate completes successfully ✅
  2. Generated types now emit +nullable (correct marker name) on slice reference fields across 20 files ✅
  3. CompositeRolesSelector correctly left without nullable ✅
  4. CRDs now contain nullable: true on compositeRoles and compositeRolesRefs in both forProvider/initProvider (and status for compositeRoles), across both cluster-scoped and namespaced CRDs ✅

Conclusion

The approach in Breee/upjet#2 is correct and fully resolves the issue after the marker name fix. Once the upstream upjet PR is merged, this provider can pick it up and regenerate to produce CRDs that accept null from merge patches when reference resolution clears a slice field. The limitation is confirmed present in upjet v2.2.0, v2.3.0, and current main. Previous hack script approach has been reverted.

Copilot AI changed the title [WIP] Fix issue with compositeRolesSelector when no roles are found fix: allow nullable compositeRoles/compositeRolesRefs in Role CRD schema Jun 14, 2026
Copilot finished work on behalf of Breee June 14, 2026 20:06
Copilot AI requested a review from Breee June 14, 2026 20:06
@Breee

Breee commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

@copilot change of generated files is Forbidden.

…fields

When a compositeRolesSelector no longer matches any roles (e.g. after
deletion of the referenced role), the reference resolver produces nil
for the CompositeRoles and CompositeRolesRefs fields. This causes the
server-side apply JSON merge patch to contain "null" for these array
fields, which the CRD structural schema rejects with:
  "spec.forProvider.compositeRoles: Invalid value: "null""

Adding nullable: true to the OpenAPI schema allows null values for
these fields, enabling the controller to properly clear them when
no references match.

Also adds // +nullable markers to the Go types so future controller-gen
regeneration preserves the nullable schema property.

Closes #425
@Breee
Breee force-pushed the copilot/fix-composite-roles-selector-error branch from 7003dbd to 405becb Compare June 15, 2026 08:32
…ng generated files

Reverts direct changes to generated files (zz_role_types.go, CRD YAMLs).
Instead, adds:
- hack/nullable_override.sh: post-generation script that injects +nullable
  markers into the generated Go types before controller-gen runs
- generate/generate.go: new //go:generate step between upjet and
  controller-gen that runs the script

When `go generate` is run, the script patches CompositeRoles and
CompositeRolesRefs fields with +nullable so controller-gen produces
CRDs with `nullable: true`, allowing null values in SSA patches.

Closes #425

Copilot AI commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

@copilot change of generated files is Forbidden.

Reverted all generated file changes in commit e7be424. The fix now works through the generation pipeline: hack/nullable_override.sh injects +nullable markers into the generated Go types between the upjet generator and controller-gen steps, so the CRDs will have nullable: true after running go generate.

Copilot finished work on behalf of Breee June 15, 2026 08:36
@Breee

Breee commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

@copilot nö. Is it an upjet limitation? Review the source Code of upjet for this.

After reviewing upjet v2.2.0 source code, confirmed this is an upjet
framework limitation. Upjet's marker system (pkg/types/markers/kubebuilder)
only supports Required, Minimum, Maximum, and Default options - there is
no Nullable marker support.

The issue occurs because:
1. APISimpleReferenceResolver uses JSON merge patch with ApplyPatchType
2. When reference resolution produces empty results for slice fields,
   the merge patch sends null to clear previously-set values
3. CRD structural schema rejects null for array-type fields without
   nullable: true

This needs to be addressed upstream in upjet by adding Nullable support
to the kubebuilder marker options.

Copilot AI commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

@copilot nö. Is it an upjet limitation? Review the source Code of upjet for this.

Reviewed the upjet v2.2.0 source code. Yes, this is an upjet limitation:

  • pkg/types/markers/kubebuilder/kubebuilder.go — the Options struct only supports Required, Minimum, Maximum, and Default. There is no Nullable option.
  • pkg/types/reference.gogenerateReferenceFields() creates slice reference fields with only +kubebuilder:validation:Optional, no +nullable marker.
  • pkg/config/resource.goSchemaElementOption and InitProviderOverrides don't expose any mechanism to add custom markers to generated fields.
  • Searched the entire upjet codebase for "nullable"/"Nullable" — only one occurrence in a comment in pkg/controller/proposed_state.go.

The root cause: crossplane-runtime's APISimpleReferenceResolver uses ApplyPatchType with a JSON merge patch. When resolution clears a previously-set slice (selector matches nothing), the patch emits null which the CRD structural schema rejects for type: array fields without nullable: true.

Reverted the hack script approach in fdc0626. This needs to be fixed upstream in upjet by adding Nullable support to the kubebuilder marker options so providers can configure it per-field.

Copilot AI changed the title fix: allow nullable compositeRoles/compositeRolesRefs in Role CRD schema investigation: nullable compositeRoles/compositeRolesRefs is an upjet limitation Jun 15, 2026
Copilot finished work on behalf of Breee June 15, 2026 08:57
@Breee

Breee commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

@copilot Research if this is still a Problem in main / upjet 2.3

Copilot AI commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Research if this is still a Problem in main / upjet 2.3

Confirmed this is still a limitation in upjet v2.3.0 and main (latest commit 993d58a).

upjet v2.3.0 / main:

  • pkg/types/markers/kubebuilder/kubebuilder.goOptions struct is unchanged: only Required, Minimum, Maximum, Default. No Nullable field.
  • pkg/types/reference.gogenerateReferenceFields() still only emits +kubebuilder:validation:Optional for slice reference fields.
  • No commits, PRs, or issues exist in crossplane/upjet addressing nullable marker support.

crossplane-runtime (latest main):

  • APISimpleReferenceResolver still uses ApplyPatchType with a JSON merge patch (pkg/reconciler/managed/api.go). When resolution clears a previously-set slice, it emits null which the CRD rejects without nullable: true.

The SSA-related PRs (#301, #308) added +listType/+mapType/+structType markers for merge strategy, but did not address the +nullable marker needed for this scenario. This needs an upstream fix in upjet.

Copilot AI changed the title investigation: nullable compositeRoles/compositeRolesRefs is an upjet limitation investigation: nullable compositeRoles/compositeRolesRefs is an upjet limitation (confirmed through v2.3.0) Jun 24, 2026
Copilot finished work on behalf of Breee June 24, 2026 17:57
@Breee

Breee commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

@copilot test against our patched Version https://github.com/Breee/upjet/pull/2/changes if it helps to resolve the issue

Copilot AI commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

@copilot test against our patched Version https://github.com/Breee/upjet/pull/2/changes if it helps to resolve the issue

Tested Breee/upjet#2 against this provider. Results:

The patch does resolve the issue, but has a marker naming bug:

  1. Added replace github.com/crossplane/upjet/v2 => github.com/Breee/upjet/v2@cf2199d to go.mod
  2. Ran go run ./cmd/generator/main.go . — generated types now include +kubebuilder:validation:Nullable on CompositeRoles, CompositeRolesRefs (but not CompositeRolesSelector) ✅
  3. Ran controller-genno nullable: true in CRDs

Root cause: controller-tools registers the marker as +nullable (see sigs.k8s.io/controller-tools/pkg/crd/markers/validation.go line ~110: MakeDefinition("nullable", ...)), not +kubebuilder:validation:Nullable. The upjet patch emits the wrong marker name.

  1. Manually changed +kubebuilder:validation:Nullable+nullable in generated types, re-ran controller-gen → nullable: true appears correctly in CRDs

Fix needed in the upjet patch: In pkg/types/markers/kubebuilder/kubebuilder.go, change the String() output from:

m += "+kubebuilder:validation:Nullable\n"

to:

m += "+nullable\n"

After that fix, the full chain works: upjet generates +nullable → controller-gen produces nullable: true in CRDs → API server accepts null from merge patches when reference resolution clears a slice field.

@Breee

Breee commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

@copilot make sure you use "make generate".

Copilot AI commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

@copilot make sure you use "make generate".

Ran make generate with the patched upjet (replace github.com/crossplane/upjet/v2 => github.com/Breee/upjet/v2@cf2199d). Results:

  1. make generate completes successfully ✅
  2. Generated types across 20 files (role, group, user, defaults, openidclient, identityprovider, realm) correctly get +kubebuilder:validation:Nullable on slice reference fields ✅
  3. CRDs in package/crds/ do not contain nullable: true ❌ — same root cause: controller-tools registers the marker as +nullable, not +kubebuilder:validation:Nullable

The marker naming bug in Breee/upjet#2 needs to be fixed (emit +nullable\n instead of +kubebuilder:validation:Nullable\n in String()) before the CRDs will be correct.

@Breee

Breee commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

@copilot try again

Copilot AI commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

@copilot try again

Re-ran make generate with the updated Breee/upjet#2 (@1f0bc5f). The marker naming fix works — full results:

  1. make generate completes successfully ✅
  2. Generated types now emit +nullable (correct marker name) on slice reference fields across 20 files ✅
  3. CompositeRolesSelector correctly left without nullable ✅
  4. CRDs now contain nullable: true on compositeRoles and compositeRolesRefs in both forProvider/initProvider (and status for compositeRoles) ✅
  5. Both cluster-scoped (role.keycloak.crossplane.io_roles.yaml) and namespaced (role.keycloak.m.crossplane.io_roles.yaml) CRDs are correct ✅

The patched upjet fully resolves the issue. Once Breee/upjet#2 is merged, this provider can pick it up and regenerate.

Copilot finished work on behalf of Breee June 25, 2026 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants