From 5d8617e6302713b53de695b514a20a482d2850d7 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Mon, 1 Jul 2024 20:16:20 -0400 Subject: [PATCH 1/2] tests, etc... --- apstra/blueprint/freeform_config_template.go | 29 ++- .../data_source_freeform_config_template.go | 2 + apstra/export_test.go | 1 + ...eeform_config_template_integration_test.go | 170 ++++++++++++++++++ apstra/test_helpers_test.go | 8 + apstra/test_utils/blueprint.go | 15 ++ go.mod | 2 +- go.sum | 4 +- 8 files changed, 209 insertions(+), 22 deletions(-) create mode 100644 apstra/resource_freeform_config_template_integration_test.go diff --git a/apstra/blueprint/freeform_config_template.go b/apstra/blueprint/freeform_config_template.go index 1d3239e6..b0511d9e 100644 --- a/apstra/blueprint/freeform_config_template.go +++ b/apstra/blueprint/freeform_config_template.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "regexp" ) type FreeformConfigTemplate struct { @@ -21,7 +22,6 @@ type FreeformConfigTemplate struct { BlueprintId types.String `tfsdk:"blueprint_id"` Name types.String `tfsdk:"name"` Text types.String `tfsdk:"text"` - TemplateId types.String `tfsdk:"template_id"` Tags types.Set `tfsdk:"tags"` } @@ -45,11 +45,6 @@ func (o FreeformConfigTemplate) DataSourceAttributes() map[string]dataSourceSche }...), }, }, - "template_id": dataSourceSchema.StringAttribute{ - MarkdownDescription: "The Template ID of the configuration template in global catalog\n", - Computed: true, - Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, - }, "name": dataSourceSchema.StringAttribute{ MarkdownDescription: "Populate this field to look up an imported Config Template by `name`. Required when `id` is omitted.", Optional: true, @@ -81,17 +76,15 @@ func (o FreeformConfigTemplate) ResourceAttributes() map[string]resourceSchema.A "id": resourceSchema.StringAttribute{ MarkdownDescription: "ID of the Config Template.", Computed: true, - }, - "template_id": resourceSchema.StringAttribute{ - MarkdownDescription: "The template ID of the config template in the global catalog.", - Optional: true, - Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, - PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, }, "name": resourceSchema.StringAttribute{ - MarkdownDescription: "Config Template name as shown in the Web UI.", + MarkdownDescription: "Config Template name as shown in the Web UI. Must end with `.jinja`.", Required: true, - Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(7), + stringvalidator.RegexMatches(regexp.MustCompile(".jinja$"), "must end with '.jinja'"), + }, }, "text": resourceSchema.StringAttribute{ MarkdownDescription: "Configuration Jinja2 template text", @@ -115,16 +108,14 @@ func (o *FreeformConfigTemplate) Request(ctx context.Context, diags *diag.Diagno } return &apstra.ConfigTemplateData{ - Label: o.Name.ValueString(), - Text: o.Text.ValueString(), - Tags: tags, - TemplateId: apstra.ObjectId(o.TemplateId.ValueString()), + Label: o.Name.ValueString(), + Text: o.Text.ValueString(), + Tags: tags, } } func (o *FreeformConfigTemplate) LoadApiData(ctx context.Context, in *apstra.ConfigTemplateData, diags *diag.Diagnostics) { o.Name = types.StringValue(in.Label) o.Text = types.StringValue(in.Text) - o.TemplateId = types.StringValue(string(in.TemplateId)) o.Tags = utils.SetValueOrNull(ctx, types.StringType, in.Tags, diags) // safe to ignore diagnostic here } diff --git a/apstra/data_source_freeform_config_template.go b/apstra/data_source_freeform_config_template.go index 58644d8c..ffa11364 100644 --- a/apstra/data_source_freeform_config_template.go +++ b/apstra/data_source_freeform_config_template.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" ) var _ datasource.DataSourceWithConfigure = &dataSourceFreeformConfigTemplate{} @@ -81,6 +82,7 @@ func (o *dataSourceFreeformConfigTemplate) Read(ctx context.Context, req datasou return } + config.Id = types.StringValue(api.Id.String()) config.LoadApiData(ctx, api.Data, &resp.Diagnostics) if resp.Diagnostics.HasError() { return diff --git a/apstra/export_test.go b/apstra/export_test.go index 01f0ba1d..551baf52 100644 --- a/apstra/export_test.go +++ b/apstra/export_test.go @@ -10,6 +10,7 @@ var ( ResourceAgentProfile = resourceAgentProfile{} ResourceDatacenterGenericSystem = resourceDatacenterGenericSystem{} ResourceDatacenterRoutingZone = resourceDatacenterRoutingZone{} + ResourceFreeformConfigTemplate = resourceFreeformConfigTemplate{} ResourceIpv4Pool = resourceIpv4Pool{} ResourceTemplatePodBased = resourceTemplatePodBased{} ResourceTemplateCollapsed = resourceTemplateCollapsed{} diff --git a/apstra/resource_freeform_config_template_integration_test.go b/apstra/resource_freeform_config_template_integration_test.go new file mode 100644 index 00000000..cb95882f --- /dev/null +++ b/apstra/resource_freeform_config_template_integration_test.go @@ -0,0 +1,170 @@ +//go:build integration + +package tfapstra_test + +import ( + "context" + "fmt" + tfapstra "github.com/Juniper/terraform-provider-apstra/apstra" + testutils "github.com/Juniper/terraform-provider-apstra/apstra/test_utils" + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "math/rand" + "strconv" + "testing" +) + +const ( + resourceFreeformConfigTemplateHcl = ` +resource %q %q { + blueprint_id = %q + name = %q + text = %q + tags = %s +} +` +) + +type resourceFreeformConfigTemplate struct { + blueprintId string + name string + text string + tags []string +} + +func (o resourceFreeformConfigTemplate) render(rType, rName string) string { + return fmt.Sprintf(resourceFreeformConfigTemplateHcl, + rType, rName, + o.blueprintId, + o.name, + o.text, + stringSetOrNull(o.tags), + ) +} + +func (o resourceFreeformConfigTemplate) testChecks(t testing.TB, rType, rName string) testChecks { + result := newTestChecks(rType + "." + rName) + + // required and computed attributes can always be checked + result.append(t, "TestCheckResourceAttrSet", "id") + result.append(t, "TestCheckResourceAttr", "blueprint_id", o.blueprintId) + result.append(t, "TestCheckResourceAttr", "name", o.name) + result.append(t, "TestCheckResourceAttr", "text", o.text) + + if len(o.tags) > 0 { + result.append(t, "TestCheckResourceAttr", "tags.#", strconv.Itoa(len(o.tags))) + for _, tag := range o.tags { + result.append(t, "TestCheckTypeSetElemAttr", "tags.*", tag) + } + } + + return result +} + +func TestResourceFreeformConfigTemplate(t *testing.T) { + ctx := context.Background() + client := testutils.GetTestClient(t, ctx) + apiVersion := version.Must(version.NewVersion(client.ApiVersion())) + + // create a blueprint + bp := testutils.FfBlueprintA(t, ctx) + + type testStep struct { + config resourceFreeformConfigTemplate + } + type testCase struct { + apiVersionConstraints version.Constraints + steps []testStep + } + + testCases := map[string]testCase{ + "start_with_no_tags": { + steps: []testStep{ + { + config: resourceFreeformConfigTemplate{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6) + ".jinja", + text: acctest.RandString(6), + }, + }, + { + config: resourceFreeformConfigTemplate{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6) + ".jinja", + text: acctest.RandString(6), + tags: randomStrings(rand.Intn(10)+2, 6), + }, + }, + { + config: resourceFreeformConfigTemplate{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6) + ".jinja", + text: acctest.RandString(6), + }, + }, + }, + }, + "start_with_tags": { + steps: []testStep{ + { + config: resourceFreeformConfigTemplate{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6) + ".jinja", + text: acctest.RandString(6), + tags: randomStrings(rand.Intn(10)+2, 6), + }, + }, + { + config: resourceFreeformConfigTemplate{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6) + ".jinja", + text: acctest.RandString(6), + }, + }, + { + config: resourceFreeformConfigTemplate{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6) + ".jinja", + text: acctest.RandString(6), + tags: randomStrings(rand.Intn(10)+2, 6), + }, + }, + }, + }, + } + + resourceType := tfapstra.ResourceName(ctx, &tfapstra.ResourceFreeformConfigTemplate) + + for tName, tCase := range testCases { + tName, tCase := tName, tCase + t.Run(tName, func(t *testing.T) { + t.Parallel() + if !tCase.apiVersionConstraints.Check(apiVersion) { + t.Skipf("test case %s requires Apstra %s", tName, tCase.apiVersionConstraints.String()) + } + + steps := make([]resource.TestStep, len(tCase.steps)) + for i, step := range tCase.steps { + config := step.config.render(resourceType, tName) + checks := step.config.testChecks(t, resourceType, tName) + + chkLog := checks.string() + stepName := fmt.Sprintf("test case %q step %d", tName, i+1) + + t.Logf("\n// ------ begin config for %s ------\n%s// -------- end config for %s ------\n\n", stepName, config, stepName) + t.Logf("\n// ------ begin checks for %s ------\n%s// -------- end checks for %s ------\n\n", stepName, chkLog, stepName) + + steps[i] = resource.TestStep{ + Config: insecureProviderConfigHCL + config, + Check: resource.ComposeAggregateTestCheckFunc(checks.checks...), + } + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: steps, + }) + }) + } +} diff --git a/apstra/test_helpers_test.go b/apstra/test_helpers_test.go index 2f8ac6e0..956fe33f 100644 --- a/apstra/test_helpers_test.go +++ b/apstra/test_helpers_test.go @@ -219,6 +219,14 @@ func randomIPs(t testing.TB, n int, ipv4Cidr, ipv6Cidr string) []string { return result } +func randomStrings(strCount int, strLen int) []string { + result := make([]string, strCount) + for i := 0; i < strCount; i++ { + result[i] = acctest.RandString(strLen) + } + return result +} + type lineNumberer struct { lines []string base int diff --git a/apstra/test_utils/blueprint.go b/apstra/test_utils/blueprint.go index dfd54e52..99f03d35 100644 --- a/apstra/test_utils/blueprint.go +++ b/apstra/test_utils/blueprint.go @@ -228,3 +228,18 @@ func BlueprintF(t testing.TB, ctx context.Context) *apstra.TwoStageL3ClosClient return bpClient } + +func FfBlueprintA(t testing.TB, ctx context.Context) *apstra.FreeformClient { + t.Helper() + + client := GetTestClient(t, ctx) + + id, err := client.CreateFreeformBlueprint(ctx, acctest.RandString(6)) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, client.DeleteBlueprint(ctx, id)) }) + + bpClient, err := client.NewFreeformClient(ctx, id) + require.NoError(t, err) + + return bpClient +} diff --git a/go.mod b/go.mod index fb935ded..7791be07 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ toolchain go1.21.1 require ( github.com/IBM/netaddr v1.5.0 - github.com/Juniper/apstra-go-sdk v0.0.0-20240701173510-5c1fd83e151f + github.com/Juniper/apstra-go-sdk v0.0.0-20240701235719-e1497887d8d7 github.com/apparentlymart/go-cidr v1.1.0 github.com/chrismarget-j/go-licenses v0.0.0-20240224210557-f22f3e06d3d4 github.com/google/go-cmp v0.6.0 diff --git a/go.sum b/go.sum index 9d6b069e..cfa70871 100644 --- a/go.sum +++ b/go.sum @@ -108,8 +108,8 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/IBM/netaddr v1.5.0 h1:IJlFZe1+nFs09TeMB/HOP4+xBnX2iM/xgiDOgZgTJq0= github.com/IBM/netaddr v1.5.0/go.mod h1:DDBPeYgbFzoXHjSz9Jwk7K8wmWV4+a/Kv0LqRnb8we4= -github.com/Juniper/apstra-go-sdk v0.0.0-20240701173510-5c1fd83e151f h1:xmPPD2XieSkuu4robT6u5/zVXKJ3JTQ9suFw5GPyzpk= -github.com/Juniper/apstra-go-sdk v0.0.0-20240701173510-5c1fd83e151f/go.mod h1:Xwj3X8v/jRZWv28o6vQAqD4lz2JmzaSYLZ2ch1SS89w= +github.com/Juniper/apstra-go-sdk v0.0.0-20240701235719-e1497887d8d7 h1:omV9UgPnuSppETNuMqTqvNBXFXy8ExyG5rZUnMe7UgY= +github.com/Juniper/apstra-go-sdk v0.0.0-20240701235719-e1497887d8d7/go.mod h1:Xwj3X8v/jRZWv28o6vQAqD4lz2JmzaSYLZ2ch1SS89w= github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0= github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= From c00a71166ef932f4b89661807c7d4bcc3873f416 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Mon, 1 Jul 2024 20:38:52 -0400 Subject: [PATCH 2/2] update docs --- docs/data-sources/datacenter_configlet.md | 7 +-- docs/data-sources/freeform_config_template.md | 17 ++----- docs/resources/freeform_config_template.md | 45 ++++++++++++++----- .../apstra_datacenter_configlet/example.tf | 7 +-- .../example.tf | 16 ++----- .../example.tf | 42 +++++++++++++---- 6 files changed, 83 insertions(+), 51 deletions(-) diff --git a/docs/data-sources/datacenter_configlet.md b/docs/data-sources/datacenter_configlet.md index 464182f6..01974ba1 100644 --- a/docs/data-sources/datacenter_configlet.md +++ b/docs/data-sources/datacenter_configlet.md @@ -16,10 +16,11 @@ At least one optional attribute is required. ## Example Usage ```terraform -# This example uses the `apstra_datacenter_configlets` data source to get a list -# of all imported configlets, and then uses the apstra_datacenter_configlet data source -# to inspect the results +data "apstra_freeform_config_template" "interfaces" { + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + name = "interfaces.jinja" +} data "apstra_datacenter_blueprint" "b" { name = "test" diff --git a/docs/data-sources/freeform_config_template.md b/docs/data-sources/freeform_config_template.md index 05401840..15a21057 100644 --- a/docs/data-sources/freeform_config_template.md +++ b/docs/data-sources/freeform_config_template.md @@ -16,19 +16,11 @@ At least one optional attribute is required. ## Example Usage ```terraform -resource "apstra_freeform_config_template" "test" { - blueprint_id = "freeform_blueprint-5ba09d07" - name = "test_conf_template.jinja" - tags = ["a", "b", "c"] - text = "this is a test for a config template." +# The following example retrieves a Config Template from a Freeform Blueprint +data "apstra_freeform_config_template" "interfaces" { + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + name = "interfaces.jinja" } - -data "apstra_freeform_config_template" "test" { - blueprint_id = "freeform_blueprint-5ba09d07" - id = apstra_freeform_config_template.test.id -} - -output "test_out" {value = data.apstra_freeform_config_template.test} ``` @@ -46,5 +38,4 @@ output "test_out" {value = data.apstra_freeform_config_template.test} ### Read-Only -- `template_id` (String) The Template ID of the configuration template in global catalog - `text` (String) Configuration Jinja2 template text diff --git a/docs/resources/freeform_config_template.md b/docs/resources/freeform_config_template.md index e44293f0..46b5c33f 100644 --- a/docs/resources/freeform_config_template.md +++ b/docs/resources/freeform_config_template.md @@ -13,19 +13,43 @@ This resource creates a Config Template in a Freeform Blueprint. ## Example Usage ```terraform -resource "apstra_freeform_config_template" "test" { - blueprint_id = "freeform_blueprint-5ba09d07" - name = "test_conf_template.jinja" - tags = ["a", "b", "c"] - text = "this is a test for a config template." +# This example creates a Config Template from a local jinja file. +locals { + template_filename = "interfaces.jinja" } -data "apstra_freeform_config_template" "test" { - blueprint_id = "freeform_blueprint-5ba09d07" - id = apstra_freeform_config_template.test.id +resource "apstra_freeform_config_template" "test" { + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + name = local.template_filename + tags = ["a", "b", "c"] + text = file("${path.module}/${local.template_filename}") } -output "test_out" {value = data.apstra_freeform_config_template.test} +/* +# Contents of the interfaces.jinja file in this directory follows here: +{% set this_router=hostname %} +interfaces { +{% for interface_name, iface in interfaces.items() %} + replace: {{ interface_name }} { + unit 0 { + description "{{iface['description']}}"; + {% if iface['ipv4_address'] and iface['ipv4_prefixlen'] %} + family inet { + address {{iface['ipv4_address']}}/{{iface['ipv4_prefixlen']}}; + } + {% endif %} + } + } +{% endfor %} + replace: lo0 { + unit 0 { + family inet { + address {{ property_sets.data[this_router | replace('-', '_')]['loopback'] }}/32; + } + } + } +} +*/ ``` @@ -34,13 +58,12 @@ output "test_out" {value = data.apstra_freeform_config_template.test} ### Required - `blueprint_id` (String) Apstra Blueprint ID. -- `name` (String) Config Template name as shown in the Web UI. +- `name` (String) Config Template name as shown in the Web UI. Must end with `.jinja`. - `text` (String) Configuration Jinja2 template text ### Optional - `tags` (Set of String) Set of Tag labels -- `template_id` (String) The template ID of the config template in the global catalog. ### Read-Only diff --git a/examples/data-sources/apstra_datacenter_configlet/example.tf b/examples/data-sources/apstra_datacenter_configlet/example.tf index 28d4dc17..2ba35ac4 100644 --- a/examples/data-sources/apstra_datacenter_configlet/example.tf +++ b/examples/data-sources/apstra_datacenter_configlet/example.tf @@ -1,7 +1,8 @@ -# This example uses the `apstra_datacenter_configlets` data source to get a list -# of all imported configlets, and then uses the apstra_datacenter_configlet data source -# to inspect the results +data "apstra_freeform_config_template" "interfaces" { + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + name = "interfaces.jinja" +} data "apstra_datacenter_blueprint" "b" { name = "test" diff --git a/examples/data-sources/apstra_freeform_config_template/example.tf b/examples/data-sources/apstra_freeform_config_template/example.tf index 9b28335d..08f1b55f 100644 --- a/examples/data-sources/apstra_freeform_config_template/example.tf +++ b/examples/data-sources/apstra_freeform_config_template/example.tf @@ -1,13 +1,5 @@ -resource "apstra_freeform_config_template" "test" { - blueprint_id = "freeform_blueprint-5ba09d07" - name = "test_conf_template.jinja" - tags = ["a", "b", "c"] - text = "this is a test for a config template." +# The following example retrieves a Config Template from a Freeform Blueprint +data "apstra_freeform_config_template" "interfaces" { + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + name = "interfaces.jinja" } - -data "apstra_freeform_config_template" "test" { - blueprint_id = "freeform_blueprint-5ba09d07" - id = apstra_freeform_config_template.test.id -} - -output "test_out" {value = data.apstra_freeform_config_template.test} diff --git a/examples/resources/apstra_freeform_config_template/example.tf b/examples/resources/apstra_freeform_config_template/example.tf index 9b28335d..7223fe6d 100644 --- a/examples/resources/apstra_freeform_config_template/example.tf +++ b/examples/resources/apstra_freeform_config_template/example.tf @@ -1,13 +1,37 @@ -resource "apstra_freeform_config_template" "test" { - blueprint_id = "freeform_blueprint-5ba09d07" - name = "test_conf_template.jinja" - tags = ["a", "b", "c"] - text = "this is a test for a config template." +# This example creates a Config Template from a local jinja file. +locals { + template_filename = "interfaces.jinja" } -data "apstra_freeform_config_template" "test" { - blueprint_id = "freeform_blueprint-5ba09d07" - id = apstra_freeform_config_template.test.id +resource "apstra_freeform_config_template" "test" { + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + name = local.template_filename + tags = ["a", "b", "c"] + text = file("${path.module}/${local.template_filename}") } -output "test_out" {value = data.apstra_freeform_config_template.test} +/* +# Contents of the interfaces.jinja file in this directory follows here: +{% set this_router=hostname %} +interfaces { +{% for interface_name, iface in interfaces.items() %} + replace: {{ interface_name }} { + unit 0 { + description "{{iface['description']}}"; + {% if iface['ipv4_address'] and iface['ipv4_prefixlen'] %} + family inet { + address {{iface['ipv4_address']}}/{{iface['ipv4_prefixlen']}}; + } + {% endif %} + } + } +{% endfor %} + replace: lo0 { + unit 0 { + family inet { + address {{ property_sets.data[this_router | replace('-', '_')]['loopback'] }}/32; + } + } + } +} +*/ \ No newline at end of file