diff --git a/apstra/blueprint/routing_zone_constraint.go b/apstra/blueprint/routing_zone_constraint.go new file mode 100644 index 00000000..eb619efa --- /dev/null +++ b/apstra/blueprint/routing_zone_constraint.go @@ -0,0 +1,263 @@ +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" + "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" +) + +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) 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{ + 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("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) +} + +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/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/provider.go b/apstra/provider.go index 7fd86d45..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{} }, @@ -628,6 +630,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/apstra/resource_datacenter_routing_zone_constraint.go b/apstra/resource_datacenter_routing_zone_constraint.go new file mode 100644 index 00000000..a5a75e4a --- /dev/null +++ b/apstra/resource_datacenter_routing_zone_constraint.go @@ -0,0 +1,211 @@ +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{} + _ resourceWithSetDcBpClientFunc = &resourceDatacenterRoutingZoneConstraint{} + _ 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 + } + 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) + 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 +} 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..d8d53136 --- /dev/null +++ b/apstra/resource_datacenter_routing_zone_constraint_integration_test.go @@ -0,0 +1,227 @@ +//go:build integration + +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" + 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" +) + +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++ { 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/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/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", +# ]) +# } 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 + ] +} 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=