Skip to content

Commit

Permalink
Merge pull request #770 from Juniper/763-freeform-resource-generators
Browse files Browse the repository at this point in the history
763 freeform resource generators
  • Loading branch information
bwJuniper authored Aug 9, 2024
2 parents 716199c + 2edff95 commit 3a666d3
Show file tree
Hide file tree
Showing 12 changed files with 1,235 additions and 3 deletions.
201 changes: 201 additions & 0 deletions apstra/blueprint/freeform_resource_generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package blueprint

import (
"context"
"fmt"
apstravalidator "github.com/Juniper/terraform-provider-apstra/apstra/apstra_validator"
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
"regexp"
"strings"

"github.com/Juniper/apstra-go-sdk/apstra"
"github.com/Juniper/terraform-provider-apstra/apstra/utils"
"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 FreeformResourceGenerator struct {
BlueprintId types.String `tfsdk:"blueprint_id"`
Id types.String `tfsdk:"id"`
Type types.String `tfsdk:"type"`
Name types.String `tfsdk:"name"`
Scope types.String `tfsdk:"scope"`
AllocatedFrom types.String `tfsdk:"allocated_from"`
ContainerId types.String `tfsdk:"container_id"`
SubnetPrefixLen types.Int64 `tfsdk:"subnet_prefix_len"`
}

func (o FreeformResourceGenerator) DataSourceAttributes() map[string]dataSourceSchema.Attribute {
return map[string]dataSourceSchema.Attribute{
"blueprint_id": dataSourceSchema.StringAttribute{
MarkdownDescription: "Apstra Blueprint ID. Used to identify " +
"the Blueprint where the Resource lives.",
Required: true,
Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
},
"id": dataSourceSchema.StringAttribute{
MarkdownDescription: "Populate this field to look up the Freeform Resource Generator by ID. Required when `name` is omitted.",
Optional: true,
Computed: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
stringvalidator.ExactlyOneOf(path.Expressions{
path.MatchRelative(),
path.MatchRoot("name"),
}...),
},
},
"type": dataSourceSchema.StringAttribute{
MarkdownDescription: "Type of the Resource Generator",
Computed: true,
},
"name": dataSourceSchema.StringAttribute{
MarkdownDescription: "Populate this field to look up Resource Generator by Name. Required when `id` is omitted.",
Optional: true,
Computed: true,
Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
},
"scope": dataSourceSchema.StringAttribute{
MarkdownDescription: "Scope is a graph query which selects target nodes for which Resources should be generated.\n" +
"Example: `node('system', name='target', label=aeq('*prod*'))`",
Computed: true,
},
"allocated_from": dataSourceSchema.StringAttribute{
MarkdownDescription: "Selects the Allocation Group, parent Resource, or Local Resource Pool from which to " +
"source generated Resources. In the case of a Local Resource Pool, this value must be the name (label) " +
"of the pool. Allocation Groups and parent Resources are specified by ID.",
Computed: true,
},
"container_id": dataSourceSchema.StringAttribute{
MarkdownDescription: "ID of the group used to organize the generated resources",
Computed: true,
},
"subnet_prefix_len": dataSourceSchema.Int64Attribute{
MarkdownDescription: "Length of the subnet for the generated resources, if any.",
Computed: true,
},
}
}

