Skip to content

Commit 0918d45

Browse files
chore: Add custom plan modifier for RequestOnlyRequiredOnCreate Attribute (#3977)
* Add custom plan modifier for RequestOnlyRequiredOnCreate Attribute * refactor custom plan modifiers to get triggered when attribute of type requestonlyrequiredonCreate is detected * Follow-up from PR #3972: Rename transformation function to match attribute name * Add unit test for custom plan modifier * Refactor custom plan modifier function * Fix lint issues * Address PR comments
1 parent 800d17a commit 0918d45

File tree

6 files changed

+198
-15
lines changed

6 files changed

+198
-15
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package customplanmodifier
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hashicorp/terraform-plugin-framework/attr"
8+
"github.com/hashicorp/terraform-plugin-framework/diag"
9+
"github.com/hashicorp/terraform-plugin-framework/path"
10+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
11+
)
12+
13+
// RequestOnlyRequiredOnCreate returns a plan modifier that fails planning if the value is
14+
// missing (null/unknown) during create (i.e., when state is null), but allows omission on read/import.
15+
func RequestOnlyRequiredOnCreate() RequestOnlyRequiredOnCreateModifier {
16+
return &requestOnlyRequiredOnCreateAttributePlanModifier{}
17+
}
18+
19+
// Single interface so the modifier can be applied to any attribute type.
20+
type RequestOnlyRequiredOnCreateModifier interface {
21+
planmodifier.String
22+
planmodifier.Bool
23+
planmodifier.Int64
24+
planmodifier.Float64
25+
planmodifier.Number
26+
planmodifier.List
27+
planmodifier.Map
28+
planmodifier.Set
29+
planmodifier.Object
30+
}
31+
32+
type requestOnlyRequiredOnCreateAttributePlanModifier struct{}
33+
34+
func (m *requestOnlyRequiredOnCreateAttributePlanModifier) Description(ctx context.Context) string {
35+
return m.MarkdownDescription(ctx)
36+
}
37+
38+
func (m *requestOnlyRequiredOnCreateAttributePlanModifier) MarkdownDescription(ctx context.Context) string {
39+
return "Ensures that create operations fail when attempting to create a resource with a missing required attribute."
40+
}
41+
42+
func (m *requestOnlyRequiredOnCreateAttributePlanModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
43+
validateRequestOnlyRequiredOnCreate(
44+
isCreate(&req.State),
45+
req.PlanValue,
46+
req.Path,
47+
&resp.Diagnostics,
48+
)
49+
}
50+
51+
func (m *requestOnlyRequiredOnCreateAttributePlanModifier) PlanModifyBool(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) {
52+
validateRequestOnlyRequiredOnCreate(
53+
isCreate(&req.State),
54+
req.PlanValue,
55+
req.Path,
56+
&resp.Diagnostics,
57+
)
58+
}
59+
60+
func (m *requestOnlyRequiredOnCreateAttributePlanModifier) PlanModifyInt64(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) {
61+
validateRequestOnlyRequiredOnCreate(
62+
isCreate(&req.State),
63+
req.PlanValue,
64+
req.Path,
65+
&resp.Diagnostics,
66+
)
67+
}
68+
69+
func (m *requestOnlyRequiredOnCreateAttributePlanModifier) PlanModifyFloat64(ctx context.Context, req planmodifier.Float64Request, resp *planmodifier.Float64Response) {
70+
validateRequestOnlyRequiredOnCreate(
71+
isCreate(&req.State),
72+
req.PlanValue,
73+
req.Path,
74+
&resp.Diagnostics,
75+
)
76+
}
77+
78+
func (m *requestOnlyRequiredOnCreateAttributePlanModifier) PlanModifyNumber(ctx context.Context, req planmodifier.NumberRequest, resp *planmodifier.NumberResponse) {
79+
validateRequestOnlyRequiredOnCreate(
80+
isCreate(&req.State),
81+
req.PlanValue,
82+
req.Path,
83+
&resp.Diagnostics,
84+
)
85+
}
86+
87+
func (m *requestOnlyRequiredOnCreateAttributePlanModifier) PlanModifyList(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) {
88+
validateRequestOnlyRequiredOnCreate(
89+
isCreate(&req.State),
90+
req.PlanValue,
91+
req.Path,
92+
&resp.Diagnostics,
93+
)
94+
}
95+
96+
func (m *requestOnlyRequiredOnCreateAttributePlanModifier) PlanModifyMap(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) {
97+
validateRequestOnlyRequiredOnCreate(
98+
isCreate(&req.State),
99+
req.PlanValue,
100+
req.Path,
101+
&resp.Diagnostics,
102+
)
103+
}
104+
105+
func (m *requestOnlyRequiredOnCreateAttributePlanModifier) PlanModifySet(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) {
106+
validateRequestOnlyRequiredOnCreate(
107+
isCreate(&req.State),
108+
req.PlanValue,
109+
req.Path,
110+
&resp.Diagnostics,
111+
)
112+
}
113+
114+
func (m *requestOnlyRequiredOnCreateAttributePlanModifier) PlanModifyObject(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) {
115+
validateRequestOnlyRequiredOnCreate(
116+
isCreate(&req.State),
117+
req.PlanValue,
118+
req.Path,
119+
&resp.Diagnostics,
120+
)
121+
}
122+
123+
// validateRequestOnlyRequiredOnCreate checks that an attribute has a non-null
124+
// value during create and adds an error if it does not.
125+
func validateRequestOnlyRequiredOnCreate(isCreate bool, planValue attr.Value, attrPath path.Path, diagnostics *diag.Diagnostics) {
126+
if !isCreate {
127+
return
128+
}
129+
130+
if planValue.IsNull() {
131+
diagnostics.AddError(
132+
fmt.Sprintf("%s is required when creating this resource", attrPath),
133+
fmt.Sprintf("Provide a value for %s during resource creation.", attrPath),
134+
)
135+
}
136+
}

