From db7bf4d4e40a596395decb42d99bd8af29427022 Mon Sep 17 00:00:00 2001 From: Bill Wester Date: Thu, 8 Aug 2024 18:21:18 +0200 Subject: [PATCH 1/6] initial work on resource generators --- .../blueprint/freeform_resource_generator.go | 193 ++++++++++++++ ...data_source_freeform_resource_generator.go | 101 ++++++++ apstra/provider.go | 2 + .../resource_freeform_resource_generator.go | 237 ++++++++++++++++++ 4 files changed, 533 insertions(+) create mode 100644 apstra/blueprint/freeform_resource_generator.go create mode 100644 apstra/data_source_freeform_resource_generator.go create mode 100644 apstra/resource_freeform_resource_generator.go diff --git a/apstra/blueprint/freeform_resource_generator.go b/apstra/blueprint/freeform_resource_generator.go new file mode 100644 index 00000000..81ab15d4 --- /dev/null +++ b/apstra/blueprint/freeform_resource_generator.go @@ -0,0 +1,193 @@ +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"` + ResourceType 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 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", + Computed: true, + }, + "name": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Populate this field to look up Resource by Name. Required when `id` is omitted.", + Optional: true, + Computed: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "scope": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Scope the Resource Generator uses for resource generation", + 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.", + Computed: true, + }, + "container_id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "ID of the group generator that created the group, if any.", + 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 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 the Resource Generator uses for resource generation.", + 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.", + 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: "Length of the subnet for the generated resources, if any.", + 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))), + }, + }, + } +} + +func (o *FreeformResourceGenerator) Request(_ context.Context, diags *diag.Diagnostics) *apstra.FreeformResourceGeneratorData { + var resourceType apstra.FFResourceType + err := utils.ApiStringerFromFriendlyString(&resourceType, o.ResourceType.ValueString()) + if err != nil { + diags.AddError(fmt.Sprintf("error parsing type %q", o.ResourceType.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.ResourceType = 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) +} diff --git a/apstra/data_source_freeform_resource_generator.go b/apstra/data_source_freeform_resource_generator.go new file mode 100644 index 00000000..5ecd42cb --- /dev/null +++ b/apstra/data_source_freeform_resource_generator.go @@ -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.\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 not found", + fmt.Sprintf("Freeform Resource 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)) + return + } + } + if err != nil { + resp.Diagnostics.AddError("failed reading Freeform Resource", err.Error()) + return + } + if api.Data == nil { + resp.Diagnostics.AddError("failed reading Freeform Resource", "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 +} diff --git a/apstra/provider.go b/apstra/provider.go index d5171b18..eaa05af9 100644 --- a/apstra/provider.go +++ b/apstra/provider.go @@ -555,6 +555,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{} }, @@ -617,6 +618,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{} }, diff --git a/apstra/resource_freeform_resource_generator.go b/apstra/resource_freeform_resource_generator.go new file mode 100644 index 00000000..fc5e0988 --- /dev/null +++ b/apstra/resource_freeform_resource_generator.go @@ -0,0 +1,237 @@ +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/path" + "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 = &resourceFreeformResourceGenerator{} + _ resource.ResourceWithValidateConfig = &resourceFreeformResourceGenerator{} + _ resourceWithSetFfBpClientFunc = &resourceFreeformResourceGenerator{} + _ resourceWithSetBpLockFunc = &resourceFreeformResourceGenerator{} +) + +type resourceFreeformResourceGenerator struct { + getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) + lockFunc func(context.Context, string) error +} + +func (o *resourceFreeformResourceGenerator) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_freeform_resource_generator" +} + +func (o *resourceFreeformResourceGenerator) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + configureResource(ctx, o, req, resp) +} + +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.", + Attributes: blueprint.FreeformResourceGenerator{}.ResourceAttributes(), + } +} + +func (o *resourceFreeformResourceGenerator) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + // Retrieve values from config + var config blueprint.FreeformResourceGenerator + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config.ResourceType.IsUnknown() || config.SubnetPrefixLen.IsUnknown() { + return + } + var resourceType apstra.FFResourceType + err := utils.ApiStringerFromFriendlyString(&resourceType, config.ResourceType.ValueString()) + if err != nil { + resp.Diagnostics.AddAttributeError(path.Root("type"), "failed to parse 'type' attribute", err.Error()) + return + } + 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()) + return + } +} + +func (o *resourceFreeformResourceGenerator) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan blueprint.FreeformResourceGenerator + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // get a client for the Freeform 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 + } + + // Convert the plan into an API Request + request := plan.Request(ctx, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // Create the resource + id, err := bp.CreateResourceGenerator(ctx, request) + if err != nil { + resp.Diagnostics.AddError("error creating new Resource", err.Error()) + return + } + + plan.Id = types.StringValue(id.String()) + + // set state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (o *resourceFreeformResourceGenerator) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state blueprint.FreeformResourceGenerator + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + // get a client for the Freeform reference design + 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()) + return + } + resp.Diagnostics.AddError("failed to create blueprint client", err.Error()) + return + } + + api, err := bp.GetResourceGenerator(ctx, apstra.ObjectId(state.Id.ValueString())) + if err != nil { + if utils.IsApstra404(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("Error retrieving Freeform Resource", err.Error()) + return + } + + state.LoadApiData(ctx, api.Data, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // Set state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (o *resourceFreeformResourceGenerator) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Get plan values + var plan blueprint.FreeformResourceGenerator + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // get a client for the Freeform 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 + } + + // Convert the plan into an API Request + request := plan.Request(ctx, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // 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()) + return + } + + // set state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (o *resourceFreeformResourceGenerator) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state blueprint.FreeformResourceGenerator + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + // get a client for the Freeform 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 Config Template by calling API + err = bp.DeleteResourceGenerator(ctx, apstra.ObjectId(state.Id.ValueString())) + if err != nil { + if utils.IsApstra404(err) { + return // 404 is okay + } + resp.Diagnostics.AddError("error deleting Freeform Resource", err.Error()) + return + } +} + +func (o *resourceFreeformResourceGenerator) setBpClientFunc(f func(context.Context, string) (*apstra.FreeformClient, error)) { + o.getBpClientFunc = f +} + +func (o *resourceFreeformResourceGenerator) setBpLockFunc(f func(context.Context, string) error) { + o.lockFunc = f +} From f3da5bd0fa8cef57d6399485b8d54d674fe90d4a Mon Sep 17 00:00:00 2001 From: bwjuniper Date: Thu, 8 Aug 2024 18:28:56 +0200 Subject: [PATCH 2/6] fix commit author --- .../blueprint/freeform_resource_generator.go | 193 ++++++++++++++ ...data_source_freeform_resource_generator.go | 101 ++++++++ apstra/provider.go | 2 + .../resource_freeform_resource_generator.go | 237 ++++++++++++++++++ 4 files changed, 533 insertions(+) create mode 100644 apstra/blueprint/freeform_resource_generator.go create mode 100644 apstra/data_source_freeform_resource_generator.go create mode 100644 apstra/resource_freeform_resource_generator.go diff --git a/apstra/blueprint/freeform_resource_generator.go b/apstra/blueprint/freeform_resource_generator.go new file mode 100644 index 00000000..81ab15d4 --- /dev/null +++ b/apstra/blueprint/freeform_resource_generator.go @@ -0,0 +1,193 @@ +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"` + ResourceType 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 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", + Computed: true, + }, + "name": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Populate this field to look up Resource by Name. Required when `id` is omitted.", + Optional: true, + Computed: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "scope": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Scope the Resource Generator uses for resource generation", + 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.", + Computed: true, + }, + "container_id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "ID of the group generator that created the group, if any.", + 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 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 the Resource Generator uses for resource generation.", + 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.", + 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: "Length of the subnet for the generated resources, if any.", + 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))), + }, + }, + } +} + +func (o *FreeformResourceGenerator) Request(_ context.Context, diags *diag.Diagnostics) *apstra.FreeformResourceGeneratorData { + var resourceType apstra.FFResourceType + err := utils.ApiStringerFromFriendlyString(&resourceType, o.ResourceType.ValueString()) + if err != nil { + diags.AddError(fmt.Sprintf("error parsing type %q", o.ResourceType.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.ResourceType = 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) +} diff --git a/apstra/data_source_freeform_resource_generator.go b/apstra/data_source_freeform_resource_generator.go new file mode 100644 index 00000000..5ecd42cb --- /dev/null +++ b/apstra/data_source_freeform_resource_generator.go @@ -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.\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 not found", + fmt.Sprintf("Freeform Resource 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)) + return + } + } + if err != nil { + resp.Diagnostics.AddError("failed reading Freeform Resource", err.Error()) + return + } + if api.Data == nil { + resp.Diagnostics.AddError("failed reading Freeform Resource", "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 +} diff --git a/apstra/provider.go b/apstra/provider.go index d5171b18..eaa05af9 100644 --- a/apstra/provider.go +++ b/apstra/provider.go @@ -555,6 +555,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{} }, @@ -617,6 +618,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{} }, diff --git a/apstra/resource_freeform_resource_generator.go b/apstra/resource_freeform_resource_generator.go new file mode 100644 index 00000000..fc5e0988 --- /dev/null +++ b/apstra/resource_freeform_resource_generator.go @@ -0,0 +1,237 @@ +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/path" + "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 = &resourceFreeformResourceGenerator{} + _ resource.ResourceWithValidateConfig = &resourceFreeformResourceGenerator{} + _ resourceWithSetFfBpClientFunc = &resourceFreeformResourceGenerator{} + _ resourceWithSetBpLockFunc = &resourceFreeformResourceGenerator{} +) + +type resourceFreeformResourceGenerator struct { + getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) + lockFunc func(context.Context, string) error +} + +func (o *resourceFreeformResourceGenerator) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_freeform_resource_generator" +} + +func (o *resourceFreeformResourceGenerator) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + configureResource(ctx, o, req, resp) +} + +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.", + Attributes: blueprint.FreeformResourceGenerator{}.ResourceAttributes(), + } +} + +func (o *resourceFreeformResourceGenerator) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + // Retrieve values from config + var config blueprint.FreeformResourceGenerator + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config.ResourceType.IsUnknown() || config.SubnetPrefixLen.IsUnknown() { + return + } + var resourceType apstra.FFResourceType + err := utils.ApiStringerFromFriendlyString(&resourceType, config.ResourceType.ValueString()) + if err != nil { + resp.Diagnostics.AddAttributeError(path.Root("type"), "failed to parse 'type' attribute", err.Error()) + return + } + 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()) + return + } +} + +func (o *resourceFreeformResourceGenerator) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan blueprint.FreeformResourceGenerator + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // get a client for the Freeform 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 + } + + // Convert the plan into an API Request + request := plan.Request(ctx, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // Create the resource + id, err := bp.CreateResourceGenerator(ctx, request) + if err != nil { + resp.Diagnostics.AddError("error creating new Resource", err.Error()) + return + } + + plan.Id = types.StringValue(id.String()) + + // set state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (o *resourceFreeformResourceGenerator) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state blueprint.FreeformResourceGenerator + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + // get a client for the Freeform reference design + 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()) + return + } + resp.Diagnostics.AddError("failed to create blueprint client", err.Error()) + return + } + + api, err := bp.GetResourceGenerator(ctx, apstra.ObjectId(state.Id.ValueString())) + if err != nil { + if utils.IsApstra404(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("Error retrieving Freeform Resource", err.Error()) + return + } + + state.LoadApiData(ctx, api.Data, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // Set state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (o *resourceFreeformResourceGenerator) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Get plan values + var plan blueprint.FreeformResourceGenerator + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // get a client for the Freeform 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 + } + + // Convert the plan into an API Request + request := plan.Request(ctx, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // 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()) + return + } + + // set state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (o *resourceFreeformResourceGenerator) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state blueprint.FreeformResourceGenerator + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + // get a client for the Freeform 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 Config Template by calling API + err = bp.DeleteResourceGenerator(ctx, apstra.ObjectId(state.Id.ValueString())) + if err != nil { + if utils.IsApstra404(err) { + return // 404 is okay + } + resp.Diagnostics.AddError("error deleting Freeform Resource", err.Error()) + return + } +} + +func (o *resourceFreeformResourceGenerator) setBpClientFunc(f func(context.Context, string) (*apstra.FreeformClient, error)) { + o.getBpClientFunc = f +} + +func (o *resourceFreeformResourceGenerator) setBpLockFunc(f func(context.Context, string) error) { + o.lockFunc = f +} From a037e42edb016e284e676f21fbee8048d83a1af6 Mon Sep 17 00:00:00 2001 From: bwjuniper Date: Fri, 9 Aug 2024 13:58:03 +0200 Subject: [PATCH 3/6] add integration_test and examples --- apstra/export_test.go | 1 + ...form_resource_generator_integraion_test.go | 384 ++++++++++++++++++ .../example.tf | 53 +++ .../example.tf | 53 +++ 4 files changed, 491 insertions(+) create mode 100644 apstra/resource_freeform_resource_generator_integraion_test.go create mode 100644 examples/data-sources/apstra_freeform_resource_generator/example.tf create mode 100644 examples/resources/apstra_freeform_resource_generator/example.tf diff --git a/apstra/export_test.go b/apstra/export_test.go index b9f0dc05..8129b9d9 100644 --- a/apstra/export_test.go +++ b/apstra/export_test.go @@ -18,6 +18,7 @@ var ( ResourceFreeformLink = resourceFreeformLink{} ResourceFreeformPropertySet = resourceFreeformPropertySet{} ResourceFreeformResourceGroup = resourceFreeformResourceGroup{} + ResourceFreeformResourceGenerator = resourceFreeformResourceGenerator{} ResourceFreeformResource = resourceFreeformResource{} ResourceFreeformAllocGroup = resourceFreeformAllocGroup{} ResourceFreeformSystem = resourceFreeformSystem{} diff --git a/apstra/resource_freeform_resource_generator_integraion_test.go b/apstra/resource_freeform_resource_generator_integraion_test.go new file mode 100644 index 00000000..9bc5d350 --- /dev/null +++ b/apstra/resource_freeform_resource_generator_integraion_test.go @@ -0,0 +1,384 @@ +//go:build integration + +package tfapstra_test + +import ( + "context" + "fmt" + "net" + "testing" + + "github.com/Juniper/apstra-go-sdk/apstra" + tfapstra "github.com/Juniper/terraform-provider-apstra/apstra" + testutils "github.com/Juniper/terraform-provider-apstra/apstra/test_utils" + "github.com/Juniper/terraform-provider-apstra/apstra/utils" + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/stretchr/testify/require" +) + +const ( + resourceFreeformResourceGeneratorHcl = ` +resource %q %q { + blueprint_id = %q + name = %q + type = %q + scope = %q + allocated_from = %s + container_id = %q + } +` +) + +type resourceFreeformResourceGenerator struct { + blueprintId string + name string + resourceType apstra.FFResourceType + scope string + allocatedFrom string + containerId string +} + +func (o resourceFreeformResourceGenerator) render(rType, rName string) string { + return fmt.Sprintf(resourceFreeformResourceGeneratorHcl, + rType, rName, + o.blueprintId, + o.name, + utils.StringersToFriendlyString(o.resourceType), + o.scope, + stringOrNull(o.allocatedFrom), + o.containerId, + ) +} + +func (o resourceFreeformResourceGenerator) testChecks(t testing.TB, rType, rName string) testChecks { + result := newTestChecks(rType + "." + rName) + + // required and computed attributes can always be checked + result.append(t, "TestCheckResourceAttrSet", "id") + result.append(t, "TestCheckResourceAttr", "blueprint_id", o.blueprintId) + result.append(t, "TestCheckResourceAttr", "name", o.name) + result.append(t, "TestCheckResourceAttr", "scope", o.scope) + result.append(t, "TestCheckResourceAttr", "container_id", o.containerId) + result.append(t, "TestCheckResourceAttr", "type", utils.StringersToFriendlyString(o.resourceType)) + if o.allocatedFrom == "" { + result.append(t, "TestCheckNoResourceAttr", "allocated_from") + } else { + result.append(t, "TestCheckResourceAttr", "allocated_from", o.allocatedFrom) + } + + return result +} + +func TestResourceFreeformResourceGenerator(t *testing.T) { + ctx := context.Background() + client := testutils.GetTestClient(t, ctx) + apiVersion := version.Must(version.NewVersion(client.ApiVersion())) + + // create a blueprint and a group... + bp, groupId := testutils.FfBlueprintC(t, ctx) + + newIpv4AllocationGroup := func(t testing.TB) apstra.ObjectId { + t.Helper() + + // create an ipv4 pool + randomNet := net.IPNet{ + IP: randIpvAddressMust(t, "10.0.0.0/8"), + Mask: net.CIDRMask(24, 32), + } + ipv4poolId, err := bp.Client().CreateIp4Pool(ctx, &apstra.NewIpPoolRequest{ + DisplayName: acctest.RandString(6), + Subnets: []apstra.NewIpSubnet{{Network: randomNet.String()}}, + }) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, bp.Client().DeleteIp4Pool(ctx, ipv4poolId)) }) + + // now create the allocation group + allocGroupCfg := apstra.FreeformAllocGroupData{ + Name: "ipv4AlGr-" + acctest.RandString(6), + Type: apstra.ResourcePoolTypeIpv4, + PoolIds: []apstra.ObjectId{ipv4poolId}, + } + allocGroup, err := bp.CreateAllocGroup(ctx, utils.ToPtr(allocGroupCfg)) + require.NoError(t, err) + return allocGroup + } + + newIpv6AllocationGroup := func(t testing.TB) apstra.ObjectId { + t.Helper() + + // create an ipv6 pool + randomNet := net.IPNet{ + IP: randIpvAddressMust(t, "2002::1234:abcd:ffff:c0a8:101/64"), + Mask: net.CIDRMask(64, 128), + } + ipv6poolId, err := bp.Client().CreateIp6Pool(ctx, &apstra.NewIpPoolRequest{ + DisplayName: acctest.RandString(6), + Subnets: []apstra.NewIpSubnet{{Network: randomNet.String()}}, + }) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, bp.Client().DeleteIp6Pool(ctx, ipv6poolId)) }) + + // now create the allocation group + allocGroupCfg := apstra.FreeformAllocGroupData{ + Name: "ipv6AlGr-" + acctest.RandString(6), + Type: apstra.ResourcePoolTypeIpv6, + PoolIds: []apstra.ObjectId{ipv6poolId}, + } + allocGroup, err := bp.CreateAllocGroup(ctx, utils.ToPtr(allocGroupCfg)) + require.NoError(t, err) + return allocGroup + } + + newVniAllocationGroup := func(t testing.TB) apstra.ObjectId { + t.Helper() + + vniRange := []apstra.IntfIntRange{ + apstra.IntRangeRequest{First: 5000, Last: 6000}, + } + + vniPoolId, err := bp.Client().CreateVniPool(ctx, &apstra.VniPoolRequest{ + DisplayName: acctest.RandString(6), + Ranges: vniRange, + }) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, bp.Client().DeleteVniPool(ctx, vniPoolId)) }) + + // now create the allocation group + allocGroupCfg := apstra.FreeformAllocGroupData{ + Name: "vniAlGr-" + acctest.RandString(6), + Type: apstra.ResourcePoolTypeVni, + PoolIds: []apstra.ObjectId{vniPoolId}, + } + allocGroup, err := bp.CreateAllocGroup(ctx, utils.ToPtr(allocGroupCfg)) + require.NoError(t, err) + return allocGroup + } + + newAsnAllocationGroup := func(t testing.TB) apstra.ObjectId { + t.Helper() + + asnRange := []apstra.IntfIntRange{ + apstra.IntRangeRequest{First: 65535, Last: 65700}, + } + + asnPoolId, err := bp.Client().CreateAsnPool(ctx, &apstra.AsnPoolRequest{ + DisplayName: acctest.RandString(6), + Ranges: asnRange, + }) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, bp.Client().DeleteAsnPool(ctx, asnPoolId)) }) + + // now create the allocation group + allocGroupCfg := apstra.FreeformAllocGroupData{ + Name: "asnAlGr-" + acctest.RandString(6), + Type: apstra.ResourcePoolTypeAsn, + PoolIds: []apstra.ObjectId{asnPoolId}, + } + allocGroup, err := bp.CreateAllocGroup(ctx, utils.ToPtr(allocGroupCfg)) + require.NoError(t, err) + return allocGroup + } + newIntAllocationGroup := func(t testing.TB) apstra.ObjectId { + t.Helper() + + intRange := []apstra.IntfIntRange{ + apstra.IntRangeRequest{First: 10, Last: 65700}, + } + + intPoolId, err := bp.Client().CreateIntegerPool(ctx, &apstra.IntPoolRequest{ + DisplayName: acctest.RandString(6), + Ranges: intRange, + }) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, bp.Client().DeleteIntegerPool(ctx, intPoolId)) }) + + // now create the allocation group + allocGroupCfg := apstra.FreeformAllocGroupData{ + Name: "intAlGr-" + acctest.RandString(6), + Type: apstra.ResourcePoolTypeInt, + PoolIds: []apstra.ObjectId{intPoolId}, + } + allocGroup, err := bp.CreateAllocGroup(ctx, utils.ToPtr(allocGroupCfg)) + require.NoError(t, err) + return allocGroup + } + + type testStep struct { + config resourceFreeformResourceGenerator + } + type testCase struct { + apiVersionConstraints version.Constraints + steps []testStep + } + + testCases := map[string]testCase{ + "start_asn_resource_generator": { + steps: []testStep{ + { + config: resourceFreeformResourceGenerator{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + resourceType: apstra.FFResourceTypeAsn, + scope: "node('system', name='target')", + allocatedFrom: string(newAsnAllocationGroup(t)), + containerId: string(groupId), + }, + }, + { + config: resourceFreeformResourceGenerator{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + resourceType: apstra.FFResourceTypeAsn, + scope: "node('system', deploy_mode='deploy', name='target')", + allocatedFrom: string(newAsnAllocationGroup(t)), + containerId: string(groupId), + }, + }, + }, + }, + //"start_vni_with_static_value": { + // steps: []testStep{ + // { + // config: resourceFreeformResourceGenerator{ + // blueprintId: bp.Id().String(), + // name: acctest.RandString(6), + // containerId: string(groupId), + // resourceType: apstra.FFResourceTypeVni, + // }, + // }, + // }, + //}, + "start_int_resource_generator": { + steps: []testStep{ + { + config: resourceFreeformResourceGenerator{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + scope: "node('system', name='target')", + containerId: string(groupId), + resourceType: apstra.FFResourceTypeInt, + allocatedFrom: string(newIntAllocationGroup(t)), + }, + }, + { + config: resourceFreeformResourceGenerator{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + scope: "node('system', deploy_mode='deploy', name='target')", + containerId: string(groupId), + resourceType: apstra.FFResourceTypeInt, + allocatedFrom: string(newIntAllocationGroup(t)), + }, + }, + }, + }, + "start_ipv4_resource_generator": { + steps: []testStep{ + { + config: resourceFreeformResourceGenerator{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + scope: "node('system', name='target')", + containerId: string(groupId), + resourceType: apstra.FFResourceTypeIpv4, + allocatedFrom: string(newIpv4AllocationGroup(t)), + }, + }, + { + config: resourceFreeformResourceGenerator{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + scope: "node('system', deploy_mode='deploy', name='target')", + containerId: string(groupId), + resourceType: apstra.FFResourceTypeIpv4, + allocatedFrom: string(newIpv4AllocationGroup(t)), + }, + }, + }, + }, + "start_ipv6_with_static_value": { + steps: []testStep{ + { + config: resourceFreeformResourceGenerator{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + scope: "node('system', name='target')", + containerId: string(groupId), + resourceType: apstra.FFResourceTypeIpv6, + allocatedFrom: string(newIpv6AllocationGroup(t)), + }, + }, + { + config: resourceFreeformResourceGenerator{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + scope: "node('system', deploy_mode='deploy', name='target')", + containerId: string(groupId), + resourceType: apstra.FFResourceTypeIpv6, + allocatedFrom: string(newIpv6AllocationGroup(t)), + }, + }, + }, + }, + "start_Vni_with_alloc_group": { + steps: []testStep{ + { + config: resourceFreeformResourceGenerator{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + scope: "node('system', name='target')", + containerId: string(groupId), + resourceType: apstra.FFResourceTypeVni, + allocatedFrom: string(newVniAllocationGroup(t)), + }, + }, + { + config: resourceFreeformResourceGenerator{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + scope: "node('system', deploy_mode='deploy', name='target')", + containerId: string(groupId), + resourceType: apstra.FFResourceTypeVni, + allocatedFrom: string(newVniAllocationGroup(t)), + }, + }, + }, + }, + } + + resourceType := tfapstra.ResourceName(ctx, &tfapstra.ResourceFreeformResourceGenerator) + + for tName, tCase := range testCases { + tName, tCase := tName, tCase + t.Run(tName, func(t *testing.T) { + t.Parallel() + if !tCase.apiVersionConstraints.Check(apiVersion) { + t.Skipf("test case %s requires Apstra %s", tName, tCase.apiVersionConstraints.String()) + } + + steps := make([]resource.TestStep, len(tCase.steps)) + for i, step := range tCase.steps { + config := step.config.render(resourceType, tName) + checks := step.config.testChecks(t, resourceType, tName) + + chkLog := checks.string() + stepName := fmt.Sprintf("test case %q step %d", tName, i+1) + + t.Logf("\n// ------ begin config for %s ------%s// -------- end config for %s ------\n\n", stepName, config, stepName) + t.Logf("\n// ------ begin checks for %s ------\n%s// -------- end checks for %s ------\n\n", stepName, chkLog, stepName) + + steps[i] = resource.TestStep{ + Config: insecureProviderConfigHCL + config, + Check: resource.ComposeAggregateTestCheckFunc(checks.checks...), + } + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: steps, + }) + }) + } +} diff --git a/examples/data-sources/apstra_freeform_resource_generator/example.tf b/examples/data-sources/apstra_freeform_resource_generator/example.tf new file mode 100644 index 00000000..a6de71d9 --- /dev/null +++ b/examples/data-sources/apstra_freeform_resource_generator/example.tf @@ -0,0 +1,53 @@ +# This example creates an ASN resource Generator within a +# preexisting resource group in a Freeform Blueprint. +# +# 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 + }, + ] +} + +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] +} + +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 +} + + + +# 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" +#} diff --git a/examples/resources/apstra_freeform_resource_generator/example.tf b/examples/resources/apstra_freeform_resource_generator/example.tf new file mode 100644 index 00000000..a6de71d9 --- /dev/null +++ b/examples/resources/apstra_freeform_resource_generator/example.tf @@ -0,0 +1,53 @@ +# This example creates an ASN resource Generator within a +# preexisting resource group in a Freeform Blueprint. +# +# 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 + }, + ] +} + +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] +} + +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 +} + + + +# 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" +#} From 67f37058d5954d72cfcf92ae2bac12a867416978 Mon Sep 17 00:00:00 2001 From: bwjuniper Date: Fri, 9 Aug 2024 15:24:08 +0200 Subject: [PATCH 4/6] update sdk version --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 48b27712..87270cc3 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ go 1.22.5 require ( github.com/IBM/netaddr v1.5.0 - github.com/Juniper/apstra-go-sdk v0.0.0-20240801202534-f251446c2750 + github.com/Juniper/apstra-go-sdk v0.0.0-20240807201924-9a8d4a6b3ebf github.com/chrismarget-j/go-licenses v0.0.0-20240224210557-f22f3e06d3d4 github.com/google/go-cmp v0.6.0 github.com/hashicorp/go-version v1.7.0 diff --git a/go.sum b/go.sum index bfd180ae..f3d23e2e 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0 github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/IBM/netaddr v1.5.0 h1:IJlFZe1+nFs09TeMB/HOP4+xBnX2iM/xgiDOgZgTJq0= github.com/IBM/netaddr v1.5.0/go.mod h1:DDBPeYgbFzoXHjSz9Jwk7K8wmWV4+a/Kv0LqRnb8we4= -github.com/Juniper/apstra-go-sdk v0.0.0-20240801202534-f251446c2750 h1:0p7UP5v6422lyiVX1/nCX+TaawoK6JZuss8RZMfJvCs= -github.com/Juniper/apstra-go-sdk v0.0.0-20240801202534-f251446c2750/go.mod h1:Xwj3X8v/jRZWv28o6vQAqD4lz2JmzaSYLZ2ch1SS89w= +github.com/Juniper/apstra-go-sdk v0.0.0-20240807201924-9a8d4a6b3ebf h1:G5MohgVCO8fzW/Z40HTd8xQOyt6mcHXPpIsO/xqQVUA= +github.com/Juniper/apstra-go-sdk v0.0.0-20240807201924-9a8d4a6b3ebf/go.mod h1:cSUzaIIQzZysIVKgJnt2/jO2EKeAB60Xgbx8yBGwJ8Y= github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0= github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= From 84a252f491b421864006629d580a8808a9842df1 Mon Sep 17 00:00:00 2001 From: bwjuniper Date: Fri, 9 Aug 2024 17:34:21 +0200 Subject: [PATCH 5/6] update attribute descriptions and make docs --- .../blueprint/freeform_resource_generator.go | 10 +- .../freeform_resource_generator.md | 92 ++++++++++++++++ docs/resources/freeform_resource_generator.md | 100 ++++++++++++++++++ 3 files changed, 197 insertions(+), 5 deletions(-) create mode 100644 docs/data-sources/freeform_resource_generator.md create mode 100644 docs/resources/freeform_resource_generator.md diff --git a/apstra/blueprint/freeform_resource_generator.go b/apstra/blueprint/freeform_resource_generator.go index 81ab15d4..b352e258 100644 --- a/apstra/blueprint/freeform_resource_generator.go +++ b/apstra/blueprint/freeform_resource_generator.go @@ -41,7 +41,7 @@ func (o FreeformResourceGenerator) DataSourceAttributes() map[string]dataSourceS Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, "id": dataSourceSchema.StringAttribute{ - MarkdownDescription: "Populate this field to look up the Freeform Resource by ID. Required when `name` is omitted.", + 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{ @@ -53,11 +53,11 @@ func (o FreeformResourceGenerator) DataSourceAttributes() map[string]dataSourceS }, }, "type": dataSourceSchema.StringAttribute{ - MarkdownDescription: "Type of the Resource", + MarkdownDescription: "Type of the Resource Generator", Computed: true, }, "name": dataSourceSchema.StringAttribute{ - MarkdownDescription: "Populate this field to look up Resource by Name. Required when `id` is omitted.", + 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)}, @@ -73,7 +73,7 @@ func (o FreeformResourceGenerator) DataSourceAttributes() map[string]dataSourceS Computed: true, }, "container_id": dataSourceSchema.StringAttribute{ - MarkdownDescription: "ID of the group generator that created the group, if any.", + MarkdownDescription: "ID of the group used to organize the generated resources", Computed: true, }, "subnet_prefix_len": dataSourceSchema.Int64Attribute{ @@ -104,7 +104,7 @@ func (o FreeformResourceGenerator) ResourceAttributes() map[string]resourceSchem PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, }, "name": resourceSchema.StringAttribute{ - MarkdownDescription: "Freeform Resource name as shown in the Web UI.", + 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.-_"), diff --git a/docs/data-sources/freeform_resource_generator.md b/docs/data-sources/freeform_resource_generator.md new file mode 100644 index 00000000..d627d2e8 --- /dev/null +++ b/docs/data-sources/freeform_resource_generator.md @@ -0,0 +1,92 @@ +--- +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. + At least one optional attribute is required. +--- + +# apstra_freeform_resource_generator (Data Source) + +This data source provides details of a specific Freeform Resource. + +At least one optional attribute is required. + + +## Example Usage + +```terraform +# This example creates an ASN resource Generator within a +# preexisting resource group in a Freeform Blueprint. +# +# 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 + }, + ] +} + +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] +} + +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 +} + + + +# 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" +#} +``` + + +## Schema + +### Required + +- `blueprint_id` (String) Apstra Blueprint ID. Used to identify the Blueprint where the Resource lives. + +### Optional + +- `id` (String) Populate this field to look up the Freeform Resource Generator by ID. Required when `name` is omitted. +- `name` (String) Populate this field to look up Resource Generator by Name. Required when `id` is omitted. + +### 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. +- `container_id` (String) ID of the group used to organize the generated resources +- `scope` (String) Scope the Resource Generator uses for resource generation +- `subnet_prefix_len` (Number) Length of the subnet for the generated resources, if any. +- `type` (String) Type of the Resource Generator diff --git a/docs/resources/freeform_resource_generator.md b/docs/resources/freeform_resource_generator.md new file mode 100644 index 00000000..8aa783c5 --- /dev/null +++ b/docs/resources/freeform_resource_generator.md @@ -0,0 +1,100 @@ +--- +page_title: "apstra_freeform_resource_generator Resource - terraform-provider-apstra" +subcategory: "Reference Design: Freeform" +description: |- + This resource creates a Resource in a Freeform Blueprint. +--- + +# apstra_freeform_resource_generator (Resource) + +This resource creates a Resource in a Freeform Blueprint. + + +## Example Usage + +```terraform +# This example creates an ASN resource Generator within a +# preexisting resource group in a Freeform Blueprint. +# +# 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 + }, + ] +} + +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] +} + +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 +} + + + +# 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" +#} +``` + + +## Schema + +### Required + +- `allocated_from` (String) 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. +- `blueprint_id` (String) Apstra Blueprint ID. +- `container_id` (String) ID of the group where resources are generated. +- `name` (String) Freeform Resource Generator name as shown in the Web UI. +- `scope` (String) Scope the Resource Generator uses for resource generation. +- `type` (String) type of the Resource Generator, must be one of : + - `asn` + - `host_ipv4` + - `host_ipv6` + - `integer` + - `ipv4` + - `ipv6` + - `vlan` + - `vni` + +### Optional + +- `subnet_prefix_len` (Number) Length of the subnet for the generated resources, if any. + +### Read-Only + +- `id` (String) ID of the Resource Generator within the Freeform Blueprint. + + + From 72c382f0635d77d130ddbf6f398cb07ebb1bf3ae Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Fri, 9 Aug 2024 13:20:25 -0400 Subject: [PATCH 6/6] 763 review --- .../blueprint/freeform_resource_generator.go | 64 +++++++------ ...data_source_freeform_resource_generator.go | 14 +-- .../resource_freeform_resource_generator.go | 22 +++-- .../freeform_resource_generator.md | 85 +++++++++--------- docs/resources/freeform_resource_generator.md | 89 ++++++++++--------- .../example.tf | 76 ++++++++-------- .../example.tf | 76 ++++++++-------- 7 files changed, 221 insertions(+), 205 deletions(-) diff --git a/apstra/blueprint/freeform_resource_generator.go b/apstra/blueprint/freeform_resource_generator.go index b352e258..6bfd3a30 100644 --- a/apstra/blueprint/freeform_resource_generator.go +++ b/apstra/blueprint/freeform_resource_generator.go @@ -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"` @@ -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{ @@ -107,40 +108,48 @@ 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))), }, }, } @@ -148,14 +157,13 @@ func (o FreeformResourceGenerator) ResourceAttributes() map[string]resourceSchem 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 { @@ -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 { diff --git a/apstra/data_source_freeform_resource_generator.go b/apstra/data_source_freeform_resource_generator.go index 5ecd42cb..9932a11f 100644 --- a/apstra/data_source_freeform_resource_generator.go +++ b/apstra/data_source_freeform_resource_generator.go @@ -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(), } @@ -63,8 +63,8 @@ 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(): @@ -72,17 +72,17 @@ func (o *dataSourceFreeformResourceGenerator) Read(ctx context.Context, req data 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 } diff --git a/apstra/resource_freeform_resource_generator.go b/apstra/resource_freeform_resource_generator.go index fc5e0988..54764c7b 100644 --- a/apstra/resource_freeform_resource_generator.go +++ b/apstra/resource_freeform_resource_generator.go @@ -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(), } } @@ -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 } } @@ -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 } @@ -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()) @@ -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 } @@ -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 } diff --git a/docs/data-sources/freeform_resource_generator.md b/docs/data-sources/freeform_resource_generator.md index d627d2e8..8aa332f2 100644 --- a/docs/data-sources/freeform_resource_generator.md +++ b/docs/data-sources/freeform_resource_generator.md @@ -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. @@ -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" +# } ``` @@ -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 diff --git a/docs/resources/freeform_resource_generator.md b/docs/resources/freeform_resource_generator.md index 8aa783c5..b0f4700f 100644 --- a/docs/resources/freeform_resource_generator.md +++ b/docs/resources/freeform_resource_generator.md @@ -2,12 +2,12 @@ page_title: "apstra_freeform_resource_generator Resource - terraform-provider-apstra" subcategory: "Reference Design: Freeform" description: |- - This resource creates a Resource in a Freeform Blueprint. + This resource creates a Resource Generator in a Freeform Blueprint. --- # apstra_freeform_resource_generator (Resource) -This resource creates a Resource in a Freeform Blueprint. +This resource creates a Resource Generator in a Freeform Blueprint. ## Example Usage @@ -18,54 +18,54 @@ This resource creates a Resource in a Freeform Blueprint. # # 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" +# } ``` @@ -73,11 +73,12 @@ resource "apstra_freeform_resource_generator" "test_res_gen" { ### Required -- `allocated_from` (String) 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. +- `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. - `blueprint_id` (String) Apstra Blueprint ID. -- `container_id` (String) ID of the group where resources are generated. +- `container_id` (String) ID of the group where Resources are generated. - `name` (String) Freeform Resource Generator name as shown in the Web UI. -- `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*'))` - `type` (String) type of the Resource Generator, must be one of : - `asn` - `host_ipv4` @@ -90,7 +91,7 @@ resource "apstra_freeform_resource_generator" "test_res_gen" { ### Optional -- `subnet_prefix_len` (Number) Length of the subnet for the generated resources, if any. +- `subnet_prefix_len` (Number) Length of the subnet for the generated Resources. Only applicable when `type` is `ipv4` or `ipv6` ### Read-Only diff --git a/examples/data-sources/apstra_freeform_resource_generator/example.tf b/examples/data-sources/apstra_freeform_resource_generator/example.tf index a6de71d9..3f14f5f5 100644 --- a/examples/data-sources/apstra_freeform_resource_generator/example.tf +++ b/examples/data-sources/apstra_freeform_resource_generator/example.tf @@ -3,51 +3,51 @@ # # 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" +# } diff --git a/examples/resources/apstra_freeform_resource_generator/example.tf b/examples/resources/apstra_freeform_resource_generator/example.tf index a6de71d9..3f14f5f5 100644 --- a/examples/resources/apstra_freeform_resource_generator/example.tf +++ b/examples/resources/apstra_freeform_resource_generator/example.tf @@ -3,51 +3,51 @@ # # 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" +# }