func (o FreeformResourceGenerator) ResourceAttributes() map[string]resourceSchema.Attribute {
return map[string]resourceSchema.Attribute{
"blueprint_id": resourceSchema.StringAttribute{
MarkdownDescription: "Apstra Blueprint ID.",
Required: true,
Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
},
"id": resourceSchema.StringAttribute{
MarkdownDescription: "ID of the Resource Generator within the Freeform Blueprint.",
Computed: true,
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
},
"type": resourceSchema.StringAttribute{
MarkdownDescription: "type of the Resource Generator, must be one of :\n - `" +
strings.Join(utils.AllFFResourceTypes(), "`\n - `") + "`\n",
Required: true,
Validators: []validator.String{stringvalidator.OneOf(utils.AllFFResourceTypes()...)},
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
},
"name": resourceSchema.StringAttribute{
MarkdownDescription: "Freeform Resource Generator name as shown in the Web UI.",
Required: true,
Validators: []validator.String{
stringvalidator.RegexMatches(
regexp.MustCompile("^[a-zA-Z0-9.-_]+$"),
"name may consist only of the following characters : a-zA-Z0-9.-_",
),
},
},
"scope": resourceSchema.StringAttribute{
MarkdownDescription: "Scope is a graph query which selects target nodes for which Resources should be generated.\n" +
"Example: `node('system', name='target', label=aeq('*prod*'))`",
Required: true,
Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
},
"allocated_from": resourceSchema.StringAttribute{
MarkdownDescription: "Selects the Allocation Group, parent Resource, or Local Resource Pool from which to " +
"source generated Resources. In the case of a Local Resource Pool, this value must be the name (label) " +
"of the pool. Allocation Groups and parent Resources are specified by ID.",
Required: true,
Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
},
"container_id": resourceSchema.StringAttribute{
MarkdownDescription: "ID of the group where Resources are generated. ",
Required: true,
Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
},
"subnet_prefix_len": resourceSchema.Int64Attribute{
MarkdownDescription: fmt.Sprintf("Length of the subnet for the generated Resources. "+
"Only applicable when `type` is `%s` or `%s`",
utils.StringersToFriendlyString(apstra.FFResourceTypeIpv4),
utils.StringersToFriendlyString(apstra.FFResourceTypeIpv6),
),
Optional: true,
Validators: []validator.Int64{
int64validator.Between(1, 127),
apstravalidator.ForbiddenWhenValueIs(path.MatchRoot("type"), types.StringValue(utils.StringersToFriendlyString(apstra.FFResourceTypeAsn))),
apstravalidator.ForbiddenWhenValueIs(path.MatchRoot("type"), types.StringValue(utils.StringersToFriendlyString(apstra.FFResourceTypeHostIpv4))),
apstravalidator.ForbiddenWhenValueIs(path.MatchRoot("type"), types.StringValue(utils.StringersToFriendlyString(apstra.FFResourceTypeHostIpv6))),
apstravalidator.ForbiddenWhenValueIs(path.MatchRoot("type"), types.StringValue(utils.StringersToFriendlyString(apstra.FFResourceTypeInt))),
apstravalidator.ForbiddenWhenValueIs(path.MatchRoot("type"), types.StringValue(utils.StringersToFriendlyString(apstra.FFResourceTypeVlan))),
apstravalidator.ForbiddenWhenValueIs(path.MatchRoot("type"), types.StringValue(utils.StringersToFriendlyString(apstra.FFResourceTypeVni))),
apstravalidator.RequiredWhenValueIs(path.MatchRoot("type"), types.StringValue(utils.StringersToFriendlyString(apstra.FFResourceTypeIpv4))),
apstravalidator.RequiredWhenValueIs(path.MatchRoot("type"), types.StringValue(utils.StringersToFriendlyString(apstra.FFResourceTypeIpv6))),
},
},
}
}

func (o *FreeformResourceGenerator) Request(_ context.Context, diags *diag.Diagnostics) *apstra.FreeformResourceGeneratorData {
var resourceType apstra.FFResourceType
err := utils.ApiStringerFromFriendlyString(&resourceType, o.Type.ValueString())
if err != nil {
diags.AddError(fmt.Sprintf("error parsing type %q", o.Type.ValueString()), err.Error())
}

var scopeNodePoolLabel *string
var allocatedFrom *apstra.ObjectId
if resourceType == apstra.FFResourceTypeVlan {
scopeNodePoolLabel = o.AllocatedFrom.ValueStringPointer()
} else {
allocatedFrom = (*apstra.ObjectId)(o.AllocatedFrom.ValueStringPointer())
}

var subnetPrefixLen *int
if !o.SubnetPrefixLen.IsNull() {
l := int(o.SubnetPrefixLen.ValueInt64())
subnetPrefixLen = &l
}

return &apstra.FreeformResourceGeneratorData{
ResourceType: resourceType,
Label: o.Name.ValueString(),
Scope: o.Scope.ValueString(),
AllocatedFrom: allocatedFrom,
ScopeNodePoolLabel: scopeNodePoolLabel,
ContainerId: apstra.ObjectId(o.ContainerId.ValueString()),
SubnetPrefixLen: subnetPrefixLen,
}
}

func (o *FreeformResourceGenerator) LoadApiData(_ context.Context, in *apstra.FreeformResourceGeneratorData, diags *diag.Diagnostics) {
o.Name = types.StringValue(in.Label)
o.Scope = types.StringValue(in.Scope)
o.Type = types.StringValue(utils.StringersToFriendlyString(in.ResourceType))
if in.ResourceType == apstra.FFResourceTypeVlan {
o.AllocatedFrom = types.StringPointerValue(in.ScopeNodePoolLabel)
} else {
o.AllocatedFrom = types.StringPointerValue((*string)(in.AllocatedFrom))
}
o.ContainerId = types.StringValue(string(in.ContainerId))
o.SubnetPrefixLen = int64AttrValueFromPtr(in.SubnetPrefixLen)
}
101 changes: 101 additions & 0 deletions apstra/data_source_freeform_resource_generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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 = &dataSourceFreeformResourceGenerator{}
_ datasourceWithSetFfBpClientFunc = &dataSourceFreeformResourceGenerator{}
)

type dataSourceFreeformResourceGenerator struct {
getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error)
}

func (o *dataSourceFreeformResourceGenerator) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_freeform_resource_generator"
}

func (o *dataSourceFreeformResourceGenerator) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
configureDataSource(ctx, o, req, resp)
}

func (o *dataSourceFreeformResourceGenerator) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: docCategoryFreeform + "This data source provides details of a specific Freeform Resource Generator.\n\n" +
"At least one optional attribute is required.",
Attributes: blueprint.FreeformResourceGenerator{}.DataSourceAttributes(),
}
}

func (o *dataSourceFreeformResourceGenerator) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config blueprint.FreeformResourceGenerator
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}

// get a client for the Freeform reference design
bp, err := o.getBpClientFunc(ctx, config.BlueprintId.ValueString())
if err != nil {
if utils.IsApstra404(err) {
resp.Diagnostics.AddError(fmt.Sprintf("blueprint %s not found", config.BlueprintId), err.Error())
return
}
resp.Diagnostics.AddError("failed to create blueprint client", err.Error())
return
}

var api *apstra.FreeformResourceGenerator
switch {
case !config.Id.IsNull():
api, err = bp.GetResourceGenerator(ctx, apstra.ObjectId(config.Id.ValueString()))
if utils.IsApstra404(err) {
resp.Diagnostics.AddAttributeError(
path.Root("id"),
"Freeform Resource Generator not found",
fmt.Sprintf("Freeform Resource Generator with ID %s not found", config.Id))
return
}
case !config.Name.IsNull():
api, err = bp.GetResourceGeneratorByName(ctx, config.Name.ValueString())
if utils.IsApstra404(err) {
resp.Diagnostics.AddAttributeError(
path.Root("name"),
"Freeform Resource Generator not found",
fmt.Sprintf("Freeform Resource Generator with Name %s not found", config.Name))
return
}
}
if err != nil {
resp.Diagnostics.AddError("failed reading Freeform Resource Generator", err.Error())
return
}
if api.Data == nil {
resp.Diagnostics.AddError("failed reading Freeform Resource Generator", "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 *dataSourceFreeformResourceGenerator) setBpClientFunc(f func(context.Context, string) (*apstra.FreeformClient, error)) {
o.getBpClientFunc = f
}
1 change: 1 addition & 0 deletions apstra/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var (
ResourceFreeformLink = resourceFreeformLink{}
ResourceFreeformPropertySet = resourceFreeformPropertySet{}
ResourceFreeformResourceGroup = resourceFreeformResourceGroup{}
ResourceFreeformResourceGenerator = resourceFreeformResourceGenerator{}
ResourceFreeformResource = resourceFreeformResource{}
ResourceFreeformAllocGroup = resourceFreeformAllocGroup{}
ResourceFreeformSystem = resourceFreeformSystem{}
Expand Down
2 changes: 2 additions & 0 deletions apstra/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource
func() datasource.DataSource { return &dataSourceFreeformConfigTemplate{} },
func() datasource.DataSource { return &dataSourceFreeformLink{} },
func() datasource.DataSource { return &dataSourceFreeformPropertySet{} },
func() datasource.DataSource { return &dataSourceFreeformResourceGenerator{} },
func() datasource.DataSource { return &dataSourceFreeformResourceGroup{} },
func() datasource.DataSource { return &dataSourceFreeformResource{} },
func() datasource.DataSource { return &dataSourceFreeformSystem{} },
Expand Down Expand Up @@ -619,6 +620,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource {
func() resource.Resource { return &resourceFreeformConfigTemplate{} },
func() resource.Resource { return &resourceFreeformLink{} },
func() resource.Resource { return &resourceFreeformPropertySet{} },
func() resource.Resource { return &resourceFreeformResourceGenerator{} },
func() resource.Resource { return &resourceFreeformResourceGroup{} },
func() resource.Resource { return &resourceFreeformResource{} },
func() resource.Resource { return &resourceFreeformSystem{} },
Expand Down
Loading

0 comments on commit 3a666d3

Please sign in to comment.