Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions internal/common/customplanmodifier/request_only_required_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package customplanmodifier

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
)

// RequestOnlyRequiredOnCreate returns a plan modifier that fails planning if the value is
// missing (null/unknown) during create (i.e., when state is null), but allows omission on read/import.
Copy link
Collaborator

@maastha maastha Dec 15, 2025

Choose a reason for hiding this comment

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

I believe such a value can only be null/non-null but never unknown right? Since these would be Optional attributes? If yes, I'd remove the Known check in the validation method & only check for null

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That is true yes, addressed in 7b3a175

func RequestOnlyRequiredOnCreate() RequestOnlyRequiredOnCreateModifier {
return &requestOnlyRequiredOnCreateAttributePlanModifier{}
}

// Single interface so the modifier can be applied to any attribute type.
type RequestOnlyRequiredOnCreateModifier interface {
planmodifier.String
planmodifier.Bool
planmodifier.Int64
planmodifier.Float64
planmodifier.Number
planmodifier.List
planmodifier.Map
planmodifier.Set
planmodifier.Object
}

type requestOnlyRequiredOnCreateAttributePlanModifier struct{}

func (m *requestOnlyRequiredOnCreateAttributePlanModifier) Description(ctx context.Context) string {
return m.MarkdownDescription(ctx)
}

func (m *requestOnlyRequiredOnCreateAttributePlanModifier) MarkdownDescription(ctx context.Context) string {
return "Ensures that create operations fail when attempting to create a resource with a missing required attribute."
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The description is misleading. The modifier doesn't make create operations fail - it validates that required attributes are present during creation. Consider revising to: 'Validates that the attribute is provided during resource creation, but allows it to be omitted during import operations.'

Suggested change
return "Ensures that create operations fail when attempting to create a resource with a missing required attribute."
return "Validates that the attribute is provided during resource creation, but allows it to be omitted during import operations."

Copilot uses AI. Check for mistakes.
}

func (m *requestOnlyRequiredOnCreateAttributePlanModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
validateRequestOnlyRequiredOnCreate(
isCreate(&req.State),
req.PlanValue,
req.Path,
&resp.Diagnostics,
)
}

func (m *requestOnlyRequiredOnCreateAttributePlanModifier) PlanModifyBool(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) {
validateRequestOnlyRequiredOnCreate(
isCreate(&req.State),
req.PlanValue,
req.Path,
&resp.Diagnostics,
)
}

func (m *requestOnlyRequiredOnCreateAttributePlanModifier) PlanModifyInt64(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) {
validateRequestOnlyRequiredOnCreate(
isCreate(&req.State),
req.PlanValue,
req.Path,
&resp.Diagnostics,
)
}

func (m *requestOnlyRequiredOnCreateAttributePlanModifier) PlanModifyFloat64(ctx context.Context, req planmodifier.Float64Request, resp *planmodifier.Float64Response) {
validateRequestOnlyRequiredOnCreate(
isCreate(&req.State),
req.PlanValue,
req.Path,
&resp.Diagnostics,
)
}

func (m *requestOnlyRequiredOnCreateAttributePlanModifier) PlanModifyNumber(ctx context.Context, req planmodifier.NumberRequest, resp *planmodifier.NumberResponse) {
validateRequestOnlyRequiredOnCreate(
isCreate(&req.State),
req.PlanValue,
req.Path,
&resp.Diagnostics,
)
}

func (m *requestOnlyRequiredOnCreateAttributePlanModifier) PlanModifyList(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) {
validateRequestOnlyRequiredOnCreate(
isCreate(&req.State),
req.PlanValue,
req.Path,
&resp.Diagnostics,
)
}

func (m *requestOnlyRequiredOnCreateAttributePlanModifier) PlanModifyMap(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) {
validateRequestOnlyRequiredOnCreate(
isCreate(&req.State),
req.PlanValue,
req.Path,
&resp.Diagnostics,
)
}

func (m *requestOnlyRequiredOnCreateAttributePlanModifier) PlanModifySet(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) {
validateRequestOnlyRequiredOnCreate(
isCreate(&req.State),
req.PlanValue,
req.Path,
&resp.Diagnostics,
)
}

func (m *requestOnlyRequiredOnCreateAttributePlanModifier) PlanModifyObject(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) {
validateRequestOnlyRequiredOnCreate(
isCreate(&req.State),
req.PlanValue,
req.Path,
&resp.Diagnostics,
)
}

// validateRequestOnlyRequiredOnCreate checks that an attribute has a non-null
// value during create and adds an error if it does not.
func validateRequestOnlyRequiredOnCreate(isCreate bool, planValue attr.Value, attrPath path.Path, diagnostics *diag.Diagnostics) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

should we also throw an error if the user tries to update it? Or is the expectation to always use it in conjunction with customplanmodifier.CreateOnly()?

How is it handled during resource reads? I assume the attribute would still stay in the state even though the GET calls don't return it right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If the user tries to update it, customplanmodifier.CreateOnly() is responsible for blocking the change and surfacing an error. RequestOnlyRequiredOnCreate only enforces that the attribute is present on create, so for attributes that are both required-on-create and immutable we expect to always use it in conjunction with CreateOnly(). However, an attribute could be CreateOnly, without RequestOnlyRequiredOnCreate.

