From 23b9919c099cc4c1579c8d890f2a7707217bb292 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Thu, 19 Dec 2024 17:23:45 -0500 Subject: [PATCH] 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 +}