From 76a8f089d4ff60b5b545f41a87080758d52ec4eb Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Thu, 19 Dec 2024 17:21:18 -0500 Subject: [PATCH 1/7] routing zone constraint resource todo: - resource tests - constraint data source - constraints data source --- apstra/provider.go | 1 + .../datacenter_routing_zone_constraint.md | 68 +++++++++++++++++++ .../example.tf | 28 ++++++++ 3 files changed, 97 insertions(+) create mode 100644 docs/resources/datacenter_routing_zone_constraint.md create mode 100644 examples/resources/apstra_datacenter_routing_zone_constraint/example.tf diff --git a/apstra/provider.go b/apstra/provider.go index 7fd86d45..874f7ae2 100644 --- a/apstra/provider.go +++ b/apstra/provider.go @@ -628,6 +628,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { func() resource.Resource { return &resourceDatacenterPropertySet{} }, func() resource.Resource { return &resourceDatacenterRack{} }, func() resource.Resource { return &resourceDatacenterRoutingZone{} }, + func() resource.Resource { return &resourceDatacenterRoutingZoneConstraint{} }, func() resource.Resource { return &resourceDatacenterRoutingPolicy{} }, func() resource.Resource { return &resourceDatacenterSecurityPolicy{} }, func() resource.Resource { return &resourceDatacenterIpLinkAddressing{} }, diff --git a/docs/resources/datacenter_routing_zone_constraint.md b/docs/resources/datacenter_routing_zone_constraint.md new file mode 100644 index 00000000..d99577ed --- /dev/null +++ b/docs/resources/datacenter_routing_zone_constraint.md @@ -0,0 +1,68 @@ +--- +page_title: "apstra_datacenter_routing_zone_constraint Resource - terraform-provider-apstra" +subcategory: "Reference Design: Datacenter" +description: |- + This resource creates a Routing Zone Constraint within a Datacenter Blueprint. +--- + +# apstra_datacenter_routing_zone_constraint (Resource) + +This resource creates a Routing Zone Constraint within a Datacenter Blueprint. + + +## Example Usage + +```terraform +# This example creates a Routing Zone Constraint which permits exactly one "dev" +# Routing Zone anywhere it is applied. + +# First, collect all routing zone IDs in the blueprint +data "apstra_datacenter_routing_zones" "all" { + blueprint_id = local.blueprint_id +} + +# Second, collect details about each of those routing zones +data "apstra_datacenter_routing_zone" "all" { + for_each = data.apstra_datacenter_routing_zones.all.ids + blueprint_id = local.blueprint_id + id = each.key +} + +# Finally, create the Routing Zone Constraint +resource "apstra_datacenter_routing_zone_constraint" "example" { + blueprint_id = local.blueprint_id + name = "Permit 1 dev RZ" + max_count_constraint = 1 + routing_zones_list_constraint = "allow" + # Constraints is created as a list comprehension by iterating over + # details of each RZ in data.apstra_datacenter_routing_zone.all + constraints = [ + for rz in data.apstra_datacenter_routing_zone.all : rz.id + if strcontains(rz.name, "dev") // select those with "dev" in their name + ] +} +``` + + +## Schema + +### Required + +- `blueprint_id` (String) Apstra Blueprint ID. +- `name` (String) Name displayed in the Apstra web UI. +- `routing_zones_list_constraint` (String) Instance constraint mode. +- `allow` - only allow the specified routing zones (add specific routing zones to allow) +- `deny` - denies allocation of specified routing zones (add specific routing zones to deny) +- `none` - no additional constraints on routing zones (any routing zones) + +### Optional + +- `constraints` (Set of String) When `allow` instance constraint mode is chosen, only VNs from selected Routing Zones are allowed to have endpoints on the interface(s) the policy is applied to. The permitted Routing Zones may be specified directly or indirectly (via Routing Zone Groups) +- `max_count_constraint` (Number) The maximum number of Routing Zones that the Application Point can be part of. + +### Read-Only + +- `id` (String) Apstra graph node ID. + + + diff --git a/examples/resources/apstra_datacenter_routing_zone_constraint/example.tf b/examples/resources/apstra_datacenter_routing_zone_constraint/example.tf new file mode 100644 index 00000000..2c2bc851 --- /dev/null +++ b/examples/resources/apstra_datacenter_routing_zone_constraint/example.tf @@ -0,0 +1,28 @@ +# This example creates a Routing Zone Constraint which permits exactly one "dev" +# Routing Zone anywhere it is applied. + +# First, collect all routing zone IDs in the blueprint +data "apstra_datacenter_routing_zones" "all" { + blueprint_id = local.blueprint_id +} + +# Second, collect details about each of those routing zones +data "apstra_datacenter_routing_zone" "all" { + for_each = data.apstra_datacenter_routing_zones.all.ids + blueprint_id = local.blueprint_id + id = each.key +} + +# Finally, create the Routing Zone Constraint +resource "apstra_datacenter_routing_zone_constraint" "example" { + blueprint_id = local.blueprint_id + name = "Permit 1 dev RZ" + max_count_constraint = 1 + routing_zones_list_constraint = "allow" + # Constraints is created as a list comprehension by iterating over + # details of each RZ in data.apstra_datacenter_routing_zone.all + constraints = [ + for rz in data.apstra_datacenter_routing_zone.all : rz.id + if strcontains(rz.name, "dev") // select those with "dev" in their name + ] +} From 23b9919c099cc4c1579c8d890f2a7707217bb292 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Thu, 19 Dec 2024 17:23:45 -0500 Subject: [PATCH 2/7] routing zone constraint resource todo: - resource tests - constraint data source - constraints data source --- apstra/blueprint/routing_zone_constraint.go | 175 +++++++++++++++ ...urce_datacenter_routing_zone_constraint.go | 204 ++++++++++++++++++ 2 files changed, 379 insertions(+) create mode 100644 apstra/blueprint/routing_zone_constraint.go create mode 100644 apstra/resource_datacenter_routing_zone_constraint.go diff --git a/apstra/blueprint/routing_zone_constraint.go b/apstra/blueprint/routing_zone_constraint.go new file mode 100644 index 00000000..a0a386d4 --- /dev/null +++ b/apstra/blueprint/routing_zone_constraint.go @@ -0,0 +1,175 @@ +package blueprint + +import ( + "context" + "fmt" + "github.com/Juniper/apstra-go-sdk/apstra" + "github.com/Juniper/apstra-go-sdk/apstra/enum" + "github.com/Juniper/terraform-provider-apstra/apstra/utils" + apstravalidator "github.com/Juniper/terraform-provider-apstra/apstra/validator" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + dataSourceSchema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + resourceSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "strings" +) + +type DatacenterRoutingZoneConstraint struct { + Id types.String `tfsdk:"id"` + BlueprintId types.String `tfsdk:"blueprint_id"` + Name types.String `tfsdk:"name"` + MaxCountConstraint types.Int64 `tfsdk:"max_count_constraint"` + RoutingZonesListConstraint types.String `tfsdk:"routing_zones_list_constraint"` + Constraints types.Set `tfsdk:"constraints"` +} + +func (o DatacenterRoutingZoneConstraint) DatasourceAttributes() map[string]dataSourceSchema.Attribute { + return map[string]dataSourceSchema.Attribute{ + "id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Apstra graph node ID. Required when `name` is omitted.", + Computed: true, + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + stringvalidator.ExactlyOneOf(path.Expressions{ + path.MatchRelative(), + path.MatchRoot("name"), + }...), + }, + }, + "blueprint_id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Apstra Blueprint ID.", + Required: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "name": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Name displayed in the Apstra web UI. Required when `id` is omitted.", + Computed: true, + Optional: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "max_count_constraint": dataSourceSchema.Int64Attribute{ + MarkdownDescription: "The maximum number of Routing Zones that the Application Point can be part of.", + Computed: true, + }, + "routing_zones_list_constraint": dataSourceSchema.StringAttribute{ + MarkdownDescription: fmt.Sprintf( + "Routing Zone constraint mode. One of: %s.", strings.Join( + []string{ + "`" + utils.StringersToFriendlyString(enum.RoutingZoneConstraintModeAllow) + "`", + "`" + utils.StringersToFriendlyString(enum.RoutingZoneConstraintModeDeny) + "`", + "`" + utils.StringersToFriendlyString(enum.RoutingZoneConstraintModeNone) + "`", + }, ", "), + ), + Computed: true, + }, + "constraints": dataSourceSchema.SetAttribute{ + MarkdownDescription: fmt.Sprintf("When `%s` instance constraint mode is chosen, only VNs from selected "+ + "Routing Zones are allowed to have endpoints on the interface(s) the policy is applied to. The permitted "+ + "Routing Zones may be specified directly or indirectly (via Routing Zone Groups)", + utils.StringersToFriendlyString(enum.RoutingZoneConstraintModeAllow), + ), + Computed: true, + ElementType: types.StringType, + }, + } +} + +func (o DatacenterRoutingZoneConstraint) ResourceAttributes() map[string]resourceSchema.Attribute { + return map[string]resourceSchema.Attribute{ + "id": resourceSchema.StringAttribute{ + MarkdownDescription: "Apstra graph node ID.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "blueprint_id": resourceSchema.StringAttribute{ + MarkdownDescription: "Apstra Blueprint ID.", + Required: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "name": resourceSchema.StringAttribute{ + MarkdownDescription: "Name displayed in the Apstra web UI.", + Required: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "max_count_constraint": resourceSchema.Int64Attribute{ + MarkdownDescription: "The maximum number of Routing Zones that the Application Point can be part of.", + Optional: true, + Validators: []validator.Int64{int64validator.Between(0, 255)}, + }, + "routing_zones_list_constraint": resourceSchema.StringAttribute{ + MarkdownDescription: fmt.Sprintf( + fmt.Sprintf("Instance constraint mode.\n"+ + "- `%s` - only allow the specified routing zones (add specific routing zones to allow)\n"+ + "- `%s` - denies allocation of specified routing zones (add specific routing zones to deny)\n"+ + "- `%s` - no additional constraints on routing zones (any routing zones)", + utils.StringersToFriendlyString(enum.RoutingZoneConstraintModeAllow), + utils.StringersToFriendlyString(enum.RoutingZoneConstraintModeDeny), + utils.StringersToFriendlyString(enum.RoutingZoneConstraintModeNone), + ), + ), + Required: true, + Validators: []validator.String{stringvalidator.OneOf( + utils.StringersToFriendlyString(enum.RoutingZoneConstraintModeAllow), + utils.StringersToFriendlyString(enum.RoutingZoneConstraintModeDeny), + utils.StringersToFriendlyString(enum.RoutingZoneConstraintModeNone), + )}, + }, + "constraints": resourceSchema.SetAttribute{ + MarkdownDescription: fmt.Sprintf("When `%s` instance constraint mode is chosen, only VNs from selected "+ + "Routing Zones are allowed to have endpoints on the interface(s) the policy is applied to. The permitted "+ + "Routing Zones may be specified directly or indirectly (via Routing Zone Groups)", + utils.StringersToFriendlyString(enum.RoutingZoneConstraintModeAllow), + ), + Optional: true, + ElementType: types.StringType, + Validators: []validator.Set{ + apstravalidator.ForbiddenWhenValueIs( + path.MatchRoot("routing_zones_list_constraint"), + types.StringValue(utils.StringersToFriendlyString(enum.RoutingZoneConstraintModeNone)), + ), + }, + }, + } +} + +func (o DatacenterRoutingZoneConstraint) Request(ctx context.Context, diags *diag.Diagnostics) *apstra.RoutingZoneConstraintData { + result := apstra.RoutingZoneConstraintData{ + Label: o.Name.ValueString(), + } + + // set result.Mode + err := utils.ApiStringerFromFriendlyString(&result.Mode, o.RoutingZonesListConstraint.ValueString()) + if err != nil { + diags.AddError(fmt.Sprintf("failed converting %s to API type", o.RoutingZonesListConstraint), err.Error()) + return nil + } + + // set result.MaxRoutingZones + if !o.MaxCountConstraint.IsNull() { + result.MaxRoutingZones = utils.ToPtr(int(o.MaxCountConstraint.ValueInt64())) + } + + // set result.RoutingZoneIds + diags.Append(o.Constraints.ElementsAs(ctx, &result.RoutingZoneIds, false)...) + + return &result +} + +func (o *DatacenterRoutingZoneConstraint) LoadApiData(ctx context.Context, in *apstra.RoutingZoneConstraintData, diags *diag.Diagnostics) { + o.Name = types.StringValue(in.Label) + if in.MaxRoutingZones == nil { + o.MaxCountConstraint = types.Int64Null() + } else { + o.MaxCountConstraint = types.Int64Value(int64(*in.MaxRoutingZones)) + } + o.RoutingZonesListConstraint = types.StringValue(in.Mode.String()) + o.Constraints = utils.SetValueOrNull(ctx, types.StringType, in.RoutingZoneIds, diags) +} diff --git a/apstra/resource_datacenter_routing_zone_constraint.go b/apstra/resource_datacenter_routing_zone_constraint.go new file mode 100644 index 00000000..c373a07d --- /dev/null +++ b/apstra/resource_datacenter_routing_zone_constraint.go @@ -0,0 +1,204 @@ +package tfapstra + +import ( + "context" + "fmt" + "github.com/Juniper/apstra-go-sdk/apstra" + "github.com/Juniper/terraform-provider-apstra/apstra/blueprint" + "github.com/Juniper/terraform-provider-apstra/apstra/utils" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ resource.ResourceWithConfigure = &resourceDatacenterRoutingZoneConstraint{} +var _ resourceWithSetDcBpClientFunc = &resourceDatacenterRoutingZoneConstraint{} +var _ resourceWithSetBpLockFunc = &resourceDatacenterRoutingZoneConstraint{} + +type resourceDatacenterRoutingZoneConstraint struct { + getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) + lockFunc func(context.Context, string) error +} + +func (o *resourceDatacenterRoutingZoneConstraint) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_datacenter_routing_zone_constraint" +} + +func (o *resourceDatacenterRoutingZoneConstraint) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + configureResource(ctx, o, req, resp) +} + +func (o *resourceDatacenterRoutingZoneConstraint) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: docCategoryDatacenter + "This resource creates a Routing Zone Constraint within a Datacenter Blueprint.", + Attributes: blueprint.DatacenterRoutingZoneConstraint{}.ResourceAttributes(), + } +} + +func (o *resourceDatacenterRoutingZoneConstraint) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan. + var plan blueprint.DatacenterRoutingZoneConstraint + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // get a client for the datacenter reference design + bp, err := o.getBpClientFunc(ctx, plan.BlueprintId.ValueString()) + if err != nil { + if utils.IsApstra404(err) { + resp.Diagnostics.AddError(fmt.Sprintf("blueprint %s not found", plan.BlueprintId), err.Error()) + return + } + resp.Diagnostics.AddError("failed to create blueprint client", err.Error()) + return + } + + // Lock the blueprint mutex. + err = o.lockFunc(ctx, plan.BlueprintId.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("error locking blueprint %q mutex", plan.BlueprintId.ValueString()), + err.Error()) + return + } + + // create a routing zone constraint request + request := plan.Request(ctx, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // create the routing zone constraint + id, err := bp.CreateRoutingZoneConstraint(ctx, request) + if err != nil { + resp.Diagnostics.AddError("error creating routing zone constraint", err.Error()) + return + } + + // save the ID and set the state + plan.Id = types.StringValue(id.String()) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (o *resourceDatacenterRoutingZoneConstraint) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Retrieve values from state. + var state blueprint.DatacenterRoutingZoneConstraint + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + // get a client for the datacenter reference design + bp, err := o.getBpClientFunc(ctx, state.BlueprintId.ValueString()) + if err != nil { + if utils.IsApstra404(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("failed to create blueprint client", err.Error()) + return + } + + api, err := bp.GetRoutingZoneConstraint(ctx, apstra.ObjectId(state.Id.ValueString())) + if err != nil { + if utils.IsApstra404(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("error retrieving routing zone constraint", err.Error()) + return + } + + state.LoadApiData(ctx, api.Data, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (o *resourceDatacenterRoutingZoneConstraint) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Retrieve values from plan. + var plan blueprint.DatacenterRoutingZoneConstraint + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // get a client for the datacenter reference design + bp, err := o.getBpClientFunc(ctx, plan.BlueprintId.ValueString()) + if err != nil { + resp.Diagnostics.AddError("failed to create blueprint client", err.Error()) + return + } + + // Lock the blueprint mutex. + err = o.lockFunc(ctx, plan.BlueprintId.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("error locking blueprint %q mutex", plan.BlueprintId.ValueString()), + err.Error()) + return + } + + // create a request we'll use when invoking UpdateSecurityZone + request := plan.Request(ctx, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // send the update + err = bp.UpdateRoutingZoneConstraint(ctx, apstra.ObjectId(plan.Id.ValueString()), request) + if err != nil { + resp.Diagnostics.AddError("error updating routing zone constraint", err.Error()) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (o *resourceDatacenterRoutingZoneConstraint) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state. + var state blueprint.DatacenterRoutingZoneConstraint + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + // get a client for the datacenter reference design + bp, err := o.getBpClientFunc(ctx, state.BlueprintId.ValueString()) + if err != nil { + if utils.IsApstra404(err) { + return // 404 is okay + } + resp.Diagnostics.AddError("failed to create blueprint client", err.Error()) + return + } + + // Lock the blueprint mutex. + err = o.lockFunc(ctx, state.BlueprintId.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("error locking blueprint %q mutex", state.BlueprintId.ValueString()), + err.Error()) + return + } + + // Delete the routing zone constraint + err = bp.DeleteRoutingZoneConstraint(ctx, apstra.ObjectId(state.Id.ValueString())) + if err != nil { + if utils.IsApstra404(err) { + return // 404 is okay + } + resp.Diagnostics.AddError("error deleting routing zone constraint", err.Error()) + } +} + +func (o *resourceDatacenterRoutingZoneConstraint) setBpClientFunc(f func(context.Context, string) (*apstra.TwoStageL3ClosClient, error)) { + o.getBpClientFunc = f +} + +func (o *resourceDatacenterRoutingZoneConstraint) setBpLockFunc(f func(context.Context, string) error) { + o.lockFunc = f +} From 80365a3754416fc914cd84de94f625d650110062 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Thu, 19 Dec 2024 21:10:14 -0500 Subject: [PATCH 3/7] add routing zone constraint data sources --- apstra/blueprint/routing_zone_constraint.go | 96 ++++++++- ...urce_datacenter_routing_zone_constraint.go | 102 ++++++++++ ...rce_datacenter_routing_zone_constraints.go | 185 ++++++++++++++++++ apstra/provider.go | 2 + ...urce_datacenter_routing_zone_constraint.go | 15 +- ...outing_zone_constraint_integration_test.go | 1 + go.mod | 2 +- go.sum | 4 +- 8 files changed, 397 insertions(+), 10 deletions(-) create mode 100644 apstra/data_source_datacenter_routing_zone_constraint.go create mode 100644 apstra/data_source_datacenter_routing_zone_constraints.go create mode 100644 apstra/resource_datacenter_routing_zone_constraint_integration_test.go diff --git a/apstra/blueprint/routing_zone_constraint.go b/apstra/blueprint/routing_zone_constraint.go index a0a386d4..5fab3905 100644 --- a/apstra/blueprint/routing_zone_constraint.go +++ b/apstra/blueprint/routing_zone_constraint.go @@ -3,11 +3,14 @@ package blueprint import ( "context" "fmt" + "strings" + "github.com/Juniper/apstra-go-sdk/apstra" "github.com/Juniper/apstra-go-sdk/apstra/enum" "github.com/Juniper/terraform-provider-apstra/apstra/utils" apstravalidator "github.com/Juniper/terraform-provider-apstra/apstra/validator" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" dataSourceSchema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -17,7 +20,6 @@ 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" - "strings" ) type DatacenterRoutingZoneConstraint struct { @@ -29,7 +31,7 @@ type DatacenterRoutingZoneConstraint struct { Constraints types.Set `tfsdk:"constraints"` } -func (o DatacenterRoutingZoneConstraint) DatasourceAttributes() map[string]dataSourceSchema.Attribute { +func (o DatacenterRoutingZoneConstraint) DataSourceAttributes() map[string]dataSourceSchema.Attribute { return map[string]dataSourceSchema.Attribute{ "id": dataSourceSchema.StringAttribute{ MarkdownDescription: "Apstra graph node ID. Required when `name` is omitted.", @@ -81,6 +83,51 @@ func (o DatacenterRoutingZoneConstraint) DatasourceAttributes() map[string]dataS } } +func (o DatacenterRoutingZoneConstraint) DataSourceFilterAttributes() map[string]dataSourceSchema.Attribute { + return map[string]dataSourceSchema.Attribute{ + "id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Not applicable in filter context. Ignore.", + Computed: true, + }, + "blueprint_id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Not applicable in filter context. Ignore.", + Computed: true, + }, + "name": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Name displayed in the Apstra web UI.", + Optional: true, + }, + "max_count_constraint": dataSourceSchema.Int64Attribute{ + MarkdownDescription: "The maximum number of Routing Zones that the Application Point can be part of.", + Optional: true, + }, + "routing_zones_list_constraint": dataSourceSchema.StringAttribute{ + MarkdownDescription: fmt.Sprintf( + "Routing Zone constraint mode. One of: %s.", strings.Join( + []string{ + "`" + utils.StringersToFriendlyString(enum.RoutingZoneConstraintModeAllow) + "`", + "`" + utils.StringersToFriendlyString(enum.RoutingZoneConstraintModeDeny) + "`", + "`" + utils.StringersToFriendlyString(enum.RoutingZoneConstraintModeNone) + "`", + }, ", "), + ), + Optional: true, + Validators: []validator.String{stringvalidator.OneOf( // validated b/c this runs through rosetta + utils.StringersToFriendlyString(enum.RoutingZoneConstraintModeAllow), + utils.StringersToFriendlyString(enum.RoutingZoneConstraintModeDeny), + utils.StringersToFriendlyString(enum.RoutingZoneConstraintModeNone), + )}, + }, + "constraints": dataSourceSchema.SetAttribute{ + MarkdownDescription: "Set of Routing Zone IDs. All Routing Zones supplied here are used to match the " + + "Routing Zone Constraint, but a matching Routing Zone Constraintmay have additional Security Zones " + + "not enumerated in this set.", + Optional: true, + ElementType: types.StringType, + Validators: []validator.Set{setvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1))}, + }, + } +} + func (o DatacenterRoutingZoneConstraint) ResourceAttributes() map[string]resourceSchema.Attribute { return map[string]resourceSchema.Attribute{ "id": resourceSchema.StringAttribute{ @@ -163,7 +210,7 @@ func (o DatacenterRoutingZoneConstraint) Request(ctx context.Context, diags *dia return &result } -func (o *DatacenterRoutingZoneConstraint) LoadApiData(ctx context.Context, in *apstra.RoutingZoneConstraintData, diags *diag.Diagnostics) { +func (o *DatacenterRoutingZoneConstraint) LoadApiData(ctx context.Context, in apstra.RoutingZoneConstraintData, diags *diag.Diagnostics) { o.Name = types.StringValue(in.Label) if in.MaxRoutingZones == nil { o.MaxCountConstraint = types.Int64Null() @@ -173,3 +220,46 @@ func (o *DatacenterRoutingZoneConstraint) LoadApiData(ctx context.Context, in *a o.RoutingZonesListConstraint = types.StringValue(in.Mode.String()) o.Constraints = utils.SetValueOrNull(ctx, types.StringType, in.RoutingZoneIds, diags) } + +func (o DatacenterRoutingZoneConstraint) Query(ctx context.Context, rzcResultName string, diags *diag.Diagnostics) *apstra.MatchQuery { + rzcNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal(rzcResultName)} + nodeAttributes := []apstra.QEEAttribute{rzcNameAttr, apstra.NodeTypeRoutingZoneConstraint.QEEAttribute()} + + // add the name to the match, if any + if !o.Name.IsNull() { + nodeAttributes = append(nodeAttributes, apstra.QEEAttribute{Key: "label", Value: apstra.QEStringVal(o.Name.ValueString())}) + } + + // add the max to the match, if any + if !o.MaxCountConstraint.IsNull() { + nodeAttributes = append(nodeAttributes, apstra.QEEAttribute{Key: "max_count_constraint", Value: apstra.QEIntVal(o.MaxCountConstraint.ValueInt64())}) + } + + // add the mode to the match, if any + if !o.RoutingZonesListConstraint.IsNull() { + var rzcm enum.RoutingZoneConstraintMode + err := utils.ApiStringerFromFriendlyString(&rzcm, o.RoutingZonesListConstraint.ValueString()) + if err != nil { + diags.AddError(fmt.Sprintf("failed converting %s to API type", o.RoutingZonesListConstraint), err.Error()) + return nil + } + nodeAttributes = append(nodeAttributes, apstra.QEEAttribute{Key: "routing_zones_list_constraint", Value: apstra.QEStringVal(rzcm.String())}) + } + + query := new(apstra.MatchQuery).Match(new(apstra.PathQuery).Node(nodeAttributes)) + + var rzIds []string + diags.Append(o.Constraints.ElementsAs(ctx, &rzIds, false)...) + if diags.HasError() { + return nil + } + + for _, rzId := range rzIds { + query.Match(new(apstra.PathQuery). + Node([]apstra.QEEAttribute{rzcNameAttr}). + Out([]apstra.QEEAttribute{apstra.RelationshipTypeConstraint.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeSecurityZone.QEEAttribute(), {Key: "id", Value: apstra.QEStringVal(rzId)}})) + } + + return query +} diff --git a/apstra/data_source_datacenter_routing_zone_constraint.go b/apstra/data_source_datacenter_routing_zone_constraint.go new file mode 100644 index 00000000..3202e51d --- /dev/null +++ b/apstra/data_source_datacenter_routing_zone_constraint.go @@ -0,0 +1,102 @@ +package tfapstra + +import ( + "context" + "fmt" + + "github.com/Juniper/apstra-go-sdk/apstra" + "github.com/Juniper/terraform-provider-apstra/apstra/blueprint" + "github.com/Juniper/terraform-provider-apstra/apstra/utils" + "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 = &dataSourceDatacenterRoutingZoneConstraint{} + _ datasourceWithSetDcBpClientFunc = &dataSourceDatacenterRoutingZoneConstraint{} +) + +type dataSourceDatacenterRoutingZoneConstraint struct { + getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) +} + +func (o *dataSourceDatacenterRoutingZoneConstraint) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_datacenter_routing_zone_constraint" +} + +func (o *dataSourceDatacenterRoutingZoneConstraint) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + configureDataSource(ctx, o, req, resp) +} + +func (o *dataSourceDatacenterRoutingZoneConstraint) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: docCategoryDatacenter + "This resource returns details of a Routing Zone Constraint within a Datacenter Blueprint.\n\n" + + "At least one optional attribute is required.", + Attributes: blueprint.DatacenterRoutingZoneConstraint{}.DataSourceAttributes(), + } +} + +func (o *dataSourceDatacenterRoutingZoneConstraint) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + // Retrieve values from config. + var config blueprint.DatacenterRoutingZoneConstraint + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + // get a client for the datacenter reference design + bp, err := o.getBpClientFunc(ctx, config.BlueprintId.ValueString()) + if err != nil { + if utils.IsApstra404(err) { + resp.Diagnostics.AddError(fmt.Sprintf(errBpNotFoundSummary, config.BlueprintId), err.Error()) + return + } + resp.Diagnostics.AddError(fmt.Sprintf(errBpClientCreateSummary, config.BlueprintId), err.Error()) + return + } + + var api *apstra.RoutingZoneConstraint + switch { + case !config.Id.IsNull(): + api, err = bp.GetRoutingZoneConstraint(ctx, apstra.ObjectId(config.Id.ValueString())) + if utils.IsApstra404(err) { + resp.Diagnostics.AddAttributeError( + path.Root("id"), + "Routing Zone not found", + fmt.Sprintf("Routing Zone Constraint with ID %s not found", config.Id)) + return + } + case !config.Name.IsNull(): + api, err = bp.GetRoutingZoneConstraintByName(ctx, config.Name.ValueString()) + if utils.IsApstra404(err) { + resp.Diagnostics.AddAttributeError( + path.Root("name"), + "Routing Zone not found", + fmt.Sprintf("Routing Zone Constraint with Name %s not found", config.Name)) + return + } + } + if err != nil { + resp.Diagnostics.AddError("failed reading Routing Zone Constraint", err.Error()) + return + } + if api == nil || api.Data == nil { + resp.Diagnostics.AddError("failed reading Routing Zone Constraint", "api response has no payload") + return + } + + config.Id = types.StringValue(api.Id.String()) + config.LoadApiData(ctx, *api.Data, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // set state + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) +} + +func (o *dataSourceDatacenterRoutingZoneConstraint) setBpClientFunc(f func(context.Context, string) (*apstra.TwoStageL3ClosClient, error)) { + o.getBpClientFunc = f +} diff --git a/apstra/data_source_datacenter_routing_zone_constraints.go b/apstra/data_source_datacenter_routing_zone_constraints.go new file mode 100644 index 00000000..d7adf32a --- /dev/null +++ b/apstra/data_source_datacenter_routing_zone_constraints.go @@ -0,0 +1,185 @@ +package tfapstra + +import ( + "context" + "fmt" + + "github.com/Juniper/apstra-go-sdk/apstra" + "github.com/Juniper/terraform-provider-apstra/apstra/blueprint" + "github.com/Juniper/terraform-provider-apstra/apstra/utils" + apstravalidator "github.com/Juniper/terraform-provider-apstra/apstra/validator" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ datasource.DataSourceWithConfigure = &dataSourceDatacenterRoutingZoneConstraints{} + _ datasourceWithSetDcBpClientFunc = &dataSourceDatacenterRoutingZoneConstraints{} +) + +type dataSourceDatacenterRoutingZoneConstraints struct { + getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) +} + +func (o *dataSourceDatacenterRoutingZoneConstraints) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_datacenter_routing_zone_constraints" +} + +func (o *dataSourceDatacenterRoutingZoneConstraints) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + configureDataSource(ctx, o, req, resp) +} + +func (o *dataSourceDatacenterRoutingZoneConstraints) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: docCategoryDatacenter + "This data source returns the IDs of Routing Zone Constraints within the specified Blueprint. " + + "All of the `filter` attributes are optional.", + Attributes: map[string]schema.Attribute{ + "blueprint_id": schema.StringAttribute{ + MarkdownDescription: "Apstra Blueprint ID.", + Required: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "ids": schema.SetAttribute{ + MarkdownDescription: "Set of Routing Zone Constraint IDs", + Computed: true, + ElementType: types.StringType, + }, + "filters": schema.ListNestedAttribute{ + MarkdownDescription: "List of filters used to select only desired node IDs. For a node " + + "to match a filter, all specified attributes must match (each attribute within a " + + "filter is AND-ed together). The returned node IDs represent the nodes matched by " + + "all of the filters together (filters are OR-ed together).", + Optional: true, + Validators: []validator.List{listvalidator.SizeAtLeast(1)}, + NestedObject: schema.NestedAttributeObject{ + Attributes: blueprint.DatacenterRoutingZoneConstraint{}.DataSourceFilterAttributes(), + Validators: []validator.Object{ + apstravalidator.AtLeastNAttributes( + 1, + "name", "max_count_constraint", "routing_zones_list_constraint", "constraints", + ), + }, + }, + }, + "graph_queries": schema.ListAttribute{ + MarkdownDescription: "Graph datastore queries which performed the lookup based on supplied filters.", + Computed: true, + ElementType: types.StringType, + }, + }, + } +} + +func (o *dataSourceDatacenterRoutingZoneConstraints) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + type routingZoneConstraints struct { + BlueprintId types.String `tfsdk:"blueprint_id"` + IDs types.Set `tfsdk:"ids"` + Filters types.List `tfsdk:"filters"` + GraphQueries types.List `tfsdk:"graph_queries"` + } + + // get the configuration + var config routingZoneConstraints + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + // get a client for the datacenter reference design + bp, err := o.getBpClientFunc(ctx, config.BlueprintId.ValueString()) + if err != nil { + if utils.IsApstra404(err) { + resp.Diagnostics.AddError(fmt.Sprintf(errBpNotFoundSummary, config.BlueprintId), err.Error()) + return + } + resp.Diagnostics.AddError(fmt.Sprintf(errBpClientCreateSummary, config.BlueprintId), err.Error()) + return + } + + // If no filters supplied, we can just fetch IDs via the API + if config.Filters.IsNull() { + allRoutingZoneConstraints, err := bp.GetAllRoutingZoneConstraints(ctx) + if err != nil { + resp.Diagnostics.AddError("failed to fetch routing zone constraints", err.Error()) + return + } + + // collect the IDs + ids := make([]attr.Value, len(allRoutingZoneConstraints)) + for i, routingZoneConstraint := range allRoutingZoneConstraints { + ids[i] = types.StringValue(routingZoneConstraint.Id.String()) + } + + // set the state + config.IDs = types.SetValueMust(types.StringType, ids) + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) + return + } + + // extract the supplied filters + var filters []blueprint.DatacenterRoutingZoneConstraint + resp.Diagnostics.Append(config.Filters.ElementsAs(ctx, &filters, false)...) + if resp.Diagnostics.HasError() { + return + } + + idMap := make(map[string]struct{}) // collect IDs here + graphQueries := make([]attr.Value, len(filters)) // collect graph query strings here + for i, filter := range filters { + // prep a query + query := filter.Query(ctx, "n_routing_zone_constraint", &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // save the query + graphQueries[i] = types.StringValue(query.String()) + + // query response target + queryResponse := new(struct { + Items []struct { + RoutingZoneConstraint struct { + Id string `json:"id"` + } `json:"n_routing_zone_constraint"` + } `json:"items"` + }) + + // run the query + query. + SetClient(bp.Client()). + SetBlueprintId(apstra.ObjectId(config.BlueprintId.ValueString())). + SetBlueprintType(apstra.BlueprintTypeStaging) + err = query.Do(ctx, queryResponse) + if err != nil { + resp.Diagnostics.AddError("error querying graph datastore", err.Error()) + return + } + + // save the IDs into idMap + for _, item := range queryResponse.Items { + idMap[item.RoutingZoneConstraint.Id] = struct{}{} + } + } + + // pull the IDs out of the map + ids := make([]attr.Value, len(idMap)) + var i int + for id := range idMap { + ids[i] = types.StringValue(id) + i++ + } + + // set the state + config.IDs = types.SetValueMust(types.StringType, ids) + config.GraphQueries = types.ListValueMust(types.StringType, graphQueries) + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) +} + +func (o *dataSourceDatacenterRoutingZoneConstraints) setBpClientFunc(f func(context.Context, string) (*apstra.TwoStageL3ClosClient, error)) { + o.getBpClientFunc = f +} diff --git a/apstra/provider.go b/apstra/provider.go index 874f7ae2..e486710f 100644 --- a/apstra/provider.go +++ b/apstra/provider.go @@ -550,6 +550,8 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource func() datasource.DataSource { return &dataSourceDatacenterRoutingPolicies{} }, func() datasource.DataSource { return &dataSourceDatacenterRoutingPolicy{} }, func() datasource.DataSource { return &dataSourceDatacenterRoutingZone{} }, + func() datasource.DataSource { return &dataSourceDatacenterRoutingZoneConstraint{} }, + func() datasource.DataSource { return &dataSourceDatacenterRoutingZoneConstraints{} }, func() datasource.DataSource { return &dataSourceDatacenterRoutingZones{} }, func() datasource.DataSource { return &dataSourceDatacenterSecurityPolicies{} }, func() datasource.DataSource { return &dataSourceDatacenterSecurityPolicy{} }, diff --git a/apstra/resource_datacenter_routing_zone_constraint.go b/apstra/resource_datacenter_routing_zone_constraint.go index c373a07d..a5a75e4a 100644 --- a/apstra/resource_datacenter_routing_zone_constraint.go +++ b/apstra/resource_datacenter_routing_zone_constraint.go @@ -3,6 +3,7 @@ package tfapstra import ( "context" "fmt" + "github.com/Juniper/apstra-go-sdk/apstra" "github.com/Juniper/terraform-provider-apstra/apstra/blueprint" "github.com/Juniper/terraform-provider-apstra/apstra/utils" @@ -11,9 +12,11 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -var _ resource.ResourceWithConfigure = &resourceDatacenterRoutingZoneConstraint{} -var _ resourceWithSetDcBpClientFunc = &resourceDatacenterRoutingZoneConstraint{} -var _ resourceWithSetBpLockFunc = &resourceDatacenterRoutingZoneConstraint{} +var ( + _ resource.ResourceWithConfigure = &resourceDatacenterRoutingZoneConstraint{} + _ resourceWithSetDcBpClientFunc = &resourceDatacenterRoutingZoneConstraint{} + _ resourceWithSetBpLockFunc = &resourceDatacenterRoutingZoneConstraint{} +) type resourceDatacenterRoutingZoneConstraint struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) @@ -109,8 +112,12 @@ func (o *resourceDatacenterRoutingZoneConstraint) Read(ctx context.Context, req resp.Diagnostics.AddError("error retrieving routing zone constraint", err.Error()) return } + if api == nil || api.Data == nil { + resp.Diagnostics.AddError("failed reading Routing Zone Constraint", "api response has no payload") + return + } - state.LoadApiData(ctx, api.Data, &resp.Diagnostics) + state.LoadApiData(ctx, *api.Data, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } diff --git a/apstra/resource_datacenter_routing_zone_constraint_integration_test.go b/apstra/resource_datacenter_routing_zone_constraint_integration_test.go new file mode 100644 index 00000000..20699884 --- /dev/null +++ b/apstra/resource_datacenter_routing_zone_constraint_integration_test.go @@ -0,0 +1 @@ +package tfapstra_test diff --git a/go.mod b/go.mod index 7eac6898..9f0c0d5e 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ toolchain go1.22.10 require ( github.com/IBM/netaddr v1.5.0 - github.com/Juniper/apstra-go-sdk v0.0.0-20241218190445-7367e0b85e5f + github.com/Juniper/apstra-go-sdk v0.0.0-20241220010754-e4f59ed93cd7 github.com/chrismarget-j/go-licenses v0.0.0-20240224210557-f22f3e06d3d4 github.com/chrismarget-j/version-constraints v0.0.0-20240925155624-26771a0a6820 github.com/google/go-cmp v0.6.0 diff --git a/go.sum b/go.sum index 51c950d0..d162bed9 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0 github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 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-20241218190445-7367e0b85e5f h1:BSj2IKyq61icff5YXzzHS2sDoAxd992QTx6LKZI5f/A= -github.com/Juniper/apstra-go-sdk v0.0.0-20241218190445-7367e0b85e5f/go.mod h1:j0XhEo0IoltyST4cqdLwrDUNLDHC7JWJxBPDVffeSCg= +github.com/Juniper/apstra-go-sdk v0.0.0-20241220010754-e4f59ed93cd7 h1:HE5NqogM/GBkUOcM6qgwoNpZa6sgf6co8Juee4UMZKM= +github.com/Juniper/apstra-go-sdk v0.0.0-20241220010754-e4f59ed93cd7/go.mod h1:j0XhEo0IoltyST4cqdLwrDUNLDHC7JWJxBPDVffeSCg= 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 dce8ca5dcd420f029bb05154a4f9fc0a0a0d1be9 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Thu, 19 Dec 2024 21:27:49 -0500 Subject: [PATCH 4/7] update docs --- apstra/blueprint/routing_zone_constraint.go | 16 ++- .../datacenter_routing_zone_constraint.md | 63 ++++++++++ .../datacenter_routing_zone_constraints.md | 113 ++++++++++++++++++ .../example.tf | 26 ++++ .../example.tf | 66 ++++++++++ 5 files changed, 275 insertions(+), 9 deletions(-) create mode 100644 docs/data-sources/datacenter_routing_zone_constraint.md create mode 100644 docs/data-sources/datacenter_routing_zone_constraints.md create mode 100644 examples/data-sources/apstra_datacenter_routing_zone_constraint/example.tf create mode 100644 examples/data-sources/apstra_datacenter_routing_zone_constraints/example.tf diff --git a/apstra/blueprint/routing_zone_constraint.go b/apstra/blueprint/routing_zone_constraint.go index 5fab3905..eb619efa 100644 --- a/apstra/blueprint/routing_zone_constraint.go +++ b/apstra/blueprint/routing_zone_constraint.go @@ -152,15 +152,13 @@ func (o DatacenterRoutingZoneConstraint) ResourceAttributes() map[string]resourc Validators: []validator.Int64{int64validator.Between(0, 255)}, }, "routing_zones_list_constraint": resourceSchema.StringAttribute{ - MarkdownDescription: fmt.Sprintf( - fmt.Sprintf("Instance constraint mode.\n"+ - "- `%s` - only allow the specified routing zones (add specific routing zones to allow)\n"+ - "- `%s` - denies allocation of specified routing zones (add specific routing zones to deny)\n"+ - "- `%s` - no additional constraints on routing zones (any routing zones)", - utils.StringersToFriendlyString(enum.RoutingZoneConstraintModeAllow), - utils.StringersToFriendlyString(enum.RoutingZoneConstraintModeDeny), - utils.StringersToFriendlyString(enum.RoutingZoneConstraintModeNone), - ), + MarkdownDescription: fmt.Sprintf("Instance constraint mode.\n"+ + "- `%s` - only allow the specified routing zones (add specific routing zones to allow)\n"+ + "- `%s` - denies allocation of specified routing zones (add specific routing zones to deny)\n"+ + "- `%s` - no additional constraints on routing zones (any routing zones)", + utils.StringersToFriendlyString(enum.RoutingZoneConstraintModeAllow), + utils.StringersToFriendlyString(enum.RoutingZoneConstraintModeDeny), + utils.StringersToFriendlyString(enum.RoutingZoneConstraintModeNone), ), Required: true, Validators: []validator.String{stringvalidator.OneOf( diff --git a/docs/data-sources/datacenter_routing_zone_constraint.md b/docs/data-sources/datacenter_routing_zone_constraint.md new file mode 100644 index 00000000..8703883d --- /dev/null +++ b/docs/data-sources/datacenter_routing_zone_constraint.md @@ -0,0 +1,63 @@ +--- +page_title: "apstra_datacenter_routing_zone_constraint Data Source - terraform-provider-apstra" +subcategory: "Reference Design: Datacenter" +description: |- + This resource returns details of a Routing Zone Constraint within a Datacenter Blueprint. + At least one optional attribute is required. +--- + +# apstra_datacenter_routing_zone_constraint (Data Source) + +This resource returns details of a Routing Zone Constraint within a Datacenter Blueprint. + +At least one optional attribute is required. + + +## Example Usage + +```terraform +# This example pulls the details of a Routing Zone Constraint +# using a "by name" lookup. Lookup by ID is also supported. + +data "apstra_datacenter_routing_zone_constraint" "vasili" { + blueprint_id = "372eca0d-41de-47cc-a17d-65f27960ca3f" + name = "one_zone_only" +} + +output "routing_zone_constraint" { + value = data.apstra_datacenter_routing_zone_constraint.vasili +} + +# The output looks like this: + +# routing_zone_constraint = { +# "blueprint_id" = "372eca0d-41de-47cc-a17d-65f27960ca3f" +# "constraints" = toset([ +# "6uEL07avVGEjxXYiZQ", +# "J7ApJRAmqWOIjVCV4A", +# "a8cU-tv0eNwj-KG-wg", +# ]) +# "id" = "qEH5mRPjsxhuyDovLg" +# "max_count_constraint" = 1 +# "name" = "one_zone_only" +# "routing_zones_list_constraint" = "allow" +# } +``` + + +## Schema + +### Required + +- `blueprint_id` (String) Apstra Blueprint ID. + +### Optional + +- `id` (String) Apstra graph node ID. Required when `name` is omitted. +- `name` (String) Name displayed in the Apstra web UI. Required when `id` is omitted. + +### Read-Only + +- `constraints` (Set of String) When `allow` instance constraint mode is chosen, only VNs from selected Routing Zones are allowed to have endpoints on the interface(s) the policy is applied to. The permitted Routing Zones may be specified directly or indirectly (via Routing Zone Groups) +- `max_count_constraint` (Number) The maximum number of Routing Zones that the Application Point can be part of. +- `routing_zones_list_constraint` (String) Routing Zone constraint mode. One of: `allow`, `deny`, `none`. diff --git a/docs/data-sources/datacenter_routing_zone_constraints.md b/docs/data-sources/datacenter_routing_zone_constraints.md new file mode 100644 index 00000000..0b6da24f --- /dev/null +++ b/docs/data-sources/datacenter_routing_zone_constraints.md @@ -0,0 +1,113 @@ +--- +page_title: "apstra_datacenter_routing_zone_constraints Data Source - terraform-provider-apstra" +subcategory: "Reference Design: Datacenter" +description: |- + This data source returns the IDs of Routing Zone Constraints within the specified Blueprint. All of the filter attributes are optional. +--- + +# apstra_datacenter_routing_zone_constraints (Data Source) + +This data source returns the IDs of Routing Zone Constraints within the specified Blueprint. All of the `filter` attributes are optional. + + +## Example Usage + +```terraform +# This example uses filters to find the ID of every Routing Zone +# Constraint which ether allows the routing zone named "dev-1" +# or allows the routing zone named "dev-2" + +data "apstra_datacenter_routing_zone" "dev-1" { + blueprint_id = "372eca0d-41de-47cc-a17d-65f27960ca3f" + name = "dev-1" +} + +data "apstra_datacenter_routing_zone" "dev-2" { + blueprint_id = "372eca0d-41de-47cc-a17d-65f27960ca3f" + name = "dev-2" +} + +data "apstra_datacenter_routing_zone_constraints" "allow_dev_1_or_dev_2" { + blueprint_id = "372eca0d-41de-47cc-a17d-65f27960ca3f" + filters = [ + { + routing_zones_list_constraint = "allow" + constraints = [data.apstra_datacenter_routing_zone.dev-1.id] + }, + { + routing_zones_list_constraint = "allow" + constraints = [data.apstra_datacenter_routing_zone.dev-2.id] + }, + ] +} + +output "constraint_allowing_dev_1_or_dev_2" { + value = data.apstra_datacenter_routing_zone_constraints.allow_dev_1_or_dev_2 +} + +# The output looks like this: +# constraint_allowing_dev_1_or_dev_2 = { +# "blueprint_id" = "372eca0d-41de-47cc-a17d-65f27960ca3f" +# "filters" = tolist([ +# { +# "blueprint_id" = tostring(null) +# "constraints" = toset([ +# "a8cU-tv0eNwj-KG-wg", +# ]) +# "id" = tostring(null) +# "max_count_constraint" = tonumber(null) +# "name" = tostring(null) +# "routing_zones_list_constraint" = "allow" +# }, +# { +# "blueprint_id" = tostring(null) +# "constraints" = toset([ +# "6uEL07avVGEjxXYiZQ", +# ]) +# "id" = tostring(null) +# "max_count_constraint" = tonumber(null) +# "name" = tostring(null) +# "routing_zones_list_constraint" = "allow" +# }, +# ]) +# "graph_queries" = tolist([ +# "match(node(name='n_routing_zone_constraint',type='routing_zone_constraint',routing_zones_list_constraint='allow'),node(name='n_routing_zone_constraint').out(type='constraint').node(type='security_zone',id='a8cU-tv0eNwj-KG-wg'))", +# "match(node(name='n_routing_zone_constraint',type='routing_zone_constraint',routing_zones_list_constraint='allow'),node(name='n_routing_zone_constraint').out(type='constraint').node(type='security_zone',id='6uEL07avVGEjxXYiZQ'))", +# ]) +# "ids" = toset([ +# "nbe8Ly6zUwXWWdGMjQ", +# "qEH5mRPjsxhuyDovLg", +# ]) +# } +``` + + +## Schema + +### Required + +- `blueprint_id` (String) Apstra Blueprint ID. + +### Optional + +- `filters` (Attributes List) List of filters used to select only desired node IDs. For a node to match a filter, all specified attributes must match (each attribute within a filter is AND-ed together). The returned node IDs represent the nodes matched by all of the filters together (filters are OR-ed together). (see [below for nested schema](#nestedatt--filters)) + +### Read-Only + +- `graph_queries` (List of String) Graph datastore queries which performed the lookup based on supplied filters. +- `ids` (Set of String) Set of Routing Zone Constraint IDs + + +### Nested Schema for `filters` + +Optional: + +- `constraints` (Set of String) Set of Routing Zone IDs. All Routing Zones supplied here are used to match the Routing Zone Constraint, but a matching Routing Zone Constraintmay have additional Security Zones not enumerated in this set. +- `max_count_constraint` (Number) The maximum number of Routing Zones that the Application Point can be part of. +- `name` (String) Name displayed in the Apstra web UI. +- `routing_zones_list_constraint` (String) Routing Zone constraint mode. One of: `allow`, `deny`, `none`. + +Read-Only: + +- `blueprint_id` (String) Not applicable in filter context. Ignore. +- `id` (String) Not applicable in filter context. Ignore. diff --git a/examples/data-sources/apstra_datacenter_routing_zone_constraint/example.tf b/examples/data-sources/apstra_datacenter_routing_zone_constraint/example.tf new file mode 100644 index 00000000..1406f8af --- /dev/null +++ b/examples/data-sources/apstra_datacenter_routing_zone_constraint/example.tf @@ -0,0 +1,26 @@ +# This example pulls the details of a Routing Zone Constraint +# using a "by name" lookup. Lookup by ID is also supported. + +data "apstra_datacenter_routing_zone_constraint" "vasili" { + blueprint_id = "372eca0d-41de-47cc-a17d-65f27960ca3f" + name = "one_zone_only" +} + +output "routing_zone_constraint" { + value = data.apstra_datacenter_routing_zone_constraint.vasili +} + +# The output looks like this: + +# routing_zone_constraint = { +# "blueprint_id" = "372eca0d-41de-47cc-a17d-65f27960ca3f" +# "constraints" = toset([ +# "6uEL07avVGEjxXYiZQ", +# "J7ApJRAmqWOIjVCV4A", +# "a8cU-tv0eNwj-KG-wg", +# ]) +# "id" = "qEH5mRPjsxhuyDovLg" +# "max_count_constraint" = 1 +# "name" = "one_zone_only" +# "routing_zones_list_constraint" = "allow" +# } diff --git a/examples/data-sources/apstra_datacenter_routing_zone_constraints/example.tf b/examples/data-sources/apstra_datacenter_routing_zone_constraints/example.tf new file mode 100644 index 00000000..a0750749 --- /dev/null +++ b/examples/data-sources/apstra_datacenter_routing_zone_constraints/example.tf @@ -0,0 +1,66 @@ +# This example uses filters to find the ID of every Routing Zone +# Constraint which ether allows the routing zone named "dev-1" +# or allows the routing zone named "dev-2" + +data "apstra_datacenter_routing_zone" "dev-1" { + blueprint_id = "372eca0d-41de-47cc-a17d-65f27960ca3f" + name = "dev-1" +} + +data "apstra_datacenter_routing_zone" "dev-2" { + blueprint_id = "372eca0d-41de-47cc-a17d-65f27960ca3f" + name = "dev-2" +} + +data "apstra_datacenter_routing_zone_constraints" "allow_dev_1_or_dev_2" { + blueprint_id = "372eca0d-41de-47cc-a17d-65f27960ca3f" + filters = [ + { + routing_zones_list_constraint = "allow" + constraints = [data.apstra_datacenter_routing_zone.dev-1.id] + }, + { + routing_zones_list_constraint = "allow" + constraints = [data.apstra_datacenter_routing_zone.dev-2.id] + }, + ] +} + +output "constraint_allowing_dev_1_or_dev_2" { + value = data.apstra_datacenter_routing_zone_constraints.allow_dev_1_or_dev_2 +} + +# The output looks like this: +# constraint_allowing_dev_1_or_dev_2 = { +# "blueprint_id" = "372eca0d-41de-47cc-a17d-65f27960ca3f" +# "filters" = tolist([ +# { +# "blueprint_id" = tostring(null) +# "constraints" = toset([ +# "a8cU-tv0eNwj-KG-wg", +# ]) +# "id" = tostring(null) +# "max_count_constraint" = tonumber(null) +# "name" = tostring(null) +# "routing_zones_list_constraint" = "allow" +# }, +# { +# "blueprint_id" = tostring(null) +# "constraints" = toset([ +# "6uEL07avVGEjxXYiZQ", +# ]) +# "id" = tostring(null) +# "max_count_constraint" = tonumber(null) +# "name" = tostring(null) +# "routing_zones_list_constraint" = "allow" +# }, +# ]) +# "graph_queries" = tolist([ +# "match(node(name='n_routing_zone_constraint',type='routing_zone_constraint',routing_zones_list_constraint='allow'),node(name='n_routing_zone_constraint').out(type='constraint').node(type='security_zone',id='a8cU-tv0eNwj-KG-wg'))", +# "match(node(name='n_routing_zone_constraint',type='routing_zone_constraint',routing_zones_list_constraint='allow'),node(name='n_routing_zone_constraint').out(type='constraint').node(type='security_zone',id='6uEL07avVGEjxXYiZQ'))", +# ]) +# "ids" = toset([ +# "nbe8Ly6zUwXWWdGMjQ", +# "qEH5mRPjsxhuyDovLg", +# ]) +# } From da40ca66f22ab6311a567488915a4b3a2c2c6d51 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Thu, 19 Dec 2024 23:30:18 -0500 Subject: [PATCH 5/7] tests for routing_zone_constraint resource --- apstra/export_test.go | 1 + ...outing_zone_constraint_integration_test.go | 223 ++++++++++++++++++ apstra/test_helpers_test.go | 22 ++ 3 files changed, 246 insertions(+) diff --git a/apstra/export_test.go b/apstra/export_test.go index 2603c62d..066a2065 100644 --- a/apstra/export_test.go +++ b/apstra/export_test.go @@ -21,6 +21,7 @@ var ( ResourceDatacenterGenericSystem = resourceDatacenterGenericSystem{} ResourceDatacenterIpLinkAddressing = resourceDatacenterIpLinkAddressing{} ResourceDatacenterRoutingZone = resourceDatacenterRoutingZone{} + ResourceDatacenterRoutingZoneConstraint = resourceDatacenterRoutingZoneConstraint{} ResourceDatacenterVirtualNetwork = resourceDatacenterVirtualNetwork{} ResourceFreeformAllocGroup = resourceFreeformAllocGroup{} ResourceFreeformBlueprint = resourceFreeformBlueprint{} diff --git a/apstra/resource_datacenter_routing_zone_constraint_integration_test.go b/apstra/resource_datacenter_routing_zone_constraint_integration_test.go index 20699884..306ea059 100644 --- a/apstra/resource_datacenter_routing_zone_constraint_integration_test.go +++ b/apstra/resource_datacenter_routing_zone_constraint_integration_test.go @@ -1 +1,224 @@ package tfapstra_test + +import ( + "context" + "fmt" + "github.com/Juniper/apstra-go-sdk/apstra" + "github.com/Juniper/apstra-go-sdk/apstra/enum" + tfapstra "github.com/Juniper/terraform-provider-apstra/apstra" + testutils "github.com/Juniper/terraform-provider-apstra/apstra/test_utils" + "github.com/Juniper/terraform-provider-apstra/apstra/utils" + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/stretchr/testify/require" + "strconv" + "testing" +) + +const ( + resourceDataCenterRoutingZoneConstraintHCL = `resource %q %q { + blueprint_id = %q // required attribute + name = %q // required attribute + routing_zones_list_constraint = %q // required attribute + max_count_constraint = %s + constraints = %s +} + +data %[1]q "by_id" { + blueprint_id = %[3]q + id = %[1]s.%[2]s.id + depends_on = [%[1]s.%[2]s] +} + +data %[1]q "by_name" { + blueprint_id = %[3]q + name = %[1]s.%[2]s.name + depends_on = [%[1]s.%[2]s] +} +` +) + +type testRoutingZoneConstraint struct { + name string + MaxCountConstraint *int + RoutingZoneListConstraint enum.RoutingZoneConstraintMode + Constraints []string +} + +func (o testRoutingZoneConstraint) render(bpId apstra.ObjectId, rType, rName string) string { + return fmt.Sprintf(resourceDataCenterRoutingZoneConstraintHCL, + rType, rName, + bpId, + o.name, + o.RoutingZoneListConstraint, + intPtrOrNull(o.MaxCountConstraint), + stringSliceOrNull(o.Constraints), + ) +} + +func (o testRoutingZoneConstraint) testChecks(t testing.TB, bpId apstra.ObjectId, 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", bpId.String()) + result.append(t, "TestCheckResourceAttr", "name", o.name) + result.append(t, "TestCheckResourceAttr", "routing_zones_list_constraint", o.RoutingZoneListConstraint.String()) + + if o.MaxCountConstraint == nil { + result.append(t, "TestCheckNoResourceAttr", "max_count_constraint") + } else { + result.append(t, "TestCheckResourceAttr", "max_count_constraint", strconv.Itoa(*o.MaxCountConstraint)) + } + + result.append(t, "TestCheckResourceAttr", "constraints.#", strconv.Itoa(len(o.Constraints))) + for _, constraint := range o.Constraints { + result.append(t, "TestCheckTypeSetElemAttr", "constraints.*", constraint) + } + + return result +} + +func TestResourceDatacenterRoutingZoneConstraint(t *testing.T) { + ctx := context.Background() + + // create a blueprint + bp := testutils.BlueprintA(t, ctx) + + routingZoneIds := make([]string, acctest.RandIntRange(5, 10)) + for i := range routingZoneIds { + label := acctest.RandString(6) + id, err := bp.CreateSecurityZone(ctx, &apstra.SecurityZoneData{ + Label: label, + SzType: apstra.SecurityZoneTypeEVPN, + VrfName: label, + }) + require.NoError(t, err) + routingZoneIds[i] = id.String() + } + + type testStep struct { + config testRoutingZoneConstraint + } + + type testCase struct { + steps []testStep + versionConstraints version.Constraints + } + + testCases := map[string]testCase{ + "start_minimal": { + steps: []testStep{ + { + config: testRoutingZoneConstraint{ + name: acctest.RandString(6), + RoutingZoneListConstraint: enum.RoutingZoneConstraintModeNone, + }, + }, + { + config: testRoutingZoneConstraint{ + name: acctest.RandString(6), + MaxCountConstraint: utils.ToPtr(acctest.RandIntRange(10, 100)), + RoutingZoneListConstraint: oneOf(enum.RoutingZoneConstraintModeAllow, enum.RoutingZoneConstraintModeDeny), + Constraints: randomSelection(routingZoneIds, len(routingZoneIds)/2), + }, + }, + { + config: testRoutingZoneConstraint{ + name: acctest.RandString(6), + RoutingZoneListConstraint: enum.RoutingZoneConstraintModeNone, + }, + }, + }, + }, + "start_maximal": { + steps: []testStep{ + { + config: testRoutingZoneConstraint{ + name: acctest.RandString(6), + MaxCountConstraint: utils.ToPtr(acctest.RandIntRange(10, 100)), + RoutingZoneListConstraint: oneOf(enum.RoutingZoneConstraintModeAllow, enum.RoutingZoneConstraintModeDeny), + Constraints: randomSelection(routingZoneIds, len(routingZoneIds)/2), + }, + }, + { + config: testRoutingZoneConstraint{ + name: acctest.RandString(6), + MaxCountConstraint: utils.ToPtr(acctest.RandIntRange(10, 100)), + RoutingZoneListConstraint: oneOf(enum.RoutingZoneConstraintModeAllow, enum.RoutingZoneConstraintModeDeny), + Constraints: randomSelection(routingZoneIds, len(routingZoneIds)/2), + }, + }, + { + config: testRoutingZoneConstraint{ + name: acctest.RandString(6), + RoutingZoneListConstraint: enum.RoutingZoneConstraintModeAllow, + }, + }, + { + config: testRoutingZoneConstraint{ + name: acctest.RandString(6), + RoutingZoneListConstraint: enum.RoutingZoneConstraintModeDeny, + }, + }, + { + config: testRoutingZoneConstraint{ + name: acctest.RandString(6), + RoutingZoneListConstraint: enum.RoutingZoneConstraintModeNone, + }, + }, + { + config: testRoutingZoneConstraint{ + name: acctest.RandString(6), + MaxCountConstraint: utils.ToPtr(acctest.RandIntRange(10, 100)), + RoutingZoneListConstraint: oneOf(enum.RoutingZoneConstraintModeAllow, enum.RoutingZoneConstraintModeDeny), + Constraints: randomSelection(routingZoneIds, len(routingZoneIds)/2), + }, + }, + { + config: testRoutingZoneConstraint{ + name: acctest.RandString(6), + MaxCountConstraint: utils.ToPtr(acctest.RandIntRange(10, 100)), + RoutingZoneListConstraint: oneOf(enum.RoutingZoneConstraintModeAllow, enum.RoutingZoneConstraintModeDeny), + Constraints: randomSelection(routingZoneIds, len(routingZoneIds)/2), + }, + }, + }, + }, + } + + resourceType := tfapstra.ResourceName(ctx, &tfapstra.ResourceDatacenterRoutingZoneConstraint) + + for tName, tCase := range testCases { + t.Run(tName, func(t *testing.T) { + t.Parallel() + + if !tCase.versionConstraints.Check(version.Must(version.NewVersion(bp.Client().ApiVersion()))) { + t.Skipf("test case %s requires Apstra %s", tName, tCase.versionConstraints.String()) + } + + steps := make([]resource.TestStep, len(tCase.steps)) + for i, step := range tCase.steps { + config := step.config.render(bp.Id(), resourceType, tName) + checks := step.config.testChecks(t, bp.Id(), 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 938c9287..5e201535 100644 --- a/apstra/test_helpers_test.go +++ b/apstra/test_helpers_test.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + "log" "math" "math/rand" "net" @@ -260,6 +261,27 @@ func randomIPs(t testing.TB, n int, ipv4Cidr, ipv6Cidr string) []string { return result } +func randomSelection[A comparable](s []A, n int) []A { + l := len(s) + if l < n { + log.Panicf("cannot randomly select %d members from a set of %d", n, l) + } + + m := make(map[A]struct{}, n) + for len(m) < n { + m[s[rand.Intn(l)]] = struct{}{} + } + + result := make([]A, n) + i := 0 + for k := range m { + result[i] = k + i++ + } + + return result +} + func randomStrings(strCount int, strLen int) []string { result := make([]string, strCount) for i := 0; i < strCount; i++ { From 8d472b79eb1a0908a5b16a28bf6f09ed9f019a91 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Thu, 19 Dec 2024 23:31:48 -0500 Subject: [PATCH 6/7] gofumpt --- ...ce_datacenter_routing_zone_constraint_integration_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apstra/resource_datacenter_routing_zone_constraint_integration_test.go b/apstra/resource_datacenter_routing_zone_constraint_integration_test.go index 306ea059..4bb1d38c 100644 --- a/apstra/resource_datacenter_routing_zone_constraint_integration_test.go +++ b/apstra/resource_datacenter_routing_zone_constraint_integration_test.go @@ -3,6 +3,9 @@ package tfapstra_test import ( "context" "fmt" + "strconv" + "testing" + "github.com/Juniper/apstra-go-sdk/apstra" "github.com/Juniper/apstra-go-sdk/apstra/enum" tfapstra "github.com/Juniper/terraform-provider-apstra/apstra" @@ -12,8 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/stretchr/testify/require" - "strconv" - "testing" ) const ( From 9dc622b2adcd7e65b5e12114fd3477a8faa51d50 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Thu, 19 Dec 2024 23:33:46 -0500 Subject: [PATCH 7/7] missing build constraint --- ...ource_datacenter_routing_zone_constraint_integration_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apstra/resource_datacenter_routing_zone_constraint_integration_test.go b/apstra/resource_datacenter_routing_zone_constraint_integration_test.go index 4bb1d38c..d8d53136 100644 --- a/apstra/resource_datacenter_routing_zone_constraint_integration_test.go +++ b/apstra/resource_datacenter_routing_zone_constraint_integration_test.go @@ -1,3 +1,5 @@ +//go:build integration + package tfapstra_test import (