Skip to content

Commit 6ab060d

Browse files
committed
WIP: Adding support for Create() and Delete() of non-exhaustive lists
1 parent e042a69 commit 6ab060d

File tree

2 files changed

+232
-21
lines changed

2 files changed

+232
-21
lines changed

pkg/translate/terraform_provider/funcs.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1614,10 +1614,16 @@ func ResourceCreateFunction(resourceTyp properties.ResourceType, names *NameProv
16141614

16151615
var tmpl string
16161616
var listAttribute string
1617+
var exhaustive bool
16171618
switch resourceTyp {
16181619
case properties.ResourceEntry:
16191620
tmpl = resourceCreateFunction
1620-
default:
1621+
case properties.ResourceUuid:
1622+
exhaustive = true
1623+
tmpl = resourceCreateManyFunction
1624+
listAttribute = pascalCase(paramSpec.TerraformProviderConfig.PluralName)
1625+
case properties.ResourceUuidPlural:
1626+
exhaustive = false
16211627
tmpl = resourceCreateManyFunction
16221628
listAttribute = pascalCase(paramSpec.TerraformProviderConfig.PluralName)
16231629
}
@@ -1630,6 +1636,7 @@ func ResourceCreateFunction(resourceTyp properties.ResourceType, names *NameProv
16301636

16311637
data := map[string]interface{}{
16321638
"HasEncryptedResources": paramSpec.HasEncryptedResources(),
1639+
"Exhaustive": exhaustive,
16331640
"ListAttribute": listAttributeVariant,
16341641
"EntryOrConfig": paramSpec.EntryOrConfig(),
16351642
"HasEntryName": paramSpec.HasEntryName(),
@@ -1724,6 +1731,7 @@ func ResourceReadFunction(resourceTyp properties.ResourceType, names *NameProvid
17241731
"EntryOrConfig": paramSpec.EntryOrConfig(),
17251732
"HasEntryName": paramSpec.HasEntryName(),
17261733
"structName": names.StructName,
1734+
"datasourceStructName": names.DataSourceStructName,
17271735
"resourceStructName": names.ResourceStructName,
17281736
"serviceName": naming.CamelCase("", serviceName, "", false),
17291737
"resourceSDKName": resourceSDKName,

pkg/translate/terraform_provider/template.go

Lines changed: 223 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,9 @@ func (r *{{ resourceStructName }}) ImportState(ctx context.Context, req resource
203203
{{- /* Done */ -}}`
204204

205205
const resourceCreateManyFunction = `
206+
{{ $resourceSDKStructName := printf "%s.%s" .resourceSDKName .EntryOrConfig }}
207+
{{ $resourceTFStructName := printf "%s%sObject" .structName .ListAttribute.CamelCase }}
208+
206209
var state, createdState {{ .structName }}Model
207210
resp.Diagnostics.Append(req.Plan.Get(ctx, &state)...)
208211
if resp.Diagnostics.HasError() {
@@ -215,37 +218,188 @@ tflog.Info(ctx, "performing resource create", map[string]any{
215218
"function": "Create",
216219
})
217220
218-
svc := {{ .resourceSDKName }}.NewService(r.client)
219-
220221
var location {{ .resourceSDKName }}.Location
221222
{{ RenderLocationsStateToPango "state.Location" "location" }}
222223
223-
var elements []{{ .structName }}{{ .ListAttribute.CamelCase }}Object
224+
var elements []{{ $resourceTFStructName }}
224225
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))
226233
for idx, elt := range elements {
227234
var list_diags diag.Diagnostics
228-
var entry *{{ .resourceSDKName }}.{{ .EntryOrConfig }}
235+
var entry *{{ $resourceSDKStructName }}
229236
entry, list_diags = elt.CopyToPango(ctx, nil)
230237
resp.Diagnostics.Append(list_diags...)
231238
if resp.Diagnostics.HasError() {
232239
return
233240
}
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
235375
}
376+
{{- end }}
236377
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)
240385
if err != nil {
241-
resp.Diagnostics.AddError("SDK error during create", err.Error())
386+
resp.Diagnostics.AddError("Failed to reorder entries", err.Error())
242387
return
243388
}
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...)
246396
objects[idx] = object
247397
}
248398
399+
if resp.Diagnostics.HasError() {
400+
return
401+
}
402+
249403
var list_diags diag.Diagnostics
250404
createdState.Location = state.Location
251405
createdState.{{ .ListAttribute.CamelCase }}, list_diags = types.ListValueFrom(ctx, state.getTypeFor("{{ .ListAttribute.Underscore }}"), objects)
@@ -363,6 +517,17 @@ const resourceCreateFunction = `
363517
`
364518

365519
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+
366531
{{- $stateName := "" }}
367532
{{- if eq .ResourceOrDS "DataSource" }}
368533
{{- $stateName = "Config" }}
@@ -393,19 +558,35 @@ if err != nil {
393558
return
394559
}
395560
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.
402564
objects := make([]{{ .structName }}{{ .ResourceOrDS }}{{ .ListAttribute.CamelCase }}Object, len(existing))
403565
for idx, elt := range existing {
404-
fmt.Printf("Read() entry: %v %v", elt.Name, *elt.Uuid)
405566
var object {{ .structName }}{{ .ResourceOrDS }}{{ .ListAttribute.CamelCase }}Object
406567
object.CopyFromPango(ctx, &elt, nil)
407568
objects[idx] = object
408569
}
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 }}
409590
410591
411592
var list_diags diag.Diagnostics
@@ -598,6 +779,14 @@ for idx, elt := range stateEntries {
598779
}
599780
}
600781
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+
601790
findMatchingStateEntry := func(entry *{{ $resourceSDKStructName }}) (*{{ $resourceSDKStructName }}, bool) {
602791
var found *{{ $resourceSDKStructName }}
603792
@@ -696,6 +885,16 @@ for _, existingElt := range existing {
696885
resp.Diagnostics.AddError("Failed to create xpath for existing entry", err.Error())
697886
}
698887
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+
699898
// If the existing entry name matches new name for the renamed entry,
700899
// we delete it before adding Renamed commands.
701900
if _, found := renamedEntries[existingElt.Name]; found {
@@ -708,6 +907,7 @@ for _, existingElt := range existing {
708907
}
709908
710909
processedElt, found := processedStateEntries[existingElt.Name]
910+
{{- if .Exhaustive }}
711911
if !found {
712912
// If existing entry is not found in the processedEntries map, it's not
713913
// entry we are managing and it should be deleted.
@@ -716,7 +916,10 @@ for _, existingElt := range existing {
716916
Xpath: util.AsXpath(path),
717917
Target: r.client.GetTarget(),
718918
})
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 {
720923
// XXX: If entry from the plan is in process of being renamed, and its content
721924
// differs from what exists on the server we should switch its state to entryOutdated
722925
// instead.

0 commit comments

Comments
 (0)