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=