Skip to content

Commit 1509ef9

Browse files
committed
feat(terraform): Set default vsys for template and template stacks on create
1 parent 2a5f22c commit 1509ef9

16 files changed

Lines changed: 618 additions & 52 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
6+
"github.com/PaloAltoNetworks/pango/panorama/template"
7+
"github.com/hashicorp/terraform-plugin-framework/resource"
8+
"github.com/hashicorp/terraform-plugin-log/tflog"
9+
)
10+
11+
// TemplateCustom stores state shared between PreCreate and PostCreate hooks.
12+
type TemplateCustom struct {
13+
savedDefaultVsys *string
14+
}
15+
16+
func NewTemplateCustom(data *ProviderData) (*TemplateCustom, error) {
17+
return &TemplateCustom{}, nil
18+
}
19+
20+
// PreCreate saves and strips default_vsys from the SDK object before Create,
21+
// because PAN-OS cannot set this field during initial creation.
22+
func (o *TemplateResource) PreCreate(
23+
ctx context.Context,
24+
req resource.CreateRequest,
25+
resp *resource.CreateResponse,
26+
state *TemplateResourceModel,
27+
location template.Location,
28+
obj *template.Entry,
29+
ev *EncryptedValuesManager,
30+
) {
31+
o.custom.savedDefaultVsys = obj.DefaultVsys
32+
obj.DefaultVsys = nil
33+
}
34+
35+
// PostCreate restores default_vsys via an Update call after the resource
36+
// has been successfully created.
37+
func (o *TemplateResource) PostCreate(
38+
ctx context.Context,
39+
req resource.CreateRequest,
40+
resp *resource.CreateResponse,
41+
state *TemplateResourceModel,
42+
location template.Location,
43+
obj *template.Entry,
44+
ev *EncryptedValuesManager,
45+
) {
46+
if o.custom.savedDefaultVsys == nil {
47+
return
48+
}
49+
50+
tflog.Info(ctx, "performing post-create update to set default_vsys", map[string]any{
51+
"resource_name": "panos_template",
52+
"name": state.Name.ValueString(),
53+
"default_vsys": *o.custom.savedDefaultVsys,
54+
})
55+
56+
components, err := state.resourceXpathParentComponents()
57+
if err != nil {
58+
resp.Diagnostics.AddError("Error creating resource xpath for post-create update", err.Error())
59+
return
60+
}
61+
62+
obj.DefaultVsys = o.custom.savedDefaultVsys
63+
64+
updated, err := o.manager.Update(ctx, location, components, obj, "")
65+
if err != nil {
66+
resp.Diagnostics.AddError(
67+
"Error setting default_vsys after create",
68+
"Template created successfully but setting default_vsys failed. "+
69+
"Run terraform apply again to retry. Error: "+err.Error(),
70+
)
71+
return
72+
}
73+
74+
resp.Diagnostics.Append(state.CopyFromPango(ctx, o.client, nil, updated, ev)...)
75+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
6+
"github.com/PaloAltoNetworks/pango/panorama/template_stack"
7+
"github.com/hashicorp/terraform-plugin-framework/resource"
8+
"github.com/hashicorp/terraform-plugin-log/tflog"
9+
)
10+
11+
// TemplateStackCustom stores state shared between PreCreate and PostCreate hooks.
12+
type TemplateStackCustom struct {
13+
savedDefaultVsys *string
14+
}
15+
16+
func NewTemplateStackCustom(data *ProviderData) (*TemplateStackCustom, error) {
17+
return &TemplateStackCustom{}, nil
18+
}
19+
20+
// PreCreate saves and strips default_vsys from the SDK object before Create,
21+
// because PAN-OS cannot set this field during initial creation.
22+
func (o *TemplateStackResource) PreCreate(
23+
ctx context.Context,
24+
req resource.CreateRequest,
25+
resp *resource.CreateResponse,
26+
state *TemplateStackResourceModel,
27+
location template_stack.Location,
28+
obj *template_stack.Entry,
29+
ev *EncryptedValuesManager,
30+
) {
31+
o.custom.savedDefaultVsys = obj.DefaultVsys
32+
obj.DefaultVsys = nil
33+
}
34+
35+
// PostCreate restores default_vsys via an Update call after the resource
36+
// has been successfully created.
37+
func (o *TemplateStackResource) PostCreate(
38+
ctx context.Context,
39+
req resource.CreateRequest,
40+
resp *resource.CreateResponse,
41+
state *TemplateStackResourceModel,
42+
location template_stack.Location,
43+
obj *template_stack.Entry,
44+
ev *EncryptedValuesManager,
45+
) {
46+
if o.custom.savedDefaultVsys == nil {
47+
return
48+
}
49+
50+
tflog.Info(ctx, "performing post-create update to set default_vsys", map[string]any{
51+
"resource_name": "panos_template_stack",
52+
"name": state.Name.ValueString(),
53+
"default_vsys": *o.custom.savedDefaultVsys,
54+
})
55+
56+
components, err := state.resourceXpathParentComponents()
57+
if err != nil {
58+
resp.Diagnostics.AddError("Error creating resource xpath for post-create update", err.Error())
59+
return
60+
}
61+
62+
obj.DefaultVsys = o.custom.savedDefaultVsys
63+
64+
updated, err := o.manager.Update(ctx, location, components, obj, "")
65+
if err != nil {
66+
resp.Diagnostics.AddError(
67+
"Error setting default_vsys after create",
68+
"Template stack created successfully but setting default_vsys failed. "+
69+
"Run terraform apply again to retry. Error: "+err.Error(),
70+
)
71+
return
72+
}
73+
74+
resp.Diagnostics.Append(state.CopyFromPango(ctx, o.client, nil, updated, ev)...)
75+
}