Regarding your second question: you're correct. On resource reads, the attribute stays in the state, so it retains its last known value.

if !isCreate {
return
}

if planValue.IsNull() {
diagnostics.AddError(
fmt.Sprintf("%s is required when creating this resource", attrPath),
fmt.Sprintf("Provide a value for %s during resource creation.", attrPath),
)
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions tools/codegen/codespec/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ var transformations = []AttributeTransformation{
aliasTransformation,
overridesTransformation,
createOnlyTransformation,
requiredOnCreateInputOnlyTransformation,
requestOnlyRequiredOnCreateTransformation,
}

var dataSourceTransformations = []AttributeTransformation{
Expand Down Expand Up @@ -223,7 +223,7 @@ func createOnlyTransformation(attr *Attribute, _ *attrPaths, _ config.SchemaOpti
return nil
}

func requiredOnCreateInputOnlyTransformation(attr *Attribute, _ *attrPaths, _ config.SchemaOptions) error {
func requestOnlyRequiredOnCreateTransformation(attr *Attribute, _ *attrPaths, _ config.SchemaOptions) error {
if attr.ComputedOptionalRequired == Required && attr.ReqBodyUsage == OmitInUpdateBody && !attr.PresentInAnyResponse {
attr.RequestOnlyRequiredOnCreate = true
attr.ComputedOptionalRequired = Optional
Expand Down
27 changes: 16 additions & 11 deletions tools/codegen/gofilegen/schema/schema_attribute.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,6 @@ func commonAttrStructure(attr *codespec.Attribute, attrDefType, planModifierType
Imports: propsStmts.Imports,
}
}

func commonProperties(attr *codespec.Attribute, planModifierType string) []CodeStatement {
var result []CodeStatement
if attr.ComputedOptionalRequired == codespec.Required {
Expand Down Expand Up @@ -199,21 +198,27 @@ func commonProperties(attr *codespec.Attribute, planModifierType string) []CodeS
Imports: imports,
})
}
if attr.CreateOnly { // As of now this is the only property which implies defining plan modifiers.
planModifierImports := []string{
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/customplanmodifier",
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier",
}
code := fmt.Sprintf("PlanModifiers: []%s{customplanmodifier.CreateOnly()}", planModifierType)

// For bool attributes with create-only and default value, use CreateOnlyBoolWithDefault
var customPlanModifiers []string
if attr.CreateOnly {
if attr.Bool != nil && attr.Bool.Default != nil {
code = fmt.Sprintf("PlanModifiers: []%s{customplanmodifier.CreateOnlyBoolWithDefault(%t)}", planModifierType, *attr.Bool.Default)
// For bool attributes with create-only and default value, use CreateOnlyBoolWithDefault
customPlanModifiers = append(customPlanModifiers, fmt.Sprintf("customplanmodifier.CreateOnlyBoolWithDefault(%t)", *attr.Bool.Default))
} else {
customPlanModifiers = append(customPlanModifiers, "customplanmodifier.CreateOnly()")
}
}
if attr.RequestOnlyRequiredOnCreate {
customPlanModifiers = append(customPlanModifiers, "customplanmodifier.RequestOnlyRequiredOnCreate()")
}

if len(customPlanModifiers) > 0 {
result = append(result, CodeStatement{
Code: code,
Imports: planModifierImports,
Code: fmt.Sprintf("PlanModifiers: []%s{%s}", planModifierType, strings.Join(customPlanModifiers, ", ")),
Imports: []string{
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/customplanmodifier",
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier",
},
})
}
return result
Expand Down
42 changes: 42 additions & 0 deletions tools/codegen/gofilegen/schema/schema_attribute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,45 @@ func TestGenerateSchemaAttributes_CreateOnly(t *testing.T) {
})
}
}
func TestGenerateSchemaAttributes_RequestOnlyRequiredOnCreate(t *testing.T) {
tests := map[string]struct {
attribute codespec.Attribute
hasPlanModifier bool
}{
"No RequestOnlyRequiredOnCreate - no plan modifiers": {
attribute: codespec.Attribute{
TFSchemaName: "test_string",
TFModelName: "TestString",
String: &codespec.StringAttribute{},
ComputedOptionalRequired: codespec.Optional,
RequestOnlyRequiredOnCreate: false,
},
hasPlanModifier: false,
},
"RequestOnlyRequiredOnCreate - uses RequestOnlyRequiredOnCreate()": {
attribute: codespec.Attribute{
TFSchemaName: "test_string",
TFModelName: "TestString",
String: &codespec.StringAttribute{},
ComputedOptionalRequired: codespec.Optional,
RequestOnlyRequiredOnCreate: true,
},
hasPlanModifier: true,
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
result := schema.GenerateSchemaAttributes([]codespec.Attribute{tc.attribute})
code := result.Code
if !tc.hasPlanModifier {
assert.NotContains(t, code, "PlanModifiers:")
return
}
assert.Contains(t, code, "PlanModifiers:")
if tc.attribute.RequestOnlyRequiredOnCreate {
assert.Contains(t, code, "customplanmodifier.RequestOnlyRequiredOnCreate()")
}
})
}
}