@@ -203,6 +203,9 @@ func (r *{{ resourceStructName }}) ImportState(ctx context.Context, req resource
203
203
{{- /* Done */ -}}`
204
204
205
205
const resourceCreateManyFunction = `
206
+ {{ $resourceSDKStructName := printf "%s.%s" .resourceSDKName .EntryOrConfig }}
207
+ {{ $resourceTFStructName := printf "%s%sObject" .structName .ListAttribute.CamelCase }}
208
+
206
209
var state, createdState {{ .structName }}Model
207
210
resp.Diagnostics.Append(req.Plan.Get(ctx, &state)...)
208
211
if resp.Diagnostics.HasError() {
@@ -215,37 +218,188 @@ tflog.Info(ctx, "performing resource create", map[string]any{
215
218
"function": "Create",
216
219
})
217
220
218
- svc := {{ .resourceSDKName }}.NewService(r.client)
219
-
220
221
var location {{ .resourceSDKName }}.Location
221
222
{{ RenderLocationsStateToPango "state.Location" "location" }}
222
223
223
- var elements []{{ .structName }}{{ .ListAttribute.CamelCase }}Object
224
+ var elements []{{ $resourceTFStructName }}
224
225
state.{{ .ListAttribute.CamelCase }}.ElementsAs(ctx, &elements, false)
225
- entries := make([]*{{ .resourceSDKName }}.{{ .EntryOrConfig }}, len(elements))
226
+
227
+ type entryWithState struct {
228
+ Entry *{{ $resourceSDKStructName }}
229
+ StateIdx int
230
+ }
231
+
232
+ planEntriesByName := make(map[string]*entryWithState, len(elements))
226
233
for idx, elt := range elements {
227
234
var list_diags diag.Diagnostics
228
- var entry *{{ .resourceSDKName }}.{{ .EntryOrConfig }}
235
+ var entry *{{ $resourceSDKStructName }}
229
236
entry, list_diags = elt.CopyToPango(ctx, nil)
230
237
resp.Diagnostics.Append(list_diags...)
231
238
if resp.Diagnostics.HasError() {
232
239
return
233
240
}
234
- entries[idx] = entry
241
+
242
+ planEntriesByName[elt.Name.ValueString()] = &entryWithState{
243
+ Entry: entry,
244
+ StateIdx: idx,
245
+ }
246
+ }
247
+
248
+ svc := {{ .resourceSDKName }}.NewService(r.client)
249
+
250
+ // First, check if none of the entries from the plan already exist on the server
251
+ existing, err := svc.List(ctx, location, "get", "", "")
252
+ if err != nil && err.Error() != "Object not found" {
253
+ resp.Diagnostics.AddError("sdk error while listing resources", err.Error())
254
+ return
255
+ }
256
+
257
+ for _, elt := range existing {
258
+ _, foundInPlan := planEntriesByName[elt.Name]
259
+
260
+ if foundInPlan {
261
+ errorMsg := fmt.Sprintf("%s created outside of terraform", elt.Name)
262
+ resp.Diagnostics.AddError("Conflict between plan and server data", errorMsg)
263
+ return
264
+ }
265
+ }
266
+
267
+ specifier, _, err := {{ .resourceSDKName }}.Versioning(r.client.Versioning())
268
+ if err != nil {
269
+ resp.Diagnostics.AddError("error while creating specifier", err.Error())
270
+ return
271
+ }
272
+
273
+ updates := xmlapi.NewMultiConfig(len(planEntriesByName))
274
+
275
+ entries := make([]*{{ $resourceSDKStructName }}, len(planEntriesByName))
276
+ for _, elt := range planEntriesByName {
277
+ entries[elt.StateIdx] = elt.Entry
278
+ }
279
+
280
+ for _, elt := range entries {
281
+ path, err := location.XpathWithEntryName(r.client.Versioning(), elt.Name)
282
+ if err != nil {
283
+ resp.Diagnostics.AddError("Failed to create xpath for existing entry", err.Error())
284
+ return
285
+ }
286
+
287
+ xmlEntry, err := specifier(*elt)
288
+ if err != nil {
289
+ resp.Diagnostics.AddError("Failed to transform Entry into XML document", err.Error())
290
+ return
291
+ }
292
+
293
+ updates.Add(&xmlapi.Config{
294
+ Action: "edit",
295
+ Xpath: util.AsXpath(path),
296
+ Element: xmlEntry,
297
+ Target: r.client.GetTarget(),
298
+ })
299
+ }
300
+
301
+ if len(updates.Operations) > 0 {
302
+ if _, _, _, err := r.client.MultiConfig(ctx, updates, false, nil); err != nil {
303
+ resp.Diagnostics.AddError("error updating entries", err.Error())
304
+ return
305
+ }
306
+ }
307
+
308
+ existing, err = svc.List(ctx, location, "get", "", "")
309
+ if err != nil && err.Error() != "Object not found" {
310
+ resp.Diagnostics.AddError("sdk error while listing resources", err.Error())
311
+ return
312
+ }
313
+
314
+ var movementRequired bool
315
+ {{- if .Exhaustive }}
316
+ // We manage the entire list of PAN-OS objects, so the order of entries
317
+ // from the plan is compared against all existing PAN-OS objects.
318
+ for idx, elt := range existing {
319
+ if planEntriesByName[elt.Name].StateIdx != idx {
320
+ movementRequired = true
321
+ }
322
+ planEntriesByName[elt.Name].Entry.Uuid = elt.Uuid
323
+ }
324
+ {{- else }}
325
+ // We only manage a subset of PAN-OS object on the given list, so care
326
+ // has to be taken to calculate the order of those managed elements on the
327
+ // PAN-OS side.
328
+
329
+ // We filter all existing entries to end up with a list of entries that
330
+ // are in the plan. For every element of that list, we store its PAN-OS
331
+ // list index as StateIdx. Finally, the managedEntries index will serve
332
+ // as a way to check if managed entries are in order relative to each
333
+ // other.
334
+ managedEntries := make([]*entryWithState, len(entries))
335
+ for idx, elt := range existing {
336
+ if _, found := planEntriesByName[elt.Name]; found {
337
+ managedEntries = append(managedEntries, &entryWithState{
338
+ Entry: &elt,
339
+ StateIdx: idx,
340
+ })
341
+ }
342
+ }
343
+
344
+ var previousManagedEntry, previousPlannedEntry *entryWithState
345
+ for idx, elt := range managedEntries {
346
+ // plannedEntriesByName is a map of entries from the plan indexed by their
347
+ // name. If idx doesn't match StateIdx of the entry from the plan, the PAN-OS
348
+ // object is out of order.
349
+ plannedEntry := planEntriesByName[elt.Entry.Name]
350
+ if plannedEntry.StateIdx != idx {
351
+ movementRequired = true
352
+ break
353
+ }
354
+ // If this is the first element we are comparing, store it for future reference
355
+ // and continue. We will use it to calculate distance between two elements in
356
+ // PAN-OS list.
357
+ if previousManagedEntry == nil {
358
+ previousManagedEntry = elt
359
+ previousPlannedEntry = plannedEntry
360
+ continue
361
+ }
362
+
363
+ serverDistance := elt.StateIdx - previousManagedEntry.StateIdx
364
+ planDistance := plannedEntry.StateIdx - previousPlannedEntry.StateIdx
365
+
366
+ // If the distance between previous and current object differs between
367
+ // PAN-OS and the plan, we need to move objects around.
368
+ if serverDistance != planDistance {
369
+ movementRequired = true
370
+ break
371
+ }
372
+
373
+ previousManagedEntry = elt
374
+ previousPlannedEntry = plannedEntry
235
375
}
376
+ {{- end }}
236
377
237
- objects := make([]{{ .structName }}{{ .ListAttribute.CamelCase }}Object, len(entries))
238
- for idx, elt := range entries {
239
- created, err := svc.Create(ctx, location, *elt)
378
+ if movementRequired {
379
+ entries := make([]{{ $resourceSDKStructName }}, len(planEntriesByName))
380
+ for _, elt := range planEntriesByName {
381
+ entries[elt.StateIdx] = *elt.Entry
382
+ }
383
+ trueValue := true
384
+ err = svc.MoveGroup(ctx, location, rule.Position{First: &trueValue}, entries)
240
385
if err != nil {
241
- resp.Diagnostics.AddError("SDK error during create ", err.Error())
386
+ resp.Diagnostics.AddError("Failed to reorder entries ", err.Error())
242
387
return
243
388
}
244
- var object {{ .structName }}{{ .ListAttribute.CamelCase }}Object
245
- object.CopyFromPango(ctx, created, nil)
389
+ }
390
+
391
+ objects := make([]{{ $resourceTFStructName }}, len(planEntriesByName))
392
+ for idx, elt := range existing {
393
+ var object {{ $resourceTFStructName }}
394
+ copy_diags := object.CopyFromPango(ctx, &elt, nil)
395
+ resp.Diagnostics.Append(copy_diags...)
246
396
objects[idx] = object
247
397
}
248
398
399
+ if resp.Diagnostics.HasError() {
400
+ return
401
+ }
402
+
249
403
var list_diags diag.Diagnostics
250
404
createdState.Location = state.Location
251
405
createdState.{{ .ListAttribute.CamelCase }}, list_diags = types.ListValueFrom(ctx, state.getTypeFor("{{ .ListAttribute.Underscore }}"), objects)
@@ -363,6 +517,17 @@ const resourceCreateFunction = `
363
517
`
364
518
365
519
const resourceReadManyFunction = `
520
+ {{- $structName := "" }}
521
+ {{- if eq .ResourceOrDS "DataSource" }}
522
+ {{ $structName = .dataSourceStructName }}
523
+ {{- else }}
524
+ {{ $structName = .resourceStructName }}
525
+ {{- end }}
526
+ {{- $resourceSDKStructName := printf "%s.%s" .resourceSDKName .EntryOrConfig }}
527
+ {{- $resourceTFStructName := printf "%s%sObject" $structName .ListAttribute.CamelCase }}
528
+ // {{ $resourceSDKStructName }}
529
+ // {{ $resourceTFStructName }}
530
+
366
531
{{- $stateName := "" }}
367
532
{{- if eq .ResourceOrDS "DataSource" }}
368
533
{{- $stateName = "Config" }}
@@ -393,19 +558,35 @@ if err != nil {
393
558
return
394
559
}
395
560
396
- {{- if not .Exhaustive }}
397
- // FIXME: For non-exhaustive variants (security_policy_rules etc.) we only want
398
- // to check if all entries are in place, and in the correct position.
399
- {{- end }}
400
-
401
-
561
+ {{- if .Exhaustive }}
562
+ // For resources that take sole ownership of a given list, Read()
563
+ // will return all existing entries from the server.
402
564
objects := make([]{{ .structName }}{{ .ResourceOrDS }}{{ .ListAttribute.CamelCase }}Object, len(existing))
403
565
for idx, elt := range existing {
404
- fmt.Printf("Read() entry: %v %v", elt.Name, *elt.Uuid)
405
566
var object {{ .structName }}{{ .ResourceOrDS }}{{ .ListAttribute.CamelCase }}Object
406
567
object.CopyFromPango(ctx, &elt, nil)
407
568
objects[idx] = object
408
569
}
570
+ {{- else }}
571
+ // For resources that only manage their own items in the list, Read()
572
+ // must only objects that are already part of the state.
573
+ var elements []{{ $resourceTFStructName }}
574
+ state.{{ .ListAttribute.CamelCase }}.ElementsAs(ctx, &elements, false)
575
+ stateObjectsByName := make(map[string]*{{ $resourceTFStructName }}, len(elements))
576
+ for _, elt := range elements {
577
+ stateObjectsByName[elt.Name.ValueString()] = &elt
578
+ }
579
+
580
+ objects := make([]{{ .structName }}{{ .ResourceOrDS }}{{ .ListAttribute.CamelCase }}Object, len(state.{{ .ListAttribute.CamelCase }}.Elements()))
581
+ for idx, elt := range existing {
582
+ if _, found := stateObjectsByName[elt.Name]; !found {
583
+ continue
584
+ }
585
+ var object {{ .structName }}{{ .ResourceOrDS }}{{ .ListAttribute.CamelCase }}Object
586
+ object.CopyFromPango(ctx, &elt, nil)
587
+ objects[idx] = object
588
+ }
589
+ {{- end }}
409
590
410
591
411
592
var list_diags diag.Diagnostics
@@ -598,6 +779,14 @@ for idx, elt := range stateEntries {
598
779
}
599
780
}
600
781
782
+ planEntriesByName := make(map[string]*entryWithState, len(planEntries))
783
+ for idx, elt := range planEntries {
784
+ planEntriesByName[elt.Name] = &entryWithState{
785
+ Entry: elt,
786
+ StateIdx: idx,
787
+ }
788
+ }
789
+
601
790
findMatchingStateEntry := func(entry *{{ $resourceSDKStructName }}) (*{{ $resourceSDKStructName }}, bool) {
602
791
var found *{{ $resourceSDKStructName }}
603
792
@@ -696,6 +885,16 @@ for _, existingElt := range existing {
696
885
resp.Diagnostics.AddError("Failed to create xpath for existing entry", err.Error())
697
886
}
698
887
888
+ _, foundInState := stateEntriesByName[existingElt.Name]
889
+ _, foundInRenamed := planEntriesByName[existingElt.Name]
890
+ _, foundInPlan := planEntriesByName[existingElt.Name]
891
+
892
+ if !foundInState && (foundInRenamed || foundInPlan) {
893
+ errorMsg := fmt.Sprintf("%s created outside of terraform", existingElt.Name)
894
+ resp.Diagnostics.AddError("Conflict between plan and PAN-OS data", errorMsg)
895
+ return
896
+ }
897
+
699
898
// If the existing entry name matches new name for the renamed entry,
700
899
// we delete it before adding Renamed commands.
701
900
if _, found := renamedEntries[existingElt.Name]; found {
@@ -708,6 +907,7 @@ for _, existingElt := range existing {
708
907
}
709
908
710
909
processedElt, found := processedStateEntries[existingElt.Name]
910
+ {{- if .Exhaustive }}
711
911
if !found {
712
912
// If existing entry is not found in the processedEntries map, it's not
713
913
// entry we are managing and it should be deleted.
@@ -716,7 +916,10 @@ for _, existingElt := range existing {
716
916
Xpath: util.AsXpath(path),
717
917
Target: r.client.GetTarget(),
718
918
})
719
- } else if processedElt.Entry.Uuid != nil && *processedElt.Entry.Uuid == *existingElt.Uuid {
919
+ continue
920
+ }
921
+ {{- end }}
922
+ if found && processedElt.Entry.Uuid != nil && *processedElt.Entry.Uuid == *existingElt.Uuid {
720
923
// XXX: If entry from the plan is in process of being renamed, and its content
721
924
// differs from what exists on the server we should switch its state to entryOutdated
722
925
// instead.
0 commit comments