assets/terraform/test/resource_panorama_template_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package provider_test
22

33
import (
4+
"context"
45
"fmt"
56
"testing"
67

8+
"github.com/PaloAltoNetworks/pango/xmlapi"
79
"github.com/hashicorp/terraform-plugin-testing/config"
810
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
911
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
@@ -40,6 +42,103 @@ func TestAccPanosTemplate_RequiredInputs(t *testing.T) {
4042
})
4143
}
4244

45+
// TestAccPanosTemplate_DefaultVsys verifies that default_vsys can be set
46+
// during the initial create step. The CRUD hooks strip default_vsys from the
47+
// create call and set it via a post-create update.
48+
func TestAccPanosTemplate_DefaultVsys(t *testing.T) {
49+
t.Parallel()
50+
51+
nameSuffix := acctest.RandStringFromCharSet(6, acctest.CharSetAlphaNum)
52+
prefix := fmt.Sprintf("test-acc-%s", nameSuffix)
53+
templateName := fmt.Sprintf("%s-template", prefix)
54+
55+
location := config.ObjectVariable(map[string]config.Variable{
56+
"panorama": config.ObjectVariable(map[string]config.Variable{}),
57+
})
58+
59+
configVars := map[string]config.Variable{
60+
"prefix": config.StringVariable(prefix),
61+
"location": location,
62+
}
63+
64+
// injectVsys adds vsys1 to the template via direct API call so that
65+
// default_vsys can reference it. This runs after step 1 creates the
66+
// template but before step 2 sets default_vsys.
67+
injectVsys := func() {
68+
vsysXpath := fmt.Sprintf(
69+
"/config/devices/entry[@name='localhost.localdomain']/template/entry[@name='%s']/config/devices/entry[@name='localhost.localdomain']/vsys",
70+
templateName,
71+
)
72+
cmd := &xmlapi.Config{
73+
Action: "set",
74+
Xpath: vsysXpath,
75+
Element: "<entry name=\"vsys1\"/>",
76+
Target: sdkClient.GetTarget(),
77+
}
78+
if _, _, err := sdkClient.Communicate(context.TODO(), cmd, false, nil); err != nil {
79+
t.Fatalf("Failed to inject vsys into template: %v", err)
80+
}
81+
}
82+
83+
resource.Test(t, resource.TestCase{
84+
PreCheck: func() { testAccPreCheck(t) },
85+
ProtoV6ProviderFactories: testAccProviders,
86+
Steps: []resource.TestStep{
87+
// Step 1: create template without default_vsys
88+
{
89+
Config: template_DefaultVsys_Step1_Tmpl,
90+
ConfigVariables: configVars,
91+
ConfigStateChecks: []statecheck.StateCheck{
92+
statecheck.ExpectKnownValue(
93+
"panos_template.test",
94+
tfjsonpath.New("name"),
95+
knownvalue.StringExact(templateName),
96+
),
97+
},
98+
},
99+
// Step 2: inject vsys, then update template to set default_vsys
100+
{
101+
PreConfig: injectVsys,
102+
Config: template_DefaultVsys_Step2_Tmpl,
103+
ConfigVariables: configVars,
104+
ConfigStateChecks: []statecheck.StateCheck{
105+
statecheck.ExpectKnownValue(
106+
"panos_template.test",
107+
tfjsonpath.New("name"),
108+
knownvalue.StringExact(templateName),
109+
),
110+
statecheck.ExpectKnownValue(
111+
"panos_template.test",
112+
tfjsonpath.New("default_vsys"),
113+
knownvalue.StringExact("vsys1"),
114+
),
115+
},
116+
},
117+
},
118+
})
119+
}
120+
121+
const template_DefaultVsys_Step1_Tmpl = `
122+
variable "prefix" { type = string }
123+
variable "location" { type = any }
124+
125+
resource "panos_template" "test" {
126+
location = var.location
127+
name = "${var.prefix}-template"
128+
}
129+
`
130+
131+
const template_DefaultVsys_Step2_Tmpl = `
132+
variable "prefix" { type = string }
133+
variable "location" { type = any }
134+
135+
resource "panos_template" "test" {
136+
location = var.location
137+
name = "${var.prefix}-template"
138+
default_vsys = "vsys1"
139+
}
140+
`
141+
43142
func makePanosTemplateConfig(label string) string {
44143
configTpl := `
45144
variable "template_name" { type = string }

0 commit comments

Comments
 (0)