internal/serviceapi/orgserviceaccountapi/resource_schema.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/serviceapi/orgserviceaccountsecretapi/resource_schema.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tools/codegen/codespec/config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ var transformations = []AttributeTransformation{
8888
aliasTransformation,
8989
overridesTransformation,
9090
createOnlyTransformation,
91-
requiredOnCreateInputOnlyTransformation,
91+
requestOnlyRequiredOnCreateTransformation,
9292
}
9393

9494
var dataSourceTransformations = []AttributeTransformation{
@@ -223,7 +223,7 @@ func createOnlyTransformation(attr *Attribute, _ *attrPaths, _ config.SchemaOpti
223223
return nil
224224
}
225225

226-
func requiredOnCreateInputOnlyTransformation(attr *Attribute, _ *attrPaths, _ config.SchemaOptions) error {
226+
func requestOnlyRequiredOnCreateTransformation(attr *Attribute, _ *attrPaths, _ config.SchemaOptions) error {
227227
if attr.ComputedOptionalRequired == Required && attr.ReqBodyUsage == OmitInUpdateBody && !attr.PresentInAnyResponse {
228228
attr.RequestOnlyRequiredOnCreate = true
229229
attr.ComputedOptionalRequired = Optional

tools/codegen/gofilegen/schema/schema_attribute.go

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,6 @@ func commonAttrStructure(attr *codespec.Attribute, attrDefType, planModifierType
167167
Imports: propsStmts.Imports,
168168
}
169169
}
170-
171170
func commonProperties(attr *codespec.Attribute, planModifierType string) []CodeStatement {
172171
var result []CodeStatement
173172
if attr.ComputedOptionalRequired == codespec.Required {
@@ -199,21 +198,27 @@ func commonProperties(attr *codespec.Attribute, planModifierType string) []CodeS
199198
Imports: imports,
200199
})
201200
}
202-
if attr.CreateOnly { // As of now this is the only property which implies defining plan modifiers.
203-
planModifierImports := []string{
204-
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/customplanmodifier",
205-
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier",
206-
}
207-
code := fmt.Sprintf("PlanModifiers: []%s{customplanmodifier.CreateOnly()}", planModifierType)
208201

209-
// For bool attributes with create-only and default value, use CreateOnlyBoolWithDefault
202+
var customPlanModifiers []string
203+
if attr.CreateOnly {
210204
if attr.Bool != nil && attr.Bool.Default != nil {
211-
code = fmt.Sprintf("PlanModifiers: []%s{customplanmodifier.CreateOnlyBoolWithDefault(%t)}", planModifierType, *attr.Bool.Default)
205+
// For bool attributes with create-only and default value, use CreateOnlyBoolWithDefault
206+
customPlanModifiers = append(customPlanModifiers, fmt.Sprintf("customplanmodifier.CreateOnlyBoolWithDefault(%t)", *attr.Bool.Default))
207+
} else {
208+
customPlanModifiers = append(customPlanModifiers, "customplanmodifier.CreateOnly()")
212209
}
210+
}
211+
if attr.RequestOnlyRequiredOnCreate {
212+
customPlanModifiers = append(customPlanModifiers, "customplanmodifier.RequestOnlyRequiredOnCreate()")
213+
}
213214

215+
if len(customPlanModifiers) > 0 {
214216
result = append(result, CodeStatement{
215-
Code: code,
216-
Imports: planModifierImports,
217+
Code: fmt.Sprintf("PlanModifiers: []%s{%s}", planModifierType, strings.Join(customPlanModifiers, ", ")),
218+
Imports: []string{
219+
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/customplanmodifier",
220+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier",
221+
},
217222
})
218223
}
219224
return result

tools/codegen/gofilegen/schema/schema_attribute_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,45 @@ func TestGenerateSchemaAttributes_CreateOnly(t *testing.T) {
117117
})
118118
}
119119
}
120+
func TestGenerateSchemaAttributes_RequestOnlyRequiredOnCreate(t *testing.T) {
121+
tests := map[string]struct {
122+
attribute codespec.Attribute
123+
hasPlanModifier bool
124+
}{
125+
"No RequestOnlyRequiredOnCreate - no plan modifiers": {
126+
attribute: codespec.Attribute{
127+
TFSchemaName: "test_string",
128+
TFModelName: "TestString",
129+
String: &codespec.StringAttribute{},
130+
ComputedOptionalRequired: codespec.Optional,
131+
RequestOnlyRequiredOnCreate: false,
132+
},
133+
hasPlanModifier: false,
134+
},
135+
"RequestOnlyRequiredOnCreate - uses RequestOnlyRequiredOnCreate()": {
136+
attribute: codespec.Attribute{
137+
TFSchemaName: "test_string",
138+
TFModelName: "TestString",
139+
String: &codespec.StringAttribute{},
140+
ComputedOptionalRequired: codespec.Optional,
141+
RequestOnlyRequiredOnCreate: true,
142+
},
143+
hasPlanModifier: true,
144+
},
145+
}
146+
147+
for name, tc := range tests {
148+
t.Run(name, func(t *testing.T) {
149+
result := schema.GenerateSchemaAttributes([]codespec.Attribute{tc.attribute})
150+
code := result.Code
151+
if !tc.hasPlanModifier {
152+
assert.NotContains(t, code, "PlanModifiers:")
153+
return
154+
}
155+
assert.Contains(t, code, "PlanModifiers:")
156+
if tc.attribute.RequestOnlyRequiredOnCreate {
157+
assert.Contains(t, code, "customplanmodifier.RequestOnlyRequiredOnCreate()")
158+
}
159+
})
160+
}
161+
}

0 commit comments

Comments
 (0)