Skip to content

Commit b2a4dcc

Browse files
committed
add junos_applications_ordered resource
copy of junos_applications resource but with Block List instead of Block set to have a workaround for the performance issue on Terraform plan with many Block Sets workaround for #709
1 parent c50f373 commit b2a4dcc

File tree

8 files changed

+476
-10
lines changed

8 files changed

+476
-10
lines changed

.changes/issue-709.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<!-- markdownlint-disable-file MD013 MD041 -->
2+
FEATURES:
3+
4+
* add `junos_applications_ordered` resource, copy of `junos_applications` resource but with Block List instead of Block Set to have a workaround for the performance issue on Block Sets (workaround for [#709](https://github.com/jeremmfr/terraform-provider-junos/issues/709))
5+
6+
ENHANCEMENTS:
7+
8+
BUG FIXES:
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
page_title: "Junos: junos_applications_ordered"
3+
---
4+
5+
# junos_applications_ordered
6+
7+
It has the same functionality as the `junos_applications` resource
8+
but with `applications` and `application_set` arguments as Block List instead of Block Set.
9+
10+
This provides a workaround for the performance issue on Terraform plan with many Block Sets
11+
(details in GitHub issue [#775](https://github.com/hashicorp/terraform-plugin-framework/issues/775))
12+
but Block List involves:
13+
14+
- a change in the order of the blocks triggers a resource change.
15+
- Terraform plan output can be complex when the number of blocks on the resource changes.
16+
17+
See the [junos_applications](applications) resource
18+
for more details on arguments or attributes.

internal/providerfwk/provider.go

+1
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ func (p *junosProvider) Resources(_ context.Context) []func() resource.Resource
237237
newAggregateRouteResource,
238238
newApplicationResource,
239239
newApplicationsResource,
240+
newApplicationsOrderedResource,
240241
newApplicationSetResource,
241242
newBgpGroupResource,
242243
newBgpNeighborResource,

internal/providerfwk/resource_applications.go

+10-10
Original file line numberDiff line numberDiff line change
@@ -320,50 +320,50 @@ func (rscData *applicationsData) set(
320320
configSet := make([]string, 0, 100)
321321

322322
applicationName := make(map[string]struct{})
323-
for _, block := range rscData.Application {
323+
for i, block := range rscData.Application {
324324
name := block.Name.ValueString()
325325
if name == "" {
326-
return path.Root("application"),
326+
return path.Root("application").AtListIndex(i).AtName("name"),
327327
errors.New("name argument in application block is empty")
328328
}
329329
if _, ok := applicationName[name]; ok {
330-
return path.Root("application"),
330+
return path.Root("application").AtListIndex(i).AtName("name"),
331331
fmt.Errorf("multiple application blocks with the same name %q", name)
332332
}
333333
applicationName[name] = struct{}{}
334334

335335
blockErrorSuffix := fmt.Sprintf(" in application block %q", name)
336336
if block.isEmpty() {
337-
return path.Root("application"),
337+
return path.Root("application").AtListIndex(i).AtName("name"),
338338
errors.New("at least one of arguments need to be set (in addition to `name`)" +
339339
blockErrorSuffix)
340340
}
341341

342342
dataConfigSet, _, err := block.configSet(blockErrorSuffix)
343343
if err != nil {
344-
return path.Root("application"), err
344+
return path.Root("application").AtListIndex(i).AtName("name"), err
345345
}
346346
configSet = append(configSet, dataConfigSet...)
347347
}
348348
applicationSetName := make(map[string]struct{})
349-
for _, block := range rscData.ApplicationSet {
349+
for i, block := range rscData.ApplicationSet {
350350
name := block.Name.ValueString()
351351
if name == "" {
352-
return path.Root("application_set"),
352+
return path.Root("application_set").AtListIndex(i).AtName("name"),
353353
errors.New("name argument in application_set block is empty")
354354
}
355355
if _, ok := applicationSetName[name]; ok {
356-
return path.Root("application_set"),
356+
return path.Root("application_set").AtListIndex(i).AtName("name"),
357357
fmt.Errorf("multiple application_set blocks with the same name %q", name)
358358
}
359359
if _, ok := applicationName[name]; ok {
360-
return path.Root("application"),
360+
return path.Root("application_set").AtListIndex(i).AtName("name"),
361361
fmt.Errorf("application and application_set blocks with the same name %q", name)
362362
}
363363
applicationSetName[name] = struct{}{}
364364

365365
if block.isEmpty() {
366-
return path.Root("application_set"),
366+
return path.Root("application_set").AtListIndex(i).AtName("name"),
367367
fmt.Errorf("at least one of applications, application_set or description must be specified"+
368368
" in application_set block %q", name)
369369
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
package providerfwk
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/jeremmfr/terraform-provider-junos/internal/junos"
8+
"github.com/jeremmfr/terraform-provider-junos/internal/tfdiag"
9+
10+
"github.com/hashicorp/terraform-plugin-framework/path"
11+
"github.com/hashicorp/terraform-plugin-framework/resource"
12+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
13+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
14+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
15+
"github.com/hashicorp/terraform-plugin-framework/types"
16+
)
17+
18+
// Ensure the implementation satisfies the expected interfaces.
19+
var (
20+
_ resource.Resource = &applicationsOrdered{}
21+
_ resource.ResourceWithConfigure = &applicationsOrdered{}
22+
_ resource.ResourceWithValidateConfig = &applicationsOrdered{}
23+
_ resource.ResourceWithImportState = &applicationsOrdered{}
24+
)
25+
26+
type applicationsOrdered struct {
27+
client *junos.Client
28+
}
29+
30+
func newApplicationsOrderedResource() resource.Resource {
31+
return &applicationsOrdered{}
32+
}
33+
34+
func (rsc *applicationsOrdered) typeName() string {
35+
return providerName + "_applications_ordered"
36+
}
37+
38+
func (rsc *applicationsOrdered) junosName() string {
39+
return "applications"
40+
}
41+
42+
func (rsc *applicationsOrdered) junosClient() *junos.Client {
43+
return rsc.client
44+
}
45+
46+
func (rsc *applicationsOrdered) Metadata(
47+
_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse,
48+
) {
49+
resp.TypeName = rsc.typeName()
50+
}
51+
52+
func (rsc *applicationsOrdered) Configure(
53+
ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse,
54+
) {
55+
// Prevent panic if the provider has not been configured.
56+
if req.ProviderData == nil {
57+
return
58+
}
59+
client, ok := req.ProviderData.(*junos.Client)
60+
if !ok {
61+
unexpectedResourceConfigureType(ctx, req, resp)
62+
63+
return
64+
}
65+
rsc.client = client
66+
}
67+
68+
func (rsc *applicationsOrdered) Schema(
69+
_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse,
70+
) {
71+
resp.Schema = schema.Schema{
72+
Description: "Configure entirely `" + rsc.junosName() + "` block",
73+
Attributes: map[string]schema.Attribute{
74+
"id": schema.StringAttribute{
75+
Computed: true,
76+
Description: "An identifier for the resource with value `applications`.",
77+
PlanModifiers: []planmodifier.String{
78+
stringplanmodifier.UseStateForUnknown(),
79+
},
80+
},
81+
},
82+
Blocks: map[string]schema.Block{
83+
"application": schema.ListNestedBlock{
84+
Description: "For each name, define an application.",
85+
NestedObject: schema.NestedBlockObject{
86+
Attributes: applicationAttrData{}.attributesSchema(),
87+
Blocks: applicationAttrData{}.blocksSchema(),
88+
},
89+
},
90+
"application_set": schema.ListNestedBlock{
91+
Description: "For each name, define an application set.",
92+
NestedObject: schema.NestedBlockObject{
93+
Attributes: applicationSetAttrData{}.attributesSchema(),
94+
},
95+
},
96+
},
97+
}
98+
}
99+
100+
type applicationsOrderedConfig struct {
101+
ID types.String `tfsdk:"id"`
102+
Application types.List `tfsdk:"application"`
103+
ApplicationSet types.List `tfsdk:"application_set"`
104+
}
105+
106+
func (rsc *applicationsOrdered) ValidateConfig(
107+
ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse,
108+
) {
109+
var config applicationsOrderedConfig
110+
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
111+
if resp.Diagnostics.HasError() {
112+
return
113+
}
114+
115+
applicationName := make(map[string]struct{})
116+
if !config.Application.IsNull() &&
117+
!config.Application.IsUnknown() {
118+
var configApplication []applicationAttrConfig
119+
asDiags := config.Application.ElementsAs(ctx, &configApplication, false)
120+
if asDiags.HasError() {
121+
resp.Diagnostics.Append(asDiags...)
122+
123+
return
124+
}
125+
126+
for i, block := range configApplication {
127+
if !block.Name.IsUnknown() {
128+
name := block.Name.ValueString()
129+
if _, ok := applicationName[name]; ok {
130+
resp.Diagnostics.AddAttributeError(
131+
path.Root("application").AtListIndex(i).AtName("name"),
132+
tfdiag.DuplicateConfigErrSummary,
133+
fmt.Sprintf("multiple application blocks with the same name %q", name),
134+
)
135+
}
136+
applicationName[name] = struct{}{}
137+
}
138+
139+
if block.isEmpty() {
140+
resp.Diagnostics.AddAttributeError(
141+
path.Root("application").AtListIndex(i).AtName("*"),
142+
tfdiag.MissingConfigErrSummary,
143+
fmt.Sprintf("at least one of arguments need to be set (in addition to `name`)"+
144+
" in application block %q", block.Name.ValueString()),
145+
)
146+
}
147+
rootPath := path.Root("application").AtListIndex(i)
148+
block.validateConfig(
149+
ctx,
150+
&rootPath,
151+
fmt.Sprintf(" in application block %q", block.Name.ValueString()),
152+
resp,
153+
)
154+
}
155+
}
156+
157+
if !config.ApplicationSet.IsNull() &&
158+
!config.ApplicationSet.IsUnknown() {
159+
var configApplicationSet []applicationSetAttrConfig
160+
asDiags := config.ApplicationSet.ElementsAs(ctx, &configApplicationSet, false)
161+
if asDiags.HasError() {
162+
resp.Diagnostics.Append(asDiags...)
163+
164+
return
165+
}
166+
applicationSetName := make(map[string]struct{})
167+
for i, block := range configApplicationSet {
168+
if !block.Name.IsUnknown() {
169+
name := block.Name.ValueString()
170+
if _, ok := applicationSetName[name]; ok {
171+
resp.Diagnostics.AddAttributeError(
172+
path.Root("application_set").AtListIndex(i).AtName("name"),
173+
tfdiag.DuplicateConfigErrSummary,
174+
fmt.Sprintf("multiple application_set blocks with the same name %q", name),
175+
)
176+
}
177+
applicationSetName[name] = struct{}{}
178+
if _, ok := applicationName[name]; ok {
179+
resp.Diagnostics.AddAttributeError(
180+
path.Root("application").AtListIndex(i).AtName("name"),
181+
tfdiag.DuplicateConfigErrSummary,
182+
fmt.Sprintf("application and application_set blocks with the same name %q", name),
183+
)
184+
}
185+
}
186+
187+
if block.isEmpty() {
188+
resp.Diagnostics.AddAttributeError(
189+
path.Root("application_set").AtListIndex(i).AtName("*"),
190+
tfdiag.MissingConfigErrSummary,
191+
fmt.Sprintf("at least one of applications, application_set or description must be specified"+
192+
" in application_set block %q", block.Name.ValueString()),
193+
)
194+
}
195+
rootPath := path.Root("application_set").AtListIndex(i)
196+
block.validateConfig(
197+
ctx,
198+
&rootPath,
199+
fmt.Sprintf(" in application_set block %q", block.Name.ValueString()),
200+
resp,
201+
)
202+
}
203+
}
204+
}
205+
206+
func (rsc *applicationsOrdered) Create(
207+
ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse,
208+
) {
209+
var plan applicationsData
210+
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
211+
if resp.Diagnostics.HasError() {
212+
return
213+
}
214+
215+
defaultResourceCreate(
216+
ctx,
217+
rsc,
218+
nil,
219+
nil,
220+
&plan,
221+
resp,
222+
)
223+
}
224+
225+
func (rsc *applicationsOrdered) Read(
226+
ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse,
227+
) {
228+
var state, data applicationsData
229+
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
230+
if resp.Diagnostics.HasError() {
231+
return
232+
}
233+
234+
var _ resourceDataReadWithoutArg = &data
235+
defaultResourceRead(
236+
ctx,
237+
rsc,
238+
nil,
239+
&data,
240+
nil,
241+
resp,
242+
)
243+
}
244+
245+
func (rsc *applicationsOrdered) Update(
246+
ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse,
247+
) {
248+
var plan, state applicationsData
249+
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
250+
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
251+
if resp.Diagnostics.HasError() {
252+
return
253+
}
254+
255+
defaultResourceUpdate(
256+
ctx,
257+
rsc,
258+
&state,
259+
&plan,
260+
resp,
261+
)
262+
}
263+
264+
func (rsc *applicationsOrdered) Delete(
265+
ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse,
266+
) {
267+
var state applicationsData
268+
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
269+
if resp.Diagnostics.HasError() {
270+
return
271+
}
272+
273+
defaultResourceDelete(
274+
ctx,
275+
rsc,
276+
&state,
277+
resp,
278+
)
279+
}
280+
281+
func (rsc *applicationsOrdered) ImportState(
282+
ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse,
283+
) {
284+
var data applicationsData
285+
286+
var _ resourceDataReadWithoutArg = &data
287+
defaultResourceImportState(
288+
ctx,
289+
rsc,
290+
&data,
291+
req,
292+
resp,
293+
"",
294+
)
295+
}

0 commit comments

Comments
 (0)