Skip to content

Commit

Permalink
routing zone constraint resource
Browse files Browse the repository at this point in the history
todo:
- resource tests
- constraint data source
- constraints data source
  • Loading branch information
chrismarget-j committed Dec 19, 2024
1 parent 76a8f08 commit 23b9919
Show file tree
Hide file tree
Showing 2 changed files with 379 additions and 0 deletions.
175 changes: 175 additions & 0 deletions apstra/blueprint/routing_zone_constraint.go
Original file line number Diff line number Diff line change
@@ -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)
}
204 changes: 204 additions & 0 deletions apstra/resource_datacenter_routing_zone_constraint.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 23b9919

Please sign in to comment.