From 0f0f1380f336dc8c90802c426599e2c1c61ac3c1 Mon Sep 17 00:00:00 2001 From: akinross Date: Thu, 2 Apr 2026 11:17:14 +0200 Subject: [PATCH 01/11] [bugfix] Fix update of resource_mso_schema_template_anp_epg_subnet to only PATCH attributes that are changed --- ...urce_mso_schema_template_anp_epg_subnet.go | 128 ++++++++++-------- 1 file changed, 68 insertions(+), 60 deletions(-) diff --git a/mso/resource_mso_schema_template_anp_epg_subnet.go b/mso/resource_mso_schema_template_anp_epg_subnet.go index 85b7d1c2..ae87d17c 100644 --- a/mso/resource_mso_schema_template_anp_epg_subnet.go +++ b/mso/resource_mso_schema_template_anp_epg_subnet.go @@ -69,22 +69,28 @@ func resourceMSOSchemaTemplateAnpEpgSubnet() *schema.Resource { Optional: true, Computed: true, }, + // When set to true the attribute will error on NDO 4.1 + // Error: "EPG: epg in Schema: schema, Template: template, 'Querier' is only supported for Bridge Domain subnets"{} + // This attribute is applicable to BD subnets only and investigation should be done regarding deprecation "querier": &schema.Schema{ Type: schema.TypeBool, Optional: true, Computed: true, }, "description": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Computed: true, - ValidateFunc: validation.StringLenBetween(1, 1000), + Type: schema.TypeString, + Optional: true, + // Computed: true, + ValidateFunc: validation.StringLenBetween(0, 1000), }, "no_default_gateway": &schema.Schema{ Type: schema.TypeBool, Optional: true, Computed: true, }, + // When set to true the attribute will error on NDO 4.1 + // Error: "EPG: epg in Schema: schema, Template: template, EPG Subnet 10.0.0.1/24 cannot be marked as primary"{} + // This attribute is applicable to BD subnets only and investigation should be done regarding deprecation "primary": &schema.Schema{ Type: schema.TypeBool, Optional: true, @@ -293,88 +299,90 @@ func resourceMSOSchemaTemplateAnpEpgSubnetUpdate(d *schema.ResourceData, m inter log.Printf("[DEBUG] Schema Template Anp Epg Subnet: Beginning Updating") msoClient := m.(*client.Client) - ips := d.Id() - - var schemaId string - if schema_id, ok := d.GetOk("schema_id"); ok { - schemaId = schema_id.(string) - } - - var templateName string - if template, ok := d.GetOk("template"); ok { - templateName = template.(string) - } - - var anpName string - if name, ok := d.GetOk("anp_name"); ok { - anpName = name.(string) - } + schemaId := d.Get("schema_id").(string) + templateName := d.Get("template").(string) + anpName := d.Get("anp_name").(string) + epgName := d.Get("epg_name").(string) - var epgName string - if name, ok := d.GetOk("epg_name"); ok { - epgName = name.(string) + conts, err := msoClient.GetViaURL(fmt.Sprintf("api/v1/schemas/%s", schemaId)) + if err != nil { + return err } - var ip string - if tempVar, ok := d.GetOk("ip"); ok { - ip = tempVar.(string) + index, err := fetchIndex(conts, templateName, anpName, epgName, d.Id()) + if err != nil { + return err } - var description string - if tempVar, ok := d.GetOk("description"); ok { - description = tempVar.(string) + // We are keeping this conditional as a defensive check since this should not be triggered + // This conditional is here in case a subnet is manually deleted between the read and update operation + if index == -1 { + return fmt.Errorf("The given subnet ip is not found") } - scope := "private" - if tempVar, ok := d.GetOk("scope"); ok { - scope = tempVar.(string) - } + updatePath := fmt.Sprintf("/templates/%s/anps/%s/epgs/%s/subnets/%d", templateName, anpName, epgName, index) + payloadCont := container.New() + payloadCont.Array() - shared := false - if tempVar, ok := d.GetOk("shared"); ok { - shared = tempVar.(bool) + if d.HasChange("ip") { + err := addPatchPayloadToContainer(payloadCont, "replace", fmt.Sprintf("%s/ip", updatePath), d.Get("ip").(string)) + if err != nil { + return err + } } - primary := false - if tempVar, ok := d.GetOk("primary"); ok { - primary = tempVar.(bool) + if d.HasChange("description") { + err := addPatchPayloadToContainer(payloadCont, "replace", fmt.Sprintf("%s/description", updatePath), d.Get("description").(string)) + if err != nil { + return err + } } - querier := false - if tempVar, ok := d.GetOk("querier"); ok { - querier = tempVar.(bool) + if d.HasChange("scope") { + scope := d.Get("scope").(string) + if scope == "" { + scope = "private" + } + err := addPatchPayloadToContainer(payloadCont, "replace", fmt.Sprintf("%s/scope", updatePath), scope) + if err != nil { + return err + } } - noDefaultGateway := false - if tempVar, ok := d.GetOk("no_default_gateway"); ok { - noDefaultGateway = tempVar.(bool) + if d.HasChange("shared") { + err := addPatchPayloadToContainer(payloadCont, "replace", fmt.Sprintf("%s/shared", updatePath), d.Get("shared").(bool)) + if err != nil { + return err + } } - conts, err := msoClient.GetViaURL(fmt.Sprintf("api/v1/schemas/%s", schemaId)) - if err != nil { - return err + if d.HasChange("querier") { + err := addPatchPayloadToContainer(payloadCont, "replace", fmt.Sprintf("%s/querier", updatePath), d.Get("querier").(bool)) + if err != nil { + return err + } } - index, err := fetchIndex(conts, templateName, anpName, epgName, ips) - if err != nil { - return err + if d.HasChange("no_default_gateway") { + err := addPatchPayloadToContainer(payloadCont, "replace", fmt.Sprintf("%s/noDefaultGateway", updatePath), d.Get("no_default_gateway").(bool)) + if err != nil { + return err + } } - if index == -1 { - fmt.Errorf("The given subnet ip is not found") + if d.HasChange("primary") { + err := addPatchPayloadToContainer(payloadCont, "replace", fmt.Sprintf("%s/primary", updatePath), d.Get("primary").(bool)) + if err != nil { + return err + } } - indexs := strconv.Itoa(index) - - schemaTemplateAnpEpgSubnetApp := models.NewSchemaTemplateAnpEpgSubnet("replace", fmt.Sprintf("/templates/%s/anps/%s/epgs/%s/subnets/%s", templateName, anpName, epgName, indexs), ip, description, scope, shared, noDefaultGateway, querier, primary) - - _, err = msoClient.PatchbyID(fmt.Sprintf("api/v1/schemas/%s", schemaId), schemaTemplateAnpEpgSubnetApp) + err = doPatchRequest(msoClient, fmt.Sprintf("api/v1/schemas/%s", schemaId), payloadCont) if err != nil { - log.Println(err) return err } - d.SetId(fmt.Sprintf("%v", ip)) + d.SetId(fmt.Sprintf("%v", d.Get("ip").(string))) log.Printf("[DEBUG] %s: Updating finished successfully", d.Id()) return resourceMSOSchemaTemplateAnpEpgSubnetRead(d, m) From 396b858e3eeec167b045380a1f3b5be07f98cfc2 Mon Sep 17 00:00:00 2001 From: akinross Date: Thu, 2 Apr 2026 11:17:33 +0200 Subject: [PATCH 02/11] [ignore] add tests for mso_schema_anp_epg_subnet resource and datasource --- ...mso_schema_template_anp_epg_subnet_test.go | 62 +++ ...mso_schema_template_anp_epg_subnet_test.go | 465 ++++++++++-------- mso/test_constants.go | 8 +- 3 files changed, 338 insertions(+), 197 deletions(-) create mode 100644 mso/datasource_mso_schema_template_anp_epg_subnet_test.go diff --git a/mso/datasource_mso_schema_template_anp_epg_subnet_test.go b/mso/datasource_mso_schema_template_anp_epg_subnet_test.go new file mode 100644 index 00000000..34e7c76b --- /dev/null +++ b/mso/datasource_mso_schema_template_anp_epg_subnet_test.go @@ -0,0 +1,62 @@ +package mso + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" +) + +func TestAccMSOSchemaTemplateAnpEpgSubnetDatasource(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckMSOSchemaTemplateAnpEpgSubnetDestroy, + Steps: []resource.TestStep{ + { + PreConfig: func() { fmt.Println("Test: Read EPG Subnet datasource") }, + Config: testAccMSOSchemaTemplateAnpEpgSubnetDatasource(), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("data.mso_schema_template_anp_epg_subnet.subnet", "schema_id"), + resource.TestCheckResourceAttr("data.mso_schema_template_anp_epg_subnet.subnet", "template", msoSchemaTemplateName), + resource.TestCheckResourceAttr("data.mso_schema_template_anp_epg_subnet.subnet", "anp_name", msoSchemaTemplateAnpName), + resource.TestCheckResourceAttr("data.mso_schema_template_anp_epg_subnet.subnet", "epg_name", msoSchemaTemplateAnpEpgName), + resource.TestCheckResourceAttr("data.mso_schema_template_anp_epg_subnet.subnet", "ip", msoSchemaTemplateAnpEpgSubnetIp), + resource.TestCheckResourceAttr("data.mso_schema_template_anp_epg_subnet.subnet", "scope", "private"), + resource.TestCheckResourceAttr("data.mso_schema_template_anp_epg_subnet.subnet", "shared", "false"), + resource.TestCheckResourceAttr("data.mso_schema_template_anp_epg_subnet.subnet", "querier", "false"), + resource.TestCheckResourceAttr("data.mso_schema_template_anp_epg_subnet.subnet", "no_default_gateway", "false"), + resource.TestCheckResourceAttr("data.mso_schema_template_anp_epg_subnet.subnet", "primary", "false"), + ), + }, + { + PreConfig: func() { fmt.Println("Test: Read EPG Subnet datasource not found error") }, + Config: testAccMSOSchemaTemplateAnpEpgSubnetDatasourceNotFound(), + ExpectError: regexp.MustCompile("Unable to find the ANP EPG Subnet"), + }, + }, + }) +} + +func testAccMSOSchemaTemplateAnpEpgSubnetDatasource() string { + return fmt.Sprintf(`%s + data "mso_schema_template_anp_epg_subnet" "subnet" { + schema_id = mso_schema.%[2]s.id + template = "%[3]s" + anp_name = "%[4]s" + epg_name = "%[5]s" + ip = mso_schema_template_anp_epg_subnet.%[6]s_subnet.ip + }`, testAccMSOSchemaTemplateAnpEpgSubnetConfigCreate(), msoSchemaName, msoSchemaTemplateName, msoSchemaTemplateAnpName, msoSchemaTemplateAnpEpgName, msoSchemaTemplateAnpEpgName) +} + +func testAccMSOSchemaTemplateAnpEpgSubnetDatasourceNotFound() string { + return fmt.Sprintf(`%s + data "mso_schema_template_anp_epg_subnet" "subnet" { + schema_id = mso_schema.%[2]s.id + template = "%[3]s" + anp_name = "%[4]s" + epg_name = "%[5]s" + ip = "99.99.99.99/32" + }`, testAccMSOSchemaTemplateAnpEpgSubnetConfigCreate(), msoSchemaName, msoSchemaTemplateName, msoSchemaTemplateAnpName, msoSchemaTemplateAnpEpgName) +} diff --git a/mso/resource_mso_schema_template_anp_epg_subnet_test.go b/mso/resource_mso_schema_template_anp_epg_subnet_test.go index d4d9fd76..eb1f296f 100644 --- a/mso/resource_mso_schema_template_anp_epg_subnet_test.go +++ b/mso/resource_mso_schema_template_anp_epg_subnet_test.go @@ -2,7 +2,6 @@ package mso import ( "fmt" - "strconv" "testing" "github.com/ciscoecosystem/mso-go-client/client" @@ -11,176 +10,291 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/terraform" ) -func TestAccMSOSchemaTemplateAnpEpgSubnet_Basic(t *testing.T) { - var ss SubnetTest +// Note: The `querier` and `primary` attributes are not tested because they are BD (Bridge Domain) specific attributes. +// - querier: API returns "EPG: in Schema: , Template: