-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #770 from Juniper/763-freeform-resource-generators
763 freeform resource generators
- Loading branch information
Showing
12 changed files
with
1,235 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.