Skip to content

fix(tfjson): Reset Computed flag for SchemaNestingModeSingle blocks with required children - #593

Open
rwwiv wants to merge 1 commit into
crossplane:mainfrom
rwwiv:fix-computed-single-blocks
Open

fix(tfjson): Reset Computed flag for SchemaNestingModeSingle blocks with required children#593
rwwiv wants to merge 1 commit into
crossplane:mainfrom
rwwiv:fix-computed-single-blocks

Conversation

@rwwiv

@rwwiv rwwiv commented Jan 29, 2026

Copy link
Copy Markdown

Summary

Fixes two issues where SchemaNestingModeSingle blocks from Plugin Framework resources were incorrectly handled:

  1. Computed inference bug: Blocks were incorrectly marked as Computed=true, causing them to be excluded from ForProvider/InitProvider parameters and only appear in Observation.

  2. Type representation bug: Blocks were using TypeList instead of SchemaTypeObject, causing the generated CRD schema to expect arrays instead of objects.

Problem

In tfJSONBlockTypeToV2Schema(), when MinItems==0 && MaxItems==0, the block was being marked as Computed=true:

if nb.MinItems == 0 && nb.MaxItems == 0 {
    v2sch.Computed = true
}

This heuristic works for SDK v2 collection types (List/Set/Map) where MinItems=0, MaxItems=0 typically indicates a computed-only collection. However, Plugin Framework resources using SchemaNestingModeSingle default to MinItems=0, MaxItems=0 even for user-configurable blocks.

Additionally, SchemaNestingModeSingle blocks were using schemav2.TypeList, but according to Terraform Plugin Framework documentation, SingleNestedBlock values should be represented by an object type, not a list.

This caused blocks like metadata and spec to be:

  • Marked Computed=true, which makes IsObservation() return true, excluding them from the generated Parameters struct
  • Generated as arrays in the CRD schema, when they should be objects

Solution

  1. Move the Computed=true inference to only apply to collection nesting modes (Set/List/Map), not to SchemaNestingModeSingle. For single blocks, we now rely solely on hasRequiredChild() to determine Required/Optional, leaving Computed=false by default.

  2. Use SchemaTypeObject instead of TypeList for SchemaNestingModeSingle blocks, matching the behavior for nested attributes with SchemaNestingModeSingle in tfJSONNestedAttributeTypeToV2Schema().

case tfjson.SchemaNestingModeSingle, tfjson.SchemaNestingModeGroup:
    // For SchemaNestingModeSingle (Plugin Framework), we do NOT infer
    // Computed=true from MinItems/MaxItems==0, because Plugin Framework
    // resources default to 0 for these values even for user-configurable blocks.
    // Use SchemaTypeObject to generate an embedded object (not a list),
    // matching the behavior for nested attributes with SchemaNestingModeSingle
    // and the Terraform Plugin Framework documentation which states that
    // SingleNestedBlock values are represented by an object type.
    v2sch.Type = SchemaTypeObject
    v2sch.Required = hasRequiredChild(nb)
    v2sch.Optional = !v2sch.Required
    // Computed remains false (the default)

Testing

Added comprehensive unit tests for tfJSONBlockTypeToV2Schema and hasRequiredChild:

  • SchemaNestingModeSingle with required children → Computed=false, Required=true, Type=SchemaTypeObject
  • SchemaNestingModeSingle with only optional children → Computed=false, Optional=true, Type=SchemaTypeObject
  • SchemaNestingModeSingle with empty/nil block
  • SchemaNestingModeSingle with nested blocks containing required children
  • SchemaNestingModeList/Set/Map with MinItems=0, MaxItems=0Computed=true (preserves existing behavior)
  • SchemaNestingModeList with MinItems=1Computed=false

Impact

This fix is particularly important for Terraform Plugin Framework resources. For example, the Grafana provider's App Platform resources (grafana_apps_rules_alertrule_v0alpha1, grafana_apps_rules_recordingrule_v0alpha1, etc.) have metadata and spec blocks that were incorrectly excluded from forProvider in the generated CRDs, and were being generated as arrays instead of objects.

Before this fix:

spec:
  forProvider:
    options: {}  # Only options appeared, metadata/spec excluded
    # If they did appear, they would be arrays: metadata: [{}]

After this fix:

spec:
  forProvider:
    metadata:        # Now an object, not an array
      uid: "..."
      folderUid: "..."
    spec:            # Now an object, not an array
      title: "..."
      expressions: {}
      # ... all other fields
    options: {}

SDK v2-based resources are unaffected because they typically use MinItems=1, MaxItems=1 for required single blocks, so they never hit the MinItems == 0 && MaxItems == 0 path.

@coderabbitai

coderabbitai Bot commented Jan 29, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

tfJSONBlockTypeToV2Schema now applies SDK v2 semantics: collection modes (Set/List/Map) infer Computed when MinItems==0 && MaxItems==0; Single/Group blocks are represented as object-like with MinItems=0, MaxItems=1, Required/Optional derived from hasRequiredChild instead of inferring Computed.

Changes

Cohort / File(s) Summary
Schema conversion logic
pkg/types/conversion/tfjson/tfjson.go
Refactors tfJSONBlockTypeToV2Schema: collection nesting (Set/List/Map) infer Computed only when MinItems==0 && MaxItems==0; Single/Group no longer infer Computed from min/max, set a schema object type, MinItems=0, MaxItems=1, and compute Required/Optional via hasRequiredChild. Adds explanatory comments.
Tests for conversion and required-child detection
pkg/types/conversion/tfjson/tfjson_test.go
Adds TestTfJSONBlockTypeToV2Schema and TestHasRequiredChild table-driven tests covering Single/Group and collection mappings, MinItems/MaxItems/Computed interactions, nested required attributes, nil/empty handling, and detection of required children across nested hierarchies.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Thanks — quick question: should downstream consumers expect Single/Group to be treated strictly as object-schema (not list) for any subsequent processing?

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title check ⚠️ Warning Title exceeds the 72-character limit at 90 characters but accurately describes the main fix for SchemaNestingModeSingle Computed flag handling. Reduce title to under 72 characters while retaining the core message, for example: 'fix(tfjson): Fix Computed flag for SchemaNestingModeSingle blocks'
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed Description thoroughly explains the bugs, root causes, solutions implemented, and testing approach, directly aligned with the changeset modifications.
Configuration Api Breaking Changes ✅ Passed The pkg/config/** directory contains zero modifications, additions, or deletions in this pull request. All changes are confined to pkg/types/conversion/tfjson/.
Generated Code Manual Edits ✅ Passed PR modifications are limited to legitimate source files with no auto-generated artifacts matching restricted patterns.
Template Breaking Changes ✅ Passed Git verification confirms no modifications to any pkg/controller/external*.go files in this PR. Changes are confined to pkg/types/conversion/tfjson/ package for schema conversion logic only.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/types/conversion/tfjson/tfjson.go (1)

170-179: Add regression test for required-child Single blocks.

The behavior introduced around Line 174 should be locked in with a unit test to prevent regressions (Computed must be false when a Single block has required children).

🧪 Suggested test (standard Go testing)
+// SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io>
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package tfjson
+
+import (
+	"testing"
+
+	tfjsonsdk "github.com/hashicorp/terraform-json"
+	"github.com/zclconf/go-cty/cty"
+)
+
+func TestTFJSONBlockTypeToV2Schema_SingleRequiredChildResetsComputed(t *testing.T) {
+	nb := &tfjsonsdk.SchemaBlockType{
+		NestingMode: tfjsonsdk.SchemaNestingModeSingle,
+		MinItems:    0,
+		MaxItems:    0,
+		Block: &tfjsonsdk.SchemaBlock{
+			Attributes: map[string]*tfjsonsdk.SchemaAttribute{
+				"name": {Required: true, AttributeType: cty.String},
+			},
+		},
+	}
+
+	got := tfJSONBlockTypeToV2Schema(nb)
+
+	if got.Computed {
+		t.Fatalf("expected Computed=false for required-child single block")
+	}
+	if !got.Required || got.Optional {
+		t.Fatalf("expected Required=true and Optional=false")
+	}
+	if got.MinItems != 1 || got.MaxItems != 1 {
+		t.Fatalf("expected MinItems/MaxItems=1, got %d/%d", got.MinItems, got.MaxItems)
+	}
+}
+```
</details>
As per coding guidelines, "All Upjet code must be covered by tests; do not use Ginkgo or third-party testing libraries, use only standard Go testing".

</blockquote></details>

</blockquote></details>

@rwwiv
rwwiv force-pushed the fix-computed-single-blocks branch 3 times, most recently from ea49ff6 to 5bea688 Compare January 30, 2026 15:40

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/types/conversion/tfjson/tfjson.go (1)

176-191: ⚠️ Potential issue | 🟠 Major

The heuristic Required = hasRequiredChild(nb) is incorrect for Plugin Framework semantics and will over-constrain the CRD.

In Terraform Plugin Framework, SingleNestedBlock and SingleNestedAttribute (Group) have no built-in Required flag on the block itself—blocks are optional by default. Required child attributes do not imply the block is required; they only apply when the block is present. To make a block required in Plugin Framework, you must use validators (e.g., objectvalidator.IsRequired()).

The current code marks a block as Required whenever it contains required children, which incorrectly forces optional blocks to be required in the generated schema. This breaks configurations where the block is legitimately optional.

Fix: Determine block Required/Optional status from the tfjson schema itself (if available), not from child attribute requirements. If tfjson does not encode block-level Required, consider whether the absence of a Required indicator in the Plugin Framework schema should default to Optional = true.

Duologic added a commit to grafana/crossplane-provider-grafana that referenced this pull request Feb 3, 2026
This PR temporarily replaces upjet with the version from this PR:
crossplane/upjet#593 to generate these resources
properly.
Duologic added a commit to grafana/crossplane-provider-grafana that referenced this pull request Feb 3, 2026
This PR temporarily replaces upjet with the version from this PR:
crossplane/upjet#593 to generate these resources
properly.
rwwiv added a commit to grafana/crossplane-provider-grafana that referenced this pull request Feb 12, 2026
Replace upstream upjet with fork that fixes two issues for Plugin
Framework resources using SchemaNestingModeSingle blocks:

1. Blocks were incorrectly marked as Computed=true, causing them to be
   excluded from ForProvider/InitProvider and only appear in Observation.

2. Blocks were using TypeList instead of SchemaTypeObject, causing the
   generated CRD schema to expect arrays instead of objects.

This fix is required for Plugin Framework resources like alertrule and
recordingrule where metadata and spec blocks need to be objects, not arrays.

Upstream PR: crossplane/upjet#593
@rwwiv
rwwiv force-pushed the fix-computed-single-blocks branch from 72c6d00 to fb6b3fa Compare February 12, 2026 21:01
moustafab pushed a commit to grafana/crossplane-provider-grafana that referenced this pull request Feb 12, 2026
Replace upstream upjet with fork that fixes two issues for Plugin
Framework resources using SchemaNestingModeSingle blocks:

1. Blocks were incorrectly marked as Computed=true, causing them to be
   excluded from ForProvider/InitProvider and only appear in Observation.

2. Blocks were using TypeList instead of SchemaTypeObject, causing the
   generated CRD schema to expect arrays instead of objects.

This fix is required for Plugin Framework resources like alertrule and
recordingrule where metadata and spec blocks need to be objects, not arrays.

Upstream PR: crossplane/upjet#593
Duologic pushed a commit to grafana/crossplane-provider-grafana that referenced this pull request Feb 13, 2026
Replace upstream upjet with fork that fixes two issues for Plugin
Framework resources using SchemaNestingModeSingle blocks:

1. Blocks were incorrectly marked as Computed=true, causing them to be
   excluded from ForProvider/InitProvider and only appear in Observation.

2. Blocks were using TypeList instead of SchemaTypeObject, causing the
   generated CRD schema to expect arrays instead of objects.

This fix is required for Plugin Framework resources like alertrule and
recordingrule where metadata and spec blocks need to be objects, not arrays.

Upstream PR: crossplane/upjet#593
Duologic added a commit to grafana/crossplane-provider-grafana that referenced this pull request Feb 13, 2026
* fix: use upjet fork with SchemaNestingModeSingle object fix

Replace upstream upjet with fork that fixes two issues for Plugin
Framework resources using SchemaNestingModeSingle blocks:

1. Blocks were incorrectly marked as Computed=true, causing them to be
   excluded from ForProvider/InitProvider and only appear in Observation.

2. Blocks were using TypeList instead of SchemaTypeObject, causing the
   generated CRD schema to expect arrays instead of objects.

This fix is required for Plugin Framework resources like alertrule and
recordingrule where metadata and spec blocks need to be objects, not arrays.

Upstream PR: crossplane/upjet#593

* update generated files

* fix: update upjet to use grafana/upjet fork with fix-computed-single-blocks branch

---------

Co-authored-by: Moustafa Baiou <moustafa.baiou@grafana.com>
Co-authored-by: Duologic <jeroen@simplistic.be>
@Upbound-CLA

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

1 similar comment
@Upbound-CLA

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@erhancagirici erhancagirici left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@rwwiv thanks for reporting and the PR on this. I've investigated this and below are my findings and notes:

Per this doc comment https://github.com/hashicorp/terraform-plugin-framework/blob/a0219204842978493e5f7742b0c06d5c39951e73/internal/fwschema/block.go#L24 looks like we never get Min/MaxItems data for blocks at the corresponding TF Core schema for plugin-framework resource schemas. This is true for all block nesting types .
As a result of this, today, the inference/heuristic always ends up with Optional: true Computed: true Required: false for all plugin-FW blocks.

In fact, the TF core block fields have no direct notion of "required,optional,computed"ness.

This currently works out SchemaNestingModeSet SchemaNestingModeList SchemaNestingModeMap for fw resources, they always get a spec.forProvider field in the CRD, with the implication that they are always optional. Which is fine

The actual bug resides in the SchemaNestingModeSingle here, please see my comment: https://github.com/crossplane/upjet/pull/593/changes#r3871860815.

on the broader situation: please see https://github.com/crossplane/upjet/pull/593/changes#r3871736101

I'll do a final review after giving it some testing with the existing providers.

Comment thread pkg/types/conversion/tfjson/tfjson.go Outdated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The actual bug is here:
As I mentioned in the main review comment, we initially start with
Required: false, Optional: true, Computed: true for all blocks in plugin-fw resources.

If we determine required: true, we switch Optional: False but computed remains true. i.e. we end up with
Required: true, Optional: false, Computed: true
This is actually an invalid schema configuration. A required field cannot be computed.

However, this ends up in generation pipeline and IsObservation() check treats this as an computed-only field (because computed=true, optional=false)

In summary, adding the following aligns it with the rest of the block types and starts generating those.

Suggested change
v2sch.Computed = false

However, please also see comment for the broader situation on this and a proposed change: https://github.com/crossplane/upjet/pull/593/changes#r3871736101

Comment on lines -167 to 170
// TODO(erhan): not sure whether we need this
// the block itself can be optional, even if some child attribute
// or block is required
v2sch.Required = hasRequiredChild(nb)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

following up my previous todo:

The TF Core schema has no notion of required/optional/computed for a "block" field. Only Min/MaxItems. And they are (intentionally) not available for plugin-fw resources.
So, we have no way of inferring whether a block is "Observation"-only or intended to be configurable. plugin-fw doc comment mentions limited validation capabilities at config time, and says to offload this validation to the provider side.

Additionally, checking the TF SDKv2 code here -> here, sdk resources never end up with NestingSingle block in their core schema, the only exception being the timeout block, which upjet skips anyway.
So, I think it is safe to assume this path is only visited by plugin-fw resource blocks.

Lastly, this converted "v2schema" is only for CRD generation purposes and not utilized at runtime. It acts like an Intermediate representation for seeding CRD generation. e.g. the computed/optional etc.

Considering all of the above, I think hasRequiredChild is not a proper heuristic for requiredness. A required child attribute not necessarily mean that the block itself is required.
I propose to treat all NestingSingle blocks as optional for CRD generation (so that it always generates a nullable forProvider.myFooField ) and let the runtime validate it.

Also, this won't be a breaking change since this path was never generating a required forProvider field anyway, and optionals will just stay the same.

Also, upjet has already config machinery available for explicitly marking the field observe-only or required, so devs can modify according to their needs if they need stricter CRD API validation on this.

TLDR, my suggestion is to have:

	case tfjson.SchemaNestingModeSingle, tfjson.SchemaNestingModeGroup:
		v2sch.Type = SchemaTypeObject
		v2sch.Required = false
		v2sch.Optional = true
		v2sch.Computed = false

// matching the behavior for nested attributes with SchemaNestingModeSingle
// and the Terraform Plugin Framework documentation which states that
// SingleNestedBlock values are represented by an object type.
v2sch.Type = SchemaTypeObject

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

note: SchemaTypeObject is actually a workaround in upjet to represent plugin-fw schemas in sdkv2 schema structs. This actually leads in an invalid sdkv2 schema struct, and calling member functions like CoreConfigSchema() on the resulting TerraformResource causes a panic.

Though for plugin-framework resources, this object is CRD-generation purposes only and never utilized at runtime, and this path is framework-only ( as mentioned in https://github.com/crossplane/upjet/pull/593/changes#r3871736101 ), so I think this is acceptable.

}
if nb.MinItems == 0 && nb.MaxItems == 0 {
v2sch.Computed = true
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

as mentioned in other comments, let's put this back and fix the NestingSingle branch only.

Comment thread pkg/types/conversion/tfjson/tfjson.go Outdated
Comment on lines +157 to +161
// For collection types (Set/List/Map), infer Computed when MinItems and
// MaxItems are both 0, following SDK v2 semantics.
if nb.MinItems == 0 && nb.MaxItems == 0 {
v2sch.Computed = true
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Comment thread pkg/types/conversion/tfjson/tfjson.go Outdated
Comment on lines +164 to +168
// For collection types (Set/List/Map), infer Computed when MinItems and
// MaxItems are both 0, following SDK v2 semantics.
if nb.MinItems == 0 && nb.MaxItems == 0 {
v2sch.Computed = true
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Comment thread pkg/types/conversion/tfjson/tfjson.go Outdated
Comment on lines +171 to +175
// For collection types (Set/List/Map), infer Computed when MinItems and
// MaxItems are both 0, following SDK v2 semantics.
if nb.MinItems == 0 && nb.MaxItems == 0 {
v2sch.Computed = true
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@erhancagirici
erhancagirici force-pushed the fix-computed-single-blocks branch 2 times, most recently from f132611 to b8befe5 Compare September 10, 2026 13:03
When converting tfjson block types to SDK v2 schema, the Computed flag
was being inferred as true when MinItems==0 && MaxItems==0. This
heuristic works for SDK v2 collection types (List/Set/Map) but is
incorrect for Plugin Framework resources using SchemaNestingModeSingle.

Plugin Framework resources default MinItems/MaxItems to 0 for single
blocks even when they contain user-configurable attributes. This caused
blocks like 'metadata' and 'spec' to be incorrectly marked as Computed,
excluding them from ForProvider/InitProvider parameters and only
including them in Observation.

This fix makes SchemaNestingModeSingle blocks always `Optional` and not
`Computed`, so that they always generate a configurable spec field.
`SchemaNestingModeSingle` is only present in Plugin Framework
resource schemas, and FW resources never set `Min/MaxItems`. The heuristic
does not make sense here.

This allows Plugin Framework resources (e.g., Grafana App Platform
resources like AlertruleV0Alpha1) to properly expose their nested blocks
in the CRD's forProvider schema.

Also adds comprehensive unit tests for tfJSONBlockTypeToV2Schema and
hasRequiredChild functions.

Signed-off-by: Will Wernert <william.wernert@grafana.com>
@erhancagirici
erhancagirici force-pushed the fix-computed-single-blocks branch from b8befe5 to 5bd3d80 Compare September 10, 2026 13:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants