Skip to content

Commit

Permalink
Merge pull request #771 from Juniper/review/763-freeform-resource-gen…
Browse files Browse the repository at this point in the history
…erators

763 review
  • Loading branch information
bwJuniper authored Aug 9, 2024
2 parents 84a252f + 72c382f commit 2edff95
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 205 deletions.
64 changes: 36 additions & 28 deletions apstra/blueprint/freeform_resource_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (
type FreeformResourceGenerator struct {
BlueprintId types.String `tfsdk:"blueprint_id"`
Id types.String `tfsdk:"id"`
ResourceType types.String `tfsdk:"type"`
Type types.String `tfsdk:"type"`
Name types.String `tfsdk:"name"`
Scope types.String `tfsdk:"scope"`
AllocatedFrom types.String `tfsdk:"allocated_from"`
Expand Down Expand Up @@ -63,13 +63,14 @@ func (o FreeformResourceGenerator) DataSourceAttributes() map[string]dataSourceS
Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
},
"scope": dataSourceSchema.StringAttribute{
MarkdownDescription: "Scope the Resource Generator uses for resource generation",
Computed: true,
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: "ID of the node from which this resource generator has been sourced. This could be an ID " +
"of resource generator or another resource (in case of IP or Host IP allocations). " +
"This also can be empty. In that case it is required that value for this resource is provided by the user.",
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{
Expand Down Expand Up @@ -107,55 +108,62 @@ func (o FreeformResourceGenerator) ResourceAttributes() map[string]resourceSchem
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.-_"),
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 the Resource Generator uses for resource generation.",
Required: true,
Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
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: "ID of the node to be used as a source for this resource. This could be an ID " +
"of resource group or another resource (in case of IP or Host IP allocations). " +
"This also can be empty. In that case it is required that value for this resource is provided by the user.",
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. ",
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: "Length of the subnet for the generated resources, if any.",
Optional: true,
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("allocated_from"), types.StringValue(utils.StringersToFriendlyString(apstra.FFResourceTypeAsn))),
apstravalidator.ForbiddenWhenValueIs(path.MatchRoot("allocated_from"), types.StringValue(utils.StringersToFriendlyString(apstra.FFResourceTypeHostIpv4))),
apstravalidator.ForbiddenWhenValueIs(path.MatchRoot("allocated_from"), types.StringValue(utils.StringersToFriendlyString(apstra.FFResourceTypeHostIpv6))),
apstravalidator.ForbiddenWhenValueIs(path.MatchRoot("allocated_from"), types.StringValue(utils.StringersToFriendlyString(apstra.FFResourceTypeInt))),
apstravalidator.ForbiddenWhenValueIs(path.MatchRoot("allocated_from"), types.StringValue(utils.StringersToFriendlyString(apstra.FFResourceTypeVlan))),
apstravalidator.ForbiddenWhenValueIs(path.MatchRoot("allocated_from"), types.StringValue(utils.StringersToFriendlyString(apstra.FFResourceTypeVni))),
apstravalidator.RequiredWhenValueIs(path.MatchRoot("allocated_from"), types.StringValue(utils.StringersToFriendlyString(apstra.FFResourceTypeIpv4))),
apstravalidator.RequiredWhenValueIs(path.MatchRoot("allocated_from"), types.StringValue(utils.StringersToFriendlyString(apstra.FFResourceTypeIpv6))),
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.ResourceType.ValueString())
err := utils.ApiStringerFromFriendlyString(&resourceType, o.Type.ValueString())
if err != nil {
diags.AddError(fmt.Sprintf("error parsing type %q", o.ResourceType.ValueString()), err.Error())
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 {
Expand All @@ -182,7 +190,7 @@ func (o *FreeformResourceGenerator) Request(_ context.Context, diags *diag.Diagn
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.ResourceType = types.StringValue(utils.StringersToFriendlyString(in.ResourceType))
o.Type = types.StringValue(utils.StringersToFriendlyString(in.ResourceType))
if in.ResourceType == apstra.FFResourceTypeVlan {
o.AllocatedFrom = types.StringPointerValue(in.ScopeNodePoolLabel)
} else {
Expand Down
14 changes: 7 additions & 7 deletions apstra/data_source_freeform_resource_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func (o *dataSourceFreeformResourceGenerator) Configure(ctx context.Context, req

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.\n\n" +
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(),
}
Expand Down Expand Up @@ -63,26 +63,26 @@ func (o *dataSourceFreeformResourceGenerator) Read(ctx context.Context, req data
if utils.IsApstra404(err) {
resp.Diagnostics.AddAttributeError(
path.Root("id"),
"Freeform Resource not found",
fmt.Sprintf("Freeform Resource with ID %s not found", config.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 not found",
fmt.Sprintf("Freeform Resource with Name %s not found", config.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", err.Error())
resp.Diagnostics.AddError("failed reading Freeform Resource Generator", err.Error())
return
}
if api.Data == nil {
resp.Diagnostics.AddError("failed reading Freeform Resource", "api response has no payload")
resp.Diagnostics.AddError("failed reading Freeform Resource Generator", "api response has no payload")
return
}

Expand Down
22 changes: 14 additions & 8 deletions apstra/resource_freeform_resource_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func (o *resourceFreeformResourceGenerator) Configure(ctx context.Context, req r

func (o *resourceFreeformResourceGenerator) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: docCategoryFreeform + "This resource creates a Resource in a Freeform Blueprint.",
MarkdownDescription: docCategoryFreeform + "This resource creates a Resource Generator in a Freeform Blueprint.",
Attributes: blueprint.FreeformResourceGenerator{}.ResourceAttributes(),
}
}
Expand All @@ -47,17 +47,23 @@ func (o *resourceFreeformResourceGenerator) ValidateConfig(ctx context.Context,
if resp.Diagnostics.HasError() {
return
}
if config.ResourceType.IsUnknown() || config.SubnetPrefixLen.IsUnknown() {

// We only compare two values. Validation requires that both be known.
if config.Type.IsUnknown() || config.SubnetPrefixLen.IsUnknown() {
return
}

// Extract the type
var resourceType apstra.FFResourceType
err := utils.ApiStringerFromFriendlyString(&resourceType, config.ResourceType.ValueString())
err := utils.ApiStringerFromFriendlyString(&resourceType, config.Type.ValueString())
if err != nil {
resp.Diagnostics.AddAttributeError(path.Root("type"), "failed to parse 'type' attribute", err.Error())
return
}

// Catch v6-sized prefix specified when requesting a v4 subnet.
if resourceType == apstra.FFResourceTypeIpv4 && config.SubnetPrefixLen.ValueInt64() > 32 {
resp.Diagnostics.AddAttributeError(path.Root("subnet_prefix_len"), " 'subnet_prefix_len' cannot be greater than 32 when 'type' is %s", config.ResourceType.String())
resp.Diagnostics.AddAttributeError(path.Root("subnet_prefix_len"), " 'subnet_prefix_len' cannot be greater than 32 when 'type' is %s", config.Type.String())
return
}
}
Expand Down Expand Up @@ -99,7 +105,7 @@ func (o *resourceFreeformResourceGenerator) Create(ctx context.Context, req reso
// Create the resource
id, err := bp.CreateResourceGenerator(ctx, request)
if err != nil {
resp.Diagnostics.AddError("error creating new Resource", err.Error())
resp.Diagnostics.AddError("error creating new Resource Generator", err.Error())
return
}

Expand All @@ -120,7 +126,7 @@ func (o *resourceFreeformResourceGenerator) Read(ctx context.Context, req resour
bp, err := o.getBpClientFunc(ctx, state.BlueprintId.ValueString())
if err != nil {
if utils.IsApstra404(err) {
resp.Diagnostics.AddError(fmt.Sprintf("blueprint %s not found", state.BlueprintId), err.Error())
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("failed to create blueprint client", err.Error())
Expand All @@ -133,7 +139,7 @@ func (o *resourceFreeformResourceGenerator) Read(ctx context.Context, req resour
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("Error retrieving Freeform Resource", err.Error())
resp.Diagnostics.AddError("Error retrieving Freeform Resource Generator", err.Error())
return
}

Expand Down Expand Up @@ -183,7 +189,7 @@ func (o *resourceFreeformResourceGenerator) Update(ctx context.Context, req reso
// Update the Resource
err = bp.UpdateResourceGenerator(ctx, apstra.ObjectId(plan.Id.ValueString()), request)
if err != nil {
resp.Diagnostics.AddError("error updating Freeform Resource", err.Error())
resp.Diagnostics.AddError("error updating Freeform Resource Generator", err.Error())
return
}

Expand Down
85 changes: 43 additions & 42 deletions docs/data-sources/freeform_resource_generator.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
page_title: "apstra_freeform_resource_generator Data Source - terraform-provider-apstra"
subcategory: "Reference Design: Freeform"
description: |-
This data source provides details of a specific Freeform Resource.
This data source provides details of a specific Freeform Resource Generator.
At least one optional attribute is required.
---

# apstra_freeform_resource_generator (Data Source)

This data source provides details of a specific Freeform Resource.
This data source provides details of a specific Freeform Resource Generator.

At least one optional attribute is required.

Expand All @@ -21,54 +21,54 @@ At least one optional attribute is required.
#
# After creating the Resource Generator, the data source is invoked to look up
# the details.
resource "apstra_freeform_resource_group" "fizz_grp" {
blueprint_id = "631f8832-ae59-40ca-b4f6-9c19b411aeaf"
name = "fizz_grp"
}
resource "apstra_asn_pool" "rfc5398" {
name = "RFC5398 ASN"
ranges = [
{
first = 64496
last = 64511
},
{
first = 65536
last = 65551
},
]
# Create a resource group in a preexisting blueprint.
resource "apstra_freeform_resource_group" "fizz_grp" {
blueprint_id = "f1b86583-9139-49ed-8a3c-0490253e006e"
name = "fizz_grp"
}
resource "apstra_freeform_alloc_group" "test" {
blueprint_id = "631f8832-ae59-40ca-b4f6-9c19b411aeaf"
name = "test_alloc_group2"
type = "asn"
pool_ids = [apstra_asn_pool.rfc5398.id]
# Create an allocation group in a preexisting blueprint
# using a preexisting ASN pool.
resource "apstra_freeform_allocation_group" "test" {
blueprint_id = "f1b86583-9139-49ed-8a3c-0490253e006e"
name = "test_allocation_group2"
type = "asn"
pool_ids = ["Private-64512-65534"]
}
# Create a resource generator scoped to target all systems in the blueprint.
resource "apstra_freeform_resource_generator" "test_res_gen" {
blueprint_id = "631f8832-ae59-40ca-b4f6-9c19b411aeaf"
name = "test_res_gen"
type = "asn"
scope = "node('system', name='target')"
allocated_from = apstra_freeform_alloc_group.test.id
container_id = apstra_freeform_resource_group.fizz_grp.id
blueprint_id = "f1b86583-9139-49ed-8a3c-0490253e006e"
name = "test_res_gen"
type = "asn"
scope = "node('system', name='target')"
allocated_from = apstra_freeform_allocation_group.test.id
container_id = apstra_freeform_resource_group.fizz_grp.id
}
# Invoke the resource generator data source
data "apstra_freeform_resource_generator" "test_res_gen" {
blueprint_id = "f1b86583-9139-49ed-8a3c-0490253e006e"
id = apstra_freeform_resource_generator.test_res_gen.id
}
# Output the data source so that it prints on screen
output "test_resource_generator_out" {
value = data.apstra_freeform_resource_generator.test_res_gen
}
# The output looks like:
#test_resource_generator_out = {
# "allocated_from" = "rag_asn_test_alloc_group2"
# "blueprint_id" = "631f8832-ae59-40ca-b4f6-9c19b411aeaf"
# "container_id" = "gPJtXP7_SM31CYWDJ0g"
# "id" = "EkrP9avh6pgqRqRCm44"
# "name" = "test_res_gen"
# "scope" = "node('system', name='target')"
# "subnet_prefix_len" = tonumber(null)
# "type" = "asn"
#}
# The output looks like this:
# test_resource_generator_out = {
# "allocated_from" = "rag_asn_test_allocation_group2"
# "blueprint_id" = "f1b86583-9139-49ed-8a3c-0490253e006e"
# "container_id" = "NbzITYcIjPZN4ZBqS2Q"
# "id" = "pwJ9EOiVR8z6qbj2Ou8"
# "name" = "test_res_gen"
# "scope" = "node('system', name='target')"
# "subnet_prefix_len" = tonumber(null)
# "type" = "asn"
# }
```

<!-- schema generated by tfplugindocs -->
Expand All @@ -85,8 +85,9 @@ resource "apstra_freeform_resource_generator" "test_res_gen" {

### Read-Only

- `allocated_from` (String) ID of the node from which this resource generator has been sourced. This could be an ID of resource generator or another resource (in case of IP or Host IP allocations). This also can be empty. In that case it is required that value for this resource is provided by the user.
- `allocated_from` (String) 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.
- `container_id` (String) ID of the group used to organize the generated resources
- `scope` (String) Scope the Resource Generator uses for resource generation
- `scope` (String) Scope is a graph query which selects target nodes for which Resources should be generated.
Example: `node('system', name='target', label=aeq('*prod*'))`
- `subnet_prefix_len` (Number) Length of the subnet for the generated resources, if any.
- `type` (String) Type of the Resource Generator
Loading

0 comments on commit 2edff95

Please sign in to comment.