From 36ddf9c7c30d3003ffd9b79d7ec63ce35015fbea Mon Sep 17 00:00:00 2001 From: bwJuniper <98770420+bwJuniper@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:20:53 +0200 Subject: [PATCH 1/7] initial framework --- apstra/blueprint/freeform_alloc_group.go | 110 +++++++++++ apstra/data_source_freeform_alloc_group.go | 101 ++++++++++ apstra/resource_freeform_alloc_group.go | 210 +++++++++++++++++++++ go.mod | 4 +- go.sum | 4 +- 5 files changed, 425 insertions(+), 4 deletions(-) create mode 100644 apstra/blueprint/freeform_alloc_group.go create mode 100644 apstra/data_source_freeform_alloc_group.go create mode 100644 apstra/resource_freeform_alloc_group.go diff --git a/apstra/blueprint/freeform_alloc_group.go b/apstra/blueprint/freeform_alloc_group.go new file mode 100644 index 00000000..f4a9af2b --- /dev/null +++ b/apstra/blueprint/freeform_alloc_group.go @@ -0,0 +1,110 @@ +package blueprint + +import ( + "context" + "fmt" + "regexp" + + "github.com/Juniper/apstra-go-sdk/apstra" + "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 FreeformAllocGroup struct { + BlueprintId types.String `tfsdk:"blueprint_id"` + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Type types.String `tfsdk:"type"` + PoolId types.String `tfsdk:"pool_id"` +} + +func (o FreeformAllocGroup) 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 System lives.", + Required: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Populate this field to look up the Freeform System 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"), + }...), + }, + }, + "name": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Populate this field to look up System by Name. Required when `id` is omitted.", + Optional: true, + Computed: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "type": dataSourceSchema.StringAttribute{ + MarkdownDescription: "type of the Resource Pool, either Internal or External", + Computed: true, + }, + "pool_id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Pool ID assigned to the allocation group", + Computed: true, + }, + } +} + +func (o FreeformAllocGroup) 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 Freeform System.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "name": resourceSchema.StringAttribute{ + MarkdownDescription: "Freeform System 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.-_")}, + }, + "type": resourceSchema.StringAttribute{ + MarkdownDescription: fmt.Sprintf("Type of the System. Must be one of `%s` or `%s`", apstra.SystemTypeInternal, apstra.SystemTypeExternal), + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + Validators: []validator.String{stringvalidator.OneOf(apstra.SystemTypeInternal.String(), apstra.SystemTypeExternal.String())}, + }, + "pool_id": resourceSchema.StringAttribute{ + MarkdownDescription: "ID (usually serial number) of the Managed Device to associate with this System", + Optional: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + } +} + +func (o *FreeformAllocGroup) Request(ctx context.Context, diags *diag.Diagnostics) *apstra.FreeformAllocGroupData { + + return &apstra.FreeformAllocGroupData{ + Name: "", + Type: apstra.ResourcePoolType{}, + PoolIds: nil, + } +} + +func (o *FreeformAllocGroup) LoadApiData(ctx context.Context, in *apstra.FreeformAllocGroupData, diags *diag.Diagnostics) { + o.Name = types.StringValue(in.Name) + o.Type = types.StringValue(in.Type.String()) +} diff --git a/apstra/data_source_freeform_alloc_group.go b/apstra/data_source_freeform_alloc_group.go new file mode 100644 index 00000000..e41254b5 --- /dev/null +++ b/apstra/data_source_freeform_alloc_group.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 = &dataSourceFreeformAllocGroup{} + _ datasourceWithSetFfBpClientFunc = &dataSourceFreeformAllocGroup{} +) + +type dataSourceFreeformAllocGroup struct { + getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) +} + +func (o *dataSourceFreeformAllocGroup) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_freeform_system" +} + +func (o *dataSourceFreeformAllocGroup) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + configureDataSource(ctx, o, req, resp) +} + +func (o *dataSourceFreeformAllocGroup) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: docCategoryFreeform + "This data source provides details of a specific Freeform System.\n\n" + + "At least one optional attribute is required.", + Attributes: blueprint.FreeformAllocGroup{}.DataSourceAttributes(), + } +} + +func (o *dataSourceFreeformAllocGroup) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config blueprint.FreeformAllocGroup + 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.FreeformAllocGroup + switch { + case !config.Id.IsNull(): + api, err = bp.GetAllocGroup(ctx, apstra.ObjectId(config.Id.ValueString())) + if utils.IsApstra404(err) { + resp.Diagnostics.AddAttributeError( + path.Root("id"), + "Freeform Allocation Group not found", + fmt.Sprintf("Freeform Allocation Group with ID %s not found", config.Id)) + return + } + case !config.Name.IsNull(): + api, err = bp.GetAllocGroupByName(ctx, config.Name.ValueString()) + if utils.IsApstra404(err) { + resp.Diagnostics.AddAttributeError( + path.Root("name"), + "Freeform Allocation Group not found", + fmt.Sprintf("Freeform Allocation Group with Name %s not found", config.Name)) + return + } + } + if err != nil { + resp.Diagnostics.AddError("failed reading Freeform Allocation Group", err.Error()) + return + } + if api.Data == nil { + resp.Diagnostics.AddError("failed reading Freeform Allocation Group", "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 *dataSourceFreeformAllocGroup) setBpClientFunc(f func(context.Context, string) (*apstra.FreeformClient, error)) { + o.getBpClientFunc = f +} diff --git a/apstra/resource_freeform_alloc_group.go b/apstra/resource_freeform_alloc_group.go new file mode 100644 index 00000000..723f4303 --- /dev/null +++ b/apstra/resource_freeform_alloc_group.go @@ -0,0 +1,210 @@ +package tfapstra + +import ( + "context" + "fmt" + "github.com/Juniper/apstra-go-sdk/apstra" + "github.com/Juniper/terraform-provider-apstra/apstra/blueprint" + "github.com/Juniper/terraform-provider-apstra/apstra/utils" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.ResourceWithConfigure = &resourceFreeformAllocGroup{} + _ resourceWithSetFfBpClientFunc = &resourceFreeformAllocGroup{} + _ resourceWithSetBpLockFunc = &resourceFreeformAllocGroup{} +) + +type resourceFreeformAllocGroup struct { + getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) + lockFunc func(context.Context, string) error +} + +func (o *resourceFreeformAllocGroup) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_freeform_system" +} + +func (o *resourceFreeformAllocGroup) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + configureResource(ctx, o, req, resp) +} + +func (o *resourceFreeformAllocGroup) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: docCategoryFreeform + "This resource creates a System in a Freeform Blueprint.", + Attributes: blueprint.FreeformAllocGroup{}.ResourceAttributes(), + } +} + +func (o *resourceFreeformAllocGroup) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan blueprint.FreeformAllocGroup + 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 + } + + id, err := bp.CreateSystem(ctx, request) + if err != nil { + resp.Diagnostics.AddError("error creating new System", err.Error()) + return + } + + plan.Id = types.StringValue(id.String()) + + // set state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (o *resourceFreeformAllocGroup) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state blueprint.FreeformAllocGroup + 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.GetSystem(ctx, apstra.ObjectId(state.Id.ValueString())) + if err != nil { + if utils.IsApstra404(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("Error retrieving Freeform System", 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 *resourceFreeformAllocGroup) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Get plan values + var plan blueprint.FreeformAllocGroup + 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 + } + + request := plan.Request(ctx, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // Update Config Template + err = bp.UpdateSystem(ctx, apstra.ObjectId(plan.Id.ValueString()), request) + if err != nil { + resp.Diagnostics.AddError("error updating Freeform System", err.Error()) + return + } + + // set state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (o *resourceFreeformAllocGroup) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state blueprint.FreeformAllocGroup + 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.DeleteSystem(ctx, apstra.ObjectId(state.Id.ValueString())) + if err != nil { + if utils.IsApstra404(err) { + return // 404 is okay + } + resp.Diagnostics.AddError("error deleting Freeform System", err.Error()) + return + } +} + +func (o *resourceFreeformAllocGroup) setBpClientFunc(f func(context.Context, string) (*apstra.FreeformClient, error)) { + o.getBpClientFunc = f +} + +func (o *resourceFreeformAllocGroup) setBpLockFunc(f func(context.Context, string) error) { + o.lockFunc = f +} diff --git a/go.mod b/go.mod index e3fe9087..35942cf8 100644 --- a/go.mod +++ b/go.mod @@ -2,11 +2,11 @@ module github.com/Juniper/terraform-provider-apstra go 1.22.5 -//replace github.com/Juniper/apstra-go-sdk => ../apstra-go-sdk +replace github.com/Juniper/apstra-go-sdk => ../apstra-go-sdk require ( github.com/IBM/netaddr v1.5.0 - github.com/Juniper/apstra-go-sdk v0.0.0-20240722155513-01f6e5e4e91a + github.com/Juniper/apstra-go-sdk v0.0.0-20240730135749-72db5614fa0e 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 350654f6..ca0e5a76 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-20240722155513-01f6e5e4e91a h1:x4OFdv7+++KbErpr7QSdR0tnTQbzkXSwt+hlX0Ug/bY= -github.com/Juniper/apstra-go-sdk v0.0.0-20240722155513-01f6e5e4e91a/go.mod h1:Xwj3X8v/jRZWv28o6vQAqD4lz2JmzaSYLZ2ch1SS89w= +github.com/Juniper/apstra-go-sdk v0.0.0-20240730135749-72db5614fa0e h1:1jTeweRZ1kFLJvUgM417Kc2DUeyzsvsVbkbs4yWdNaM= +github.com/Juniper/apstra-go-sdk v0.0.0-20240730135749-72db5614fa0e/go.mod h1:Xwj3X8v/jRZWv28o6vQAqD4lz2JmzaSYLZ2ch1SS89w= 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 66417caa2103b0a2513ca8d2033241fcb29006a5 Mon Sep 17 00:00:00 2001 From: bwJuniper <98770420+bwJuniper@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:20:53 +0200 Subject: [PATCH 2/7] initial framework --- apstra/blueprint/freeform_alloc_group.go | 110 +++++++++++ apstra/data_source_freeform_alloc_group.go | 101 ++++++++++ apstra/resource_freeform_alloc_group.go | 210 +++++++++++++++++++++ go.mod | 2 +- 4 files changed, 422 insertions(+), 1 deletion(-) create mode 100644 apstra/blueprint/freeform_alloc_group.go create mode 100644 apstra/data_source_freeform_alloc_group.go create mode 100644 apstra/resource_freeform_alloc_group.go diff --git a/apstra/blueprint/freeform_alloc_group.go b/apstra/blueprint/freeform_alloc_group.go new file mode 100644 index 00000000..f4a9af2b --- /dev/null +++ b/apstra/blueprint/freeform_alloc_group.go @@ -0,0 +1,110 @@ +package blueprint + +import ( + "context" + "fmt" + "regexp" + + "github.com/Juniper/apstra-go-sdk/apstra" + "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 FreeformAllocGroup struct { + BlueprintId types.String `tfsdk:"blueprint_id"` + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Type types.String `tfsdk:"type"` + PoolId types.String `tfsdk:"pool_id"` +} + +func (o FreeformAllocGroup) 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 System lives.", + Required: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Populate this field to look up the Freeform System 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"), + }...), + }, + }, + "name": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Populate this field to look up System by Name. Required when `id` is omitted.", + Optional: true, + Computed: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "type": dataSourceSchema.StringAttribute{ + MarkdownDescription: "type of the Resource Pool, either Internal or External", + Computed: true, + }, + "pool_id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Pool ID assigned to the allocation group", + Computed: true, + }, + } +} + +func (o FreeformAllocGroup) 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 Freeform System.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "name": resourceSchema.StringAttribute{ + MarkdownDescription: "Freeform System 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.-_")}, + }, + "type": resourceSchema.StringAttribute{ + MarkdownDescription: fmt.Sprintf("Type of the System. Must be one of `%s` or `%s`", apstra.SystemTypeInternal, apstra.SystemTypeExternal), + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + Validators: []validator.String{stringvalidator.OneOf(apstra.SystemTypeInternal.String(), apstra.SystemTypeExternal.String())}, + }, + "pool_id": resourceSchema.StringAttribute{ + MarkdownDescription: "ID (usually serial number) of the Managed Device to associate with this System", + Optional: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + } +} + +func (o *FreeformAllocGroup) Request(ctx context.Context, diags *diag.Diagnostics) *apstra.FreeformAllocGroupData { + + return &apstra.FreeformAllocGroupData{ + Name: "", + Type: apstra.ResourcePoolType{}, + PoolIds: nil, + } +} + +func (o *FreeformAllocGroup) LoadApiData(ctx context.Context, in *apstra.FreeformAllocGroupData, diags *diag.Diagnostics) { + o.Name = types.StringValue(in.Name) + o.Type = types.StringValue(in.Type.String()) +} diff --git a/apstra/data_source_freeform_alloc_group.go b/apstra/data_source_freeform_alloc_group.go new file mode 100644 index 00000000..e41254b5 --- /dev/null +++ b/apstra/data_source_freeform_alloc_group.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 = &dataSourceFreeformAllocGroup{} + _ datasourceWithSetFfBpClientFunc = &dataSourceFreeformAllocGroup{} +) + +type dataSourceFreeformAllocGroup struct { + getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) +} + +func (o *dataSourceFreeformAllocGroup) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_freeform_system" +} + +func (o *dataSourceFreeformAllocGroup) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + configureDataSource(ctx, o, req, resp) +} + +func (o *dataSourceFreeformAllocGroup) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: docCategoryFreeform + "This data source provides details of a specific Freeform System.\n\n" + + "At least one optional attribute is required.", + Attributes: blueprint.FreeformAllocGroup{}.DataSourceAttributes(), + } +} + +func (o *dataSourceFreeformAllocGroup) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config blueprint.FreeformAllocGroup + 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.FreeformAllocGroup + switch { + case !config.Id.IsNull(): + api, err = bp.GetAllocGroup(ctx, apstra.ObjectId(config.Id.ValueString())) + if utils.IsApstra404(err) { + resp.Diagnostics.AddAttributeError( + path.Root("id"), + "Freeform Allocation Group not found", + fmt.Sprintf("Freeform Allocation Group with ID %s not found", config.Id)) + return + } + case !config.Name.IsNull(): + api, err = bp.GetAllocGroupByName(ctx, config.Name.ValueString()) + if utils.IsApstra404(err) { + resp.Diagnostics.AddAttributeError( + path.Root("name"), + "Freeform Allocation Group not found", + fmt.Sprintf("Freeform Allocation Group with Name %s not found", config.Name)) + return + } + } + if err != nil { + resp.Diagnostics.AddError("failed reading Freeform Allocation Group", err.Error()) + return + } + if api.Data == nil { + resp.Diagnostics.AddError("failed reading Freeform Allocation Group", "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 *dataSourceFreeformAllocGroup) setBpClientFunc(f func(context.Context, string) (*apstra.FreeformClient, error)) { + o.getBpClientFunc = f +} diff --git a/apstra/resource_freeform_alloc_group.go b/apstra/resource_freeform_alloc_group.go new file mode 100644 index 00000000..723f4303 --- /dev/null +++ b/apstra/resource_freeform_alloc_group.go @@ -0,0 +1,210 @@ +package tfapstra + +import ( + "context" + "fmt" + "github.com/Juniper/apstra-go-sdk/apstra" + "github.com/Juniper/terraform-provider-apstra/apstra/blueprint" + "github.com/Juniper/terraform-provider-apstra/apstra/utils" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.ResourceWithConfigure = &resourceFreeformAllocGroup{} + _ resourceWithSetFfBpClientFunc = &resourceFreeformAllocGroup{} + _ resourceWithSetBpLockFunc = &resourceFreeformAllocGroup{} +) + +type resourceFreeformAllocGroup struct { + getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) + lockFunc func(context.Context, string) error +} + +func (o *resourceFreeformAllocGroup) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_freeform_system" +} + +func (o *resourceFreeformAllocGroup) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + configureResource(ctx, o, req, resp) +} + +func (o *resourceFreeformAllocGroup) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: docCategoryFreeform + "This resource creates a System in a Freeform Blueprint.", + Attributes: blueprint.FreeformAllocGroup{}.ResourceAttributes(), + } +} + +func (o *resourceFreeformAllocGroup) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan blueprint.FreeformAllocGroup + 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 + } + + id, err := bp.CreateSystem(ctx, request) + if err != nil { + resp.Diagnostics.AddError("error creating new System", err.Error()) + return + } + + plan.Id = types.StringValue(id.String()) + + // set state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (o *resourceFreeformAllocGroup) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state blueprint.FreeformAllocGroup + 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.GetSystem(ctx, apstra.ObjectId(state.Id.ValueString())) + if err != nil { + if utils.IsApstra404(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("Error retrieving Freeform System", 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 *resourceFreeformAllocGroup) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Get plan values + var plan blueprint.FreeformAllocGroup + 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 + } + + request := plan.Request(ctx, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // Update Config Template + err = bp.UpdateSystem(ctx, apstra.ObjectId(plan.Id.ValueString()), request) + if err != nil { + resp.Diagnostics.AddError("error updating Freeform System", err.Error()) + return + } + + // set state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (o *resourceFreeformAllocGroup) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state blueprint.FreeformAllocGroup + 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.DeleteSystem(ctx, apstra.ObjectId(state.Id.ValueString())) + if err != nil { + if utils.IsApstra404(err) { + return // 404 is okay + } + resp.Diagnostics.AddError("error deleting Freeform System", err.Error()) + return + } +} + +func (o *resourceFreeformAllocGroup) setBpClientFunc(f func(context.Context, string) (*apstra.FreeformClient, error)) { + o.getBpClientFunc = f +} + +func (o *resourceFreeformAllocGroup) setBpLockFunc(f func(context.Context, string) error) { + o.lockFunc = f +} diff --git a/go.mod b/go.mod index 5402aabf..35942cf8 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/Juniper/terraform-provider-apstra go 1.22.5 -//replace github.com/Juniper/apstra-go-sdk => ../apstra-go-sdk +replace github.com/Juniper/apstra-go-sdk => ../apstra-go-sdk require ( github.com/IBM/netaddr v1.5.0 From 19c32884dd91e1316fd2b0a2286a4fcf41a6c012 Mon Sep 17 00:00:00 2001 From: bwJuniper <98770420+bwJuniper@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:32:52 +0200 Subject: [PATCH 3/7] go.mod change --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 35942cf8..5402aabf 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/Juniper/terraform-provider-apstra go 1.22.5 -replace github.com/Juniper/apstra-go-sdk => ../apstra-go-sdk +//replace github.com/Juniper/apstra-go-sdk => ../apstra-go-sdk require ( github.com/IBM/netaddr v1.5.0 From 2510db7ece28d995622c309a3600a466bfa7a797 Mon Sep 17 00:00:00 2001 From: bwJuniper <98770420+bwJuniper@users.noreply.github.com> Date: Thu, 1 Aug 2024 22:30:01 +0200 Subject: [PATCH 4/7] finish alloc_group integration tests --- apstra/blueprint/freeform_alloc_group.go | 68 ++-- apstra/blueprint/freeform_resource.go | 4 +- apstra/data_source_freeform_alloc_group.go | 4 +- apstra/export_test.go | 1 + apstra/provider.go | 2 + apstra/resource_freeform_alloc_group.go | 20 +- ...e_freeform_alloc_group_integration_test.go | 316 ++++++++++++++++++ apstra/utils/freeform_resource.go | 2 +- apstra/utils/resource_pool.go | 17 + apstra/utils/rosetta.go | 30 ++ apstra/utils/rosetta_test.go | 5 + docs/data-sources/freeform_alloc_group.md | 85 +++++ docs/resources/freeform_alloc_group.md | 82 +++++ .../apstra_freeform_alloc_group/example.tf | 43 +++ .../apstra_freeform_alloc_group/example.tf | 43 +++ go.mod | 4 +- go.sum | 4 +- 17 files changed, 689 insertions(+), 41 deletions(-) create mode 100644 apstra/resource_freeform_alloc_group_integration_test.go create mode 100644 apstra/utils/resource_pool.go create mode 100644 docs/data-sources/freeform_alloc_group.md create mode 100644 docs/resources/freeform_alloc_group.md create mode 100644 examples/data-sources/apstra_freeform_alloc_group/example.tf create mode 100644 examples/resources/apstra_freeform_alloc_group/example.tf diff --git a/apstra/blueprint/freeform_alloc_group.go b/apstra/blueprint/freeform_alloc_group.go index f4a9af2b..2cc32531 100644 --- a/apstra/blueprint/freeform_alloc_group.go +++ b/apstra/blueprint/freeform_alloc_group.go @@ -3,7 +3,10 @@ package blueprint import ( "context" "fmt" + "github.com/Juniper/terraform-provider-apstra/apstra/utils" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "regexp" + "strings" "github.com/Juniper/apstra-go-sdk/apstra" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -22,19 +25,19 @@ type FreeformAllocGroup struct { Id types.String `tfsdk:"id"` Name types.String `tfsdk:"name"` Type types.String `tfsdk:"type"` - PoolId types.String `tfsdk:"pool_id"` + PoolIds types.Set `tfsdk:"pool_ids"` } func (o FreeformAllocGroup) 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 System lives.", + "the Blueprint where the Allocation Group lives.", Required: true, Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, "id": dataSourceSchema.StringAttribute{ - MarkdownDescription: "Populate this field to look up the Freeform System by ID. Required when `name` is omitted.", + MarkdownDescription: "Populate this field to look up the Allocation Group by ID. Required when `name` is omitted.", Optional: true, Computed: true, Validators: []validator.String{ @@ -46,18 +49,20 @@ func (o FreeformAllocGroup) DataSourceAttributes() map[string]dataSourceSchema.A }, }, "name": dataSourceSchema.StringAttribute{ - MarkdownDescription: "Populate this field to look up System by Name. Required when `id` is omitted.", + MarkdownDescription: "Populate this field to look up the Allocation Group by Name. Required when `id` is omitted.", Optional: true, Computed: true, Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, "type": dataSourceSchema.StringAttribute{ - MarkdownDescription: "type of the Resource Pool, either Internal or External", - Computed: true, + MarkdownDescription: fmt.Sprintf("type of the Allocation Group, must be one of :\n - `" + + strings.Join(utils.AllResourcePoolTypes(), "`\n - `") + "`\n"), + Computed: true, }, - "pool_id": dataSourceSchema.StringAttribute{ + "pool_ids": dataSourceSchema.SetAttribute{ MarkdownDescription: "Pool ID assigned to the allocation group", Computed: true, + ElementType: types.StringType, }, } } @@ -71,40 +76,59 @@ func (o FreeformAllocGroup) ResourceAttributes() map[string]resourceSchema.Attri PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, }, "id": resourceSchema.StringAttribute{ - MarkdownDescription: "ID of the Freeform System.", + MarkdownDescription: "ID of the Freeform Allocation Group.", Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, }, "name": resourceSchema.StringAttribute{ - MarkdownDescription: "Freeform System name as shown in the Web UI.", + MarkdownDescription: "Freeform Allocation Group name as shown in the Web UI.", Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, 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.-_"), + }, }, "type": resourceSchema.StringAttribute{ - MarkdownDescription: fmt.Sprintf("Type of the System. Must be one of `%s` or `%s`", apstra.SystemTypeInternal, apstra.SystemTypeExternal), - Required: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, - Validators: []validator.String{stringvalidator.OneOf(apstra.SystemTypeInternal.String(), apstra.SystemTypeExternal.String())}, + MarkdownDescription: fmt.Sprintf("type of the Allocation Group, must be one of :\n - `" + + strings.Join(utils.AllResourcePoolTypes(), "`\n - `") + "`\n"), + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + Validators: []validator.String{stringvalidator.OneOf(utils.AllFFResourceTypes()...)}, }, - "pool_id": resourceSchema.StringAttribute{ - MarkdownDescription: "ID (usually serial number) of the Managed Device to associate with this System", - Optional: true, - Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + "pool_ids": resourceSchema.SetAttribute{ + MarkdownDescription: "ID of the Pool to associate with this Allocation Group", + ElementType: types.StringType, + Required: true, + Validators: []validator.Set{setvalidator.SizeAtLeast(1)}, }, } } func (o *FreeformAllocGroup) Request(ctx context.Context, diags *diag.Diagnostics) *apstra.FreeformAllocGroupData { + // unpack + var allocGroupType apstra.ResourcePoolType + err := utils.ApiStringerFromFriendlyString(&allocGroupType, o.Type.ValueString()) + if err != nil { + diags.AddError(fmt.Sprintf("error parsing type %q", o.Type.ValueString()), err.Error()) + } + + var poolIds []apstra.ObjectId + diags.Append(o.PoolIds.ElementsAs(ctx, &poolIds, false)...) + if diags.HasError() { + return nil + } return &apstra.FreeformAllocGroupData{ - Name: "", - Type: apstra.ResourcePoolType{}, - PoolIds: nil, + Name: o.Name.ValueString(), + Type: allocGroupType, + PoolIds: poolIds, } } func (o *FreeformAllocGroup) LoadApiData(ctx context.Context, in *apstra.FreeformAllocGroupData, diags *diag.Diagnostics) { + // pack o.Name = types.StringValue(in.Name) - o.Type = types.StringValue(in.Type.String()) + o.Type = types.StringValue(utils.StringersToFriendlyString(in.Type)) + o.PoolIds = utils.SetValueOrNull(ctx, types.StringType, in.PoolIds, diags) } diff --git a/apstra/blueprint/freeform_resource.go b/apstra/blueprint/freeform_resource.go index e02aac5f..cf886bb9 100644 --- a/apstra/blueprint/freeform_resource.go +++ b/apstra/blueprint/freeform_resource.go @@ -138,9 +138,9 @@ func (o FreeformResource) ResourceAttributes() map[string]resourceSchema.Attribu }, "type": resourceSchema.StringAttribute{ MarkdownDescription: "type of the Resource, must be one of :\n - `" + - strings.Join(utils.AllResourceTypes(), "`\n - `") + "`\n", + strings.Join(utils.AllFFResourceTypes(), "`\n - `") + "`\n", Optional: true, - Validators: []validator.String{stringvalidator.OneOf(utils.AllResourceTypes()...)}, + Validators: []validator.String{stringvalidator.OneOf(utils.AllFFResourceTypes()...)}, }, "integer_value": resourceSchema.Int64Attribute{ MarkdownDescription: fmt.Sprintf("Value used by integer type resources (`%s`, `%s`, `%s`, `%s`). "+ diff --git a/apstra/data_source_freeform_alloc_group.go b/apstra/data_source_freeform_alloc_group.go index e41254b5..3ea3f9d4 100644 --- a/apstra/data_source_freeform_alloc_group.go +++ b/apstra/data_source_freeform_alloc_group.go @@ -23,7 +23,7 @@ type dataSourceFreeformAllocGroup struct { } func (o *dataSourceFreeformAllocGroup) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_freeform_system" + resp.TypeName = req.ProviderTypeName + "_freeform_alloc_group" } func (o *dataSourceFreeformAllocGroup) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { @@ -32,7 +32,7 @@ func (o *dataSourceFreeformAllocGroup) Configure(ctx context.Context, req dataso func (o *dataSourceFreeformAllocGroup) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ - MarkdownDescription: docCategoryFreeform + "This data source provides details of a specific Freeform System.\n\n" + + MarkdownDescription: docCategoryFreeform + "This data source provides details of a Freeform Allocation Group.\n\n" + "At least one optional attribute is required.", Attributes: blueprint.FreeformAllocGroup{}.DataSourceAttributes(), } diff --git a/apstra/export_test.go b/apstra/export_test.go index 867ada4b..6862f898 100644 --- a/apstra/export_test.go +++ b/apstra/export_test.go @@ -18,6 +18,7 @@ var ( ResourceFreeformPropertySet = resourceFreeformPropertySet{} ResourceFreeformResourceGroup = resourceFreeformResourceGroup{} ResourceFreeformResource = resourceFreeformResource{} + ResourceFreeformAllocGroup = resourceFreeformAllocGroup{} ResourceFreeformSystem = resourceFreeformSystem{} ResourceIntegerPool = resourceIntegerPool{} ResourceIpv4Pool = resourceIpv4Pool{} diff --git a/apstra/provider.go b/apstra/provider.go index 05bdfee2..278a6da2 100644 --- a/apstra/provider.go +++ b/apstra/provider.go @@ -550,6 +550,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource func() datasource.DataSource { return &dataSourceDatacenterVirtualNetwork{} }, func() datasource.DataSource { return &dataSourceDatacenterVirtualNetworks{} }, func() datasource.DataSource { return &dataSourceDeviceConfig{} }, + func() datasource.DataSource { return &dataSourceFreeformAllocGroup{} }, func() datasource.DataSource { return &dataSourceFreeformConfigTemplate{} }, func() datasource.DataSource { return &dataSourceFreeformLink{} }, func() datasource.DataSource { return &dataSourceFreeformPropertySet{} }, @@ -610,6 +611,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { func() resource.Resource { return &resourceDatacenterIpLinkAddressing{} }, func() resource.Resource { return &resourceDatacenterVirtualNetwork{} }, func() resource.Resource { return &resourceDeviceAllocation{} }, + func() resource.Resource { return &resourceFreeformAllocGroup{} }, func() resource.Resource { return &resourceFreeformConfigTemplate{} }, func() resource.Resource { return &resourceFreeformLink{} }, func() resource.Resource { return &resourceFreeformPropertySet{} }, diff --git a/apstra/resource_freeform_alloc_group.go b/apstra/resource_freeform_alloc_group.go index 723f4303..9d14600c 100644 --- a/apstra/resource_freeform_alloc_group.go +++ b/apstra/resource_freeform_alloc_group.go @@ -23,7 +23,7 @@ type resourceFreeformAllocGroup struct { } func (o *resourceFreeformAllocGroup) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_freeform_system" + resp.TypeName = req.ProviderTypeName + "_freeform_alloc_group" } func (o *resourceFreeformAllocGroup) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { @@ -32,7 +32,7 @@ func (o *resourceFreeformAllocGroup) Configure(ctx context.Context, req resource func (o *resourceFreeformAllocGroup) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ - MarkdownDescription: docCategoryFreeform + "This resource creates a System in a Freeform Blueprint.", + MarkdownDescription: docCategoryFreeform + "This resource creates an Allocation Group in a Freeform Blueprint.", Attributes: blueprint.FreeformAllocGroup{}.ResourceAttributes(), } } @@ -71,9 +71,9 @@ func (o *resourceFreeformAllocGroup) Create(ctx context.Context, req resource.Cr return } - id, err := bp.CreateSystem(ctx, request) + id, err := bp.CreateAllocGroup(ctx, request) if err != nil { - resp.Diagnostics.AddError("error creating new System", err.Error()) + resp.Diagnostics.AddError("error creating new Allocation Group", err.Error()) return } @@ -101,13 +101,13 @@ func (o *resourceFreeformAllocGroup) Read(ctx context.Context, req resource.Read return } - api, err := bp.GetSystem(ctx, apstra.ObjectId(state.Id.ValueString())) + api, err := bp.GetAllocGroup(ctx, apstra.ObjectId(state.Id.ValueString())) if err != nil { if utils.IsApstra404(err) { resp.State.RemoveResource(ctx) return } - resp.Diagnostics.AddError("Error retrieving Freeform System", err.Error()) + resp.Diagnostics.AddError(fmt.Sprintf("Error retrieving Freeform Allocation Group %s", state.Id), err.Error()) return } @@ -154,9 +154,9 @@ func (o *resourceFreeformAllocGroup) Update(ctx context.Context, req resource.Up } // Update Config Template - err = bp.UpdateSystem(ctx, apstra.ObjectId(plan.Id.ValueString()), request) + err = bp.UpdateAllocGroup(ctx, apstra.ObjectId(plan.Id.ValueString()), request) if err != nil { - resp.Diagnostics.AddError("error updating Freeform System", err.Error()) + resp.Diagnostics.AddError("error updating Freeform Allocation Group", err.Error()) return } @@ -191,12 +191,12 @@ func (o *resourceFreeformAllocGroup) Delete(ctx context.Context, req resource.De } // Delete Config Template by calling API - err = bp.DeleteSystem(ctx, apstra.ObjectId(state.Id.ValueString())) + err = bp.DeleteAllocGroup(ctx, apstra.ObjectId(state.Id.ValueString())) if err != nil { if utils.IsApstra404(err) { return // 404 is okay } - resp.Diagnostics.AddError("error deleting Freeform System", err.Error()) + resp.Diagnostics.AddError("error deleting Freeform Allocation Group", err.Error()) return } } diff --git a/apstra/resource_freeform_alloc_group_integration_test.go b/apstra/resource_freeform_alloc_group_integration_test.go new file mode 100644 index 00000000..81bd4939 --- /dev/null +++ b/apstra/resource_freeform_alloc_group_integration_test.go @@ -0,0 +1,316 @@ +//go:build integration + +package tfapstra_test + +import ( + "context" + "fmt" + "github.com/Juniper/terraform-provider-apstra/apstra/utils" + "github.com/stretchr/testify/require" + "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/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +const ( + resourceFreeformAllocGroupHcl = ` +resource %q %q { + blueprint_id = %q + name = %q + type = %q + pool_ids = %s + } +` +) + +type resourceAllocGroup struct { + blueprintId string + name string + groupType apstra.ResourcePoolType + poolIds []string +} + +func (o resourceAllocGroup) render(rType, rName string) string { + return fmt.Sprintf(resourceFreeformAllocGroupHcl, + rType, rName, + o.blueprintId, + o.name, + utils.StringersToFriendlyString(o.groupType), + stringSetOrNull(o.poolIds), + ) +} + +func (o resourceAllocGroup) 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", "type", utils.StringersToFriendlyString(o.groupType)) + return result +} + +func TestResourceAllocGroup(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 := testutils.FfBlueprintA(t, ctx) + + newAsnPool := func(t testing.TB) string { + t.Helper() + + asnRange := []apstra.IntfIntRange{ + apstra.IntRangeRequest{First: 65535, Last: uint32(acctest.RandIntRange(65536, 67000))}, + } + + 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)) }) + return asnPoolId.String() + } + + newIntPool := func(t testing.TB) string { + t.Helper() + + intRange := []apstra.IntfIntRange{ + apstra.IntRangeRequest{First: 1, Last: uint32(acctest.RandIntRange(2, 3000))}, + } + + 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)) }) + return intPoolId.String() + } + + newVniPool := func(t testing.TB) string { + t.Helper() + + vniRange := []apstra.IntfIntRange{ + apstra.IntRangeRequest{First: 4100, Last: uint32(acctest.RandIntRange(4100, 10000))}, + } + + intPoolId, 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, intPoolId)) }) + return intPoolId.String() + } + + newIpv4Pool := func(t testing.TB) string { + t.Helper() + + ip4subnets := []net.IPNet{ + randomPrefix(t, "10.0.0.0/16", 24), + randomPrefix(t, "10.1.0.0/16", 25), + randomPrefix(t, "10.2.0.0/16", 26), + randomPrefix(t, "10.3.0.0/16", 27), + } + + netStrings := make([]apstra.NewIpSubnet, len(ip4subnets)) + for i := range netStrings { + netStrings[i].Network = ip4subnets[i].String() + } + + ipv4PoolId, err := bp.Client().CreateIp4Pool(ctx, &apstra.NewIpPoolRequest{ + DisplayName: acctest.RandString(6), + Subnets: netStrings, + }) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, bp.Client().DeleteIp4Pool(ctx, ipv4PoolId)) }) + return ipv4PoolId.String() + } + + newIpv6Pool := func(t testing.TB) string { + t.Helper() + + ip6subnets := []net.IPNet{ + randomPrefix(t, "2001:db8:0::/48", 112), + randomPrefix(t, "2001:db8:1::/48", 112), + randomPrefix(t, "2001:db8:2::/48", 112), + } + + netStrings := make([]apstra.NewIpSubnet, len(ip6subnets)) + for i := range netStrings { + netStrings[i].Network = ip6subnets[i].String() + } + + ipv6PoolId, err := bp.Client().CreateIp6Pool(ctx, &apstra.NewIpPoolRequest{ + DisplayName: acctest.RandString(6), + Subnets: netStrings, + }) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, bp.Client().DeleteIp6Pool(ctx, ipv6PoolId)) }) + return ipv6PoolId.String() + } + + namesByKey := make(map[string]string) + nameByKey := func(key string) string { + if name, ok := namesByKey[key]; ok { + return name + } + namesByKey[key] = acctest.RandString(6) + return namesByKey[key] + } + + type testStep struct { + config resourceAllocGroup + } + type testCase struct { + apiVersionConstraints version.Constraints + steps []testStep + } + + testCases := map[string]testCase{ + "start_asn": { + steps: []testStep{ + { + config: resourceAllocGroup{ + blueprintId: bp.Id().String(), + name: nameByKey("test_asn"), + groupType: apstra.ResourcePoolTypeAsn, + poolIds: []string{newAsnPool(t)}, + }, + }, + { + config: resourceAllocGroup{ + blueprintId: bp.Id().String(), + name: nameByKey("test_asn"), + groupType: apstra.ResourcePoolTypeAsn, + poolIds: []string{newAsnPool(t), newAsnPool(t)}, + }, + }, + }, + }, + "test_int": { + steps: []testStep{ + { + config: resourceAllocGroup{ + blueprintId: bp.Id().String(), + name: nameByKey("test_int"), + groupType: apstra.ResourcePoolTypeInt, + poolIds: []string{newIntPool(t)}, + }, + }, + { + config: resourceAllocGroup{ + blueprintId: bp.Id().String(), + name: nameByKey("test_int"), + groupType: apstra.ResourcePoolTypeInt, + poolIds: []string{newIntPool(t)}, + }, + }, + }, + }, + "test_vni": { + steps: []testStep{ + { + config: resourceAllocGroup{ + blueprintId: bp.Id().String(), + name: nameByKey("test_vni"), + groupType: apstra.ResourcePoolTypeVni, + poolIds: []string{newVniPool(t)}, + }, + }, + { + config: resourceAllocGroup{ + blueprintId: bp.Id().String(), + name: nameByKey("test_vni"), + groupType: apstra.ResourcePoolTypeVni, + poolIds: []string{newVniPool(t)}, + }, + }, + }, + }, + "test_ipv4": { + steps: []testStep{ + { + config: resourceAllocGroup{ + blueprintId: bp.Id().String(), + name: nameByKey("test_vni"), + groupType: apstra.ResourcePoolTypeIpv4, + poolIds: []string{newIpv4Pool(t)}, + }, + }, + { + config: resourceAllocGroup{ + blueprintId: bp.Id().String(), + name: nameByKey("test_vni"), + groupType: apstra.ResourcePoolTypeIpv4, + poolIds: []string{newIpv4Pool(t)}, + }, + }, + }, + }, + "test_ipv6": { + steps: []testStep{ + { + config: resourceAllocGroup{ + blueprintId: bp.Id().String(), + name: nameByKey("test_ipv6"), + groupType: apstra.ResourcePoolTypeIpv6, + poolIds: []string{newIpv6Pool(t)}, + }, + }, + { + config: resourceAllocGroup{ + blueprintId: bp.Id().String(), + name: nameByKey("test_ipv6"), + groupType: apstra.ResourcePoolTypeIpv6, + poolIds: []string{newIpv6Pool(t)}, + }, + }, + }, + }, + } + + resourceType := tfapstra.ResourceName(ctx, &tfapstra.ResourceFreeformAllocGroup) + + 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/apstra/utils/freeform_resource.go b/apstra/utils/freeform_resource.go index f27f1416..6031d98d 100644 --- a/apstra/utils/freeform_resource.go +++ b/apstra/utils/freeform_resource.go @@ -6,7 +6,7 @@ import ( "github.com/Juniper/apstra-go-sdk/apstra" ) -func AllResourceTypes() []string { +func AllFFResourceTypes() []string { members := apstra.FFResourceTypes.Members() result := make([]string, len(members)) for i, member := range members { diff --git a/apstra/utils/resource_pool.go b/apstra/utils/resource_pool.go new file mode 100644 index 00000000..8fe9019d --- /dev/null +++ b/apstra/utils/resource_pool.go @@ -0,0 +1,17 @@ +package utils + +import ( + "sort" + + "github.com/Juniper/apstra-go-sdk/apstra" +) + +func AllResourcePoolTypes() []string { + members := apstra.ResourcePoolTypes.Members() + result := make([]string, len(members)) + for i, member := range members { + result[i] = StringersToFriendlyString(member) + } + sort.Strings(result) + return result +} diff --git a/apstra/utils/rosetta.go b/apstra/utils/rosetta.go index 59cc302e..ba1b6572 100644 --- a/apstra/utils/rosetta.go +++ b/apstra/utils/rosetta.go @@ -38,6 +38,8 @@ const ( freeformResourceTypeIpv4 = "ipv4" freeformResourceTypeHostIpv4 = "host_ipv4" + + resourcePoolTypeIpv4 = "ipv4" ) type StringerWithFromString interface { @@ -77,6 +79,8 @@ func StringersToFriendlyString(in ...fmt.Stringer) string { return refDesignToFriendlyString(in0) case apstra.ResourceGroupName: return resourceGroupNameToFriendlyString(in0) + case apstra.ResourcePoolType: + return resourcePoolTypeToFriendlyString(in0) case apstra.StorageSchemaPath: return storageSchemaPathToFriendlyString(in0) } @@ -117,6 +121,8 @@ func ApiStringerFromFriendlyString(target StringerWithFromString, in ...string) return refDesignFromFriendlyString(target, in...) case *apstra.ResourceGroupName: return resourceGroupNameFromFriendlyString(target, in...) + case *apstra.ResourcePoolType: + return resourcePoolTypeFromFriendlyString(target, in...) case *apstra.StorageSchemaPath: return target.FromString("aos.sdk.telemetry.schemas." + in[0]) } @@ -246,6 +252,15 @@ func resourceGroupNameToFriendlyString(in apstra.ResourceGroupName) string { return in.String() } +func resourcePoolTypeToFriendlyString(in apstra.ResourcePoolType) string { + switch in { + case apstra.ResourcePoolTypeIpv4: + return resourcePoolTypeIpv4 + } + + return in.String() +} + func asnAllocationSchemeFromFriendlyString(target *apstra.AsnAllocationScheme, in ...string) error { if len(in) == 0 { return target.FromString("") @@ -419,3 +434,18 @@ func resourceGroupNameFromFriendlyString(target *apstra.ResourceGroupName, in .. return nil } + +func resourcePoolTypeFromFriendlyString(target *apstra.ResourcePoolType, in ...string) error { + if len(in) == 0 { + return target.FromString("") + } + + switch in[0] { + case resourcePoolTypeIpv4: + *target = apstra.ResourcePoolTypeIpv4 + default: + return target.FromString(in[0]) + } + + return nil +} diff --git a/apstra/utils/rosetta_test.go b/apstra/utils/rosetta_test.go index 0728cb02..2cbcb3b5 100644 --- a/apstra/utils/rosetta_test.go +++ b/apstra/utils/rosetta_test.go @@ -46,6 +46,8 @@ func TestRosetta(t *testing.T) { {string: "spine_superspine_link_ips_ipv6", stringers: []fmt.Stringer{apstra.ResourceGroupNameSuperspineSpineIp6}}, {string: "to_generic_link_ips_ipv6", stringers: []fmt.Stringer{apstra.ResourceGroupNameToGenericLinkIpv6}}, + {string: "ipv4", stringers: []fmt.Stringer{apstra.ResourcePoolTypeIpv4}}, + {string: "none", stringers: []fmt.Stringer{apstra.InterfaceNumberingIpv4TypeNone}}, {string: "none", stringers: []fmt.Stringer{apstra.InterfaceNumberingIpv6TypeNone}}, } @@ -84,6 +86,9 @@ func TestRosetta(t *testing.T) { case apstra.ResourceGroupName: x := apstra.ResourceGroupName(-1) target = &x + case apstra.ResourcePoolType: + x := apstra.ResourcePoolType{} + target = &x } if target == nil { diff --git a/docs/data-sources/freeform_alloc_group.md b/docs/data-sources/freeform_alloc_group.md new file mode 100644 index 00000000..5b8cba40 --- /dev/null +++ b/docs/data-sources/freeform_alloc_group.md @@ -0,0 +1,85 @@ +--- +page_title: "apstra_freeform_alloc_group Data Source - terraform-provider-apstra" +subcategory: "Reference Design: Freeform" +description: |- + This data source provides details of a Freeform Allocation Group. + At least one optional attribute is required. +--- + +# apstra_freeform_alloc_group (Data Source) + +This data source provides details of a Freeform Allocation Group. + +At least one optional attribute is required. + + +## Example Usage + +```terraform +resource "apstra_freeform_resource_group" "fizz_grp" { + blueprint_id = "6f1e3a4e-2920-451f-b069-c256c9fff938" + name = "fizz_grp" +} + +resource "apstra_asn_pool" "rfc5398" { + name = "RFC5398 ASNs" + ranges = [ + { + first = 64496 + last = 64511 + }, + { + first = 65536 + last = 65551 + }, + ] +} + +resource "apstra_freeform_alloc_group" "test" { + blueprint_id = "6f1e3a4e-2920-451f-b069-c256c9fff938" + name = "test_alloc_group2" + type = "asn" + pool_id = [apstra_asn_pool.rfc5398.id] +} + +data "apstra_freeform_alloc_group" "test" { + blueprint_id = "6f1e3a4e-2920-451f-b069-c256c9fff938" + id = apstra_freeform_alloc_group.test.id +} + +output "test_resource_out" { value = data.apstra_freeform_alloc_group.test } + +# output produced = +#test_resource_out = { +# "blueprint_id" = "6f1e3a4e-2920-451f-b069-c256c9fff938" +# "id" = "rag_asn_test_alloc_group2" +# "name" = "test_alloc_group2" +# "pool_id" = toset([ +# "5d945d83-3361-4e9f-8a35-e935d549c298", +# ]) +# "type" = "asn" +#} +``` + + +## Schema + +### Required + +- `blueprint_id` (String) Apstra Blueprint ID. Used to identify the Blueprint where the Allocation Group lives. + +### Optional + +- `id` (String) Populate this field to look up the Allocation Group by ID. Required when `name` is omitted. +- `name` (String) Populate this field to look up the Allocation Group by Name. Required when `id` is omitted. + +### Read-Only + +- `pool_ids` (Set of String) Pool ID assigned to the allocation group +- `type` (String) type of the Allocation Group, must be one of : + - `asn` + - `integer` + - `ipv4` + - `ipv6` + - `vlan` + - `vni` diff --git a/docs/resources/freeform_alloc_group.md b/docs/resources/freeform_alloc_group.md new file mode 100644 index 00000000..7aa4721f --- /dev/null +++ b/docs/resources/freeform_alloc_group.md @@ -0,0 +1,82 @@ +--- +page_title: "apstra_freeform_alloc_group Resource - terraform-provider-apstra" +subcategory: "Reference Design: Freeform" +description: |- + This resource creates an Allocation Group in a Freeform Blueprint. +--- + +# apstra_freeform_alloc_group (Resource) + +This resource creates an Allocation Group in a Freeform Blueprint. + + +## Example Usage + +```terraform +resource "apstra_freeform_resource_group" "fizz_grp" { + blueprint_id = "6f1e3a4e-2920-451f-b069-c256c9fff938" + name = "fizz_grp" +} + +resource "apstra_asn_pool" "rfc5398" { + name = "RFC5398 ASNs" + ranges = [ + { + first = 64496 + last = 64511 + }, + { + first = 65536 + last = 65551 + }, + ] +} + +resource "apstra_freeform_alloc_group" "test" { + blueprint_id = "6f1e3a4e-2920-451f-b069-c256c9fff938" + name = "test_alloc_group2" + type = "asn" + pool_id = [apstra_asn_pool.rfc5398.id] +} + +data "apstra_freeform_alloc_group" "test" { + blueprint_id = "6f1e3a4e-2920-451f-b069-c256c9fff938" + id = apstra_freeform_alloc_group.test.id +} + +output "test_resource_out" { value = data.apstra_freeform_alloc_group.test } + +# output produced = +#test_resource_out = { +# "blueprint_id" = "6f1e3a4e-2920-451f-b069-c256c9fff938" +# "id" = "rag_asn_test_alloc_group2" +# "name" = "test_alloc_group2" +# "pool_id" = toset([ +# "5d945d83-3361-4e9f-8a35-e935d549c298", +# ]) +# "type" = "asn" +#} +``` + + +## Schema + +### Required + +- `blueprint_id` (String) Apstra Blueprint ID. +- `name` (String) Freeform Allocation Group name as shown in the Web UI. +- `pool_ids` (Set of String) ID of the Pool to associate with this Allocation Group +- `type` (String) type of the Allocation Group, must be one of : + - `asn` + - `integer` + - `ipv4` + - `ipv6` + - `vlan` + - `vni` + +### Read-Only + +- `id` (String) ID of the Freeform Allocation Group. + + + diff --git a/examples/data-sources/apstra_freeform_alloc_group/example.tf b/examples/data-sources/apstra_freeform_alloc_group/example.tf new file mode 100644 index 00000000..d4083cef --- /dev/null +++ b/examples/data-sources/apstra_freeform_alloc_group/example.tf @@ -0,0 +1,43 @@ +resource "apstra_freeform_resource_group" "fizz_grp" { + blueprint_id = "6f1e3a4e-2920-451f-b069-c256c9fff938" + name = "fizz_grp" +} + +resource "apstra_asn_pool" "rfc5398" { + name = "RFC5398 ASNs" + ranges = [ + { + first = 64496 + last = 64511 + }, + { + first = 65536 + last = 65551 + }, + ] +} + +resource "apstra_freeform_alloc_group" "test" { + blueprint_id = "6f1e3a4e-2920-451f-b069-c256c9fff938" + name = "test_alloc_group2" + type = "asn" + pool_id = [apstra_asn_pool.rfc5398.id] +} + +data "apstra_freeform_alloc_group" "test" { + blueprint_id = "6f1e3a4e-2920-451f-b069-c256c9fff938" + id = apstra_freeform_alloc_group.test.id +} + +output "test_resource_out" { value = data.apstra_freeform_alloc_group.test } + +# output produced = +#test_resource_out = { +# "blueprint_id" = "6f1e3a4e-2920-451f-b069-c256c9fff938" +# "id" = "rag_asn_test_alloc_group2" +# "name" = "test_alloc_group2" +# "pool_id" = toset([ +# "5d945d83-3361-4e9f-8a35-e935d549c298", +# ]) +# "type" = "asn" +#} \ No newline at end of file diff --git a/examples/resources/apstra_freeform_alloc_group/example.tf b/examples/resources/apstra_freeform_alloc_group/example.tf new file mode 100644 index 00000000..88b51f8f --- /dev/null +++ b/examples/resources/apstra_freeform_alloc_group/example.tf @@ -0,0 +1,43 @@ +resource "apstra_freeform_resource_group" "fizz_grp" { + blueprint_id = "6f1e3a4e-2920-451f-b069-c256c9fff938" + name = "fizz_grp" +} + +resource "apstra_asn_pool" "rfc5398" { + name = "RFC5398 ASNs" + ranges = [ + { + first = 64496 + last = 64511 + }, + { + first = 65536 + last = 65551 + }, + ] +} + +resource "apstra_freeform_alloc_group" "test" { + blueprint_id = "6f1e3a4e-2920-451f-b069-c256c9fff938" + name = "test_alloc_group2" + type = "asn" + pool_id = [apstra_asn_pool.rfc5398.id] +} + +data "apstra_freeform_alloc_group" "test" { + blueprint_id = "6f1e3a4e-2920-451f-b069-c256c9fff938" + id = apstra_freeform_alloc_group.test.id +} + +output "test_resource_out" { value = data.apstra_freeform_alloc_group.test } + +# output produced = +#test_resource_out = { +# "blueprint_id" = "6f1e3a4e-2920-451f-b069-c256c9fff938" +# "id" = "rag_asn_test_alloc_group2" +# "name" = "test_alloc_group2" +# "pool_id" = toset([ +# "5d945d83-3361-4e9f-8a35-e935d549c298", +# ]) +# "type" = "asn" +#} diff --git a/go.mod b/go.mod index 35942cf8..48b27712 100644 --- a/go.mod +++ b/go.mod @@ -2,11 +2,11 @@ module github.com/Juniper/terraform-provider-apstra go 1.22.5 -replace github.com/Juniper/apstra-go-sdk => ../apstra-go-sdk +//replace github.com/Juniper/apstra-go-sdk => ../apstra-go-sdk require ( github.com/IBM/netaddr v1.5.0 - github.com/Juniper/apstra-go-sdk v0.0.0-20240730135749-72db5614fa0e + github.com/Juniper/apstra-go-sdk v0.0.0-20240801202534-f251446c2750 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 ca0e5a76..bfd180ae 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-20240730135749-72db5614fa0e h1:1jTeweRZ1kFLJvUgM417Kc2DUeyzsvsVbkbs4yWdNaM= -github.com/Juniper/apstra-go-sdk v0.0.0-20240730135749-72db5614fa0e/go.mod h1:Xwj3X8v/jRZWv28o6vQAqD4lz2JmzaSYLZ2ch1SS89w= +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/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 e298c2922abc6c73a85769904af0c4476d32c3fd Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Thu, 1 Aug 2024 17:36:11 -0400 Subject: [PATCH 5/7] minor string fixup --- apstra/blueprint/freeform_alloc_group.go | 4 ++-- apstra/resource_freeform_alloc_group.go | 2 +- apstra/resource_freeform_alloc_group_integration_test.go | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apstra/blueprint/freeform_alloc_group.go b/apstra/blueprint/freeform_alloc_group.go index 2cc32531..0d28df44 100644 --- a/apstra/blueprint/freeform_alloc_group.go +++ b/apstra/blueprint/freeform_alloc_group.go @@ -60,7 +60,7 @@ func (o FreeformAllocGroup) DataSourceAttributes() map[string]dataSourceSchema.A Computed: true, }, "pool_ids": dataSourceSchema.SetAttribute{ - MarkdownDescription: "Pool ID assigned to the allocation group", + MarkdownDescription: "IDs of Resource Pools assigned to the allocation group", Computed: true, ElementType: types.StringType, }, @@ -97,7 +97,7 @@ func (o FreeformAllocGroup) ResourceAttributes() map[string]resourceSchema.Attri Validators: []validator.String{stringvalidator.OneOf(utils.AllFFResourceTypes()...)}, }, "pool_ids": resourceSchema.SetAttribute{ - MarkdownDescription: "ID of the Pool to associate with this Allocation Group", + MarkdownDescription: "IDs of Resource Pools assigned to the allocation group", ElementType: types.StringType, Required: true, Validators: []validator.Set{setvalidator.SizeAtLeast(1)}, diff --git a/apstra/resource_freeform_alloc_group.go b/apstra/resource_freeform_alloc_group.go index 9d14600c..37a420a6 100644 --- a/apstra/resource_freeform_alloc_group.go +++ b/apstra/resource_freeform_alloc_group.go @@ -153,7 +153,7 @@ func (o *resourceFreeformAllocGroup) Update(ctx context.Context, req resource.Up return } - // Update Config Template + // Update the Allocation Group err = bp.UpdateAllocGroup(ctx, apstra.ObjectId(plan.Id.ValueString()), request) if err != nil { resp.Diagnostics.AddError("error updating Freeform Allocation Group", err.Error()) diff --git a/apstra/resource_freeform_alloc_group_integration_test.go b/apstra/resource_freeform_alloc_group_integration_test.go index 81bd4939..ccf19926 100644 --- a/apstra/resource_freeform_alloc_group_integration_test.go +++ b/apstra/resource_freeform_alloc_group_integration_test.go @@ -62,7 +62,7 @@ func TestResourceAllocGroup(t *testing.T) { client := testutils.GetTestClient(t, ctx) apiVersion := version.Must(version.NewVersion(client.ApiVersion())) - // create a blueprint and a group... + // create a blueprint bp := testutils.FfBlueprintA(t, ctx) newAsnPool := func(t testing.TB) string { @@ -243,7 +243,7 @@ func TestResourceAllocGroup(t *testing.T) { { config: resourceAllocGroup{ blueprintId: bp.Id().String(), - name: nameByKey("test_vni"), + name: nameByKey("test_ipv4"), groupType: apstra.ResourcePoolTypeIpv4, poolIds: []string{newIpv4Pool(t)}, }, @@ -251,7 +251,7 @@ func TestResourceAllocGroup(t *testing.T) { { config: resourceAllocGroup{ blueprintId: bp.Id().String(), - name: nameByKey("test_vni"), + name: nameByKey("test_ipv4"), groupType: apstra.ResourcePoolTypeIpv4, poolIds: []string{newIpv4Pool(t)}, }, From e5da702403b669dc96267060bd4be72b24dfac9e Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Thu, 1 Aug 2024 17:37:41 -0400 Subject: [PATCH 6/7] make docs --- docs/data-sources/freeform_alloc_group.md | 2 +- docs/resources/freeform_alloc_group.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/data-sources/freeform_alloc_group.md b/docs/data-sources/freeform_alloc_group.md index 5b8cba40..33e5f94e 100644 --- a/docs/data-sources/freeform_alloc_group.md +++ b/docs/data-sources/freeform_alloc_group.md @@ -75,7 +75,7 @@ output "test_resource_out" { value = data.apstra_freeform_alloc_group.test } ### Read-Only -- `pool_ids` (Set of String) Pool ID assigned to the allocation group +- `pool_ids` (Set of String) IDs of Resource Pools assigned to the allocation group - `type` (String) type of the Allocation Group, must be one of : - `asn` - `integer` diff --git a/docs/resources/freeform_alloc_group.md b/docs/resources/freeform_alloc_group.md index 7aa4721f..40620f4a 100644 --- a/docs/resources/freeform_alloc_group.md +++ b/docs/resources/freeform_alloc_group.md @@ -65,7 +65,7 @@ output "test_resource_out" { value = data.apstra_freeform_alloc_group.test } - `blueprint_id` (String) Apstra Blueprint ID. - `name` (String) Freeform Allocation Group name as shown in the Web UI. -- `pool_ids` (Set of String) ID of the Pool to associate with this Allocation Group +- `pool_ids` (Set of String) IDs of Resource Pools assigned to the allocation group - `type` (String) type of the Allocation Group, must be one of : - `asn` - `integer` From bcdbea1e9c807b9ca7059e5c81407a2a6a431b2f Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Thu, 1 Aug 2024 17:41:02 -0400 Subject: [PATCH 7/7] gofumpt --- apstra/blueprint/freeform_alloc_group.go | 4 ++-- apstra/resource_freeform_alloc_group.go | 1 + apstra/resource_freeform_alloc_group_integration_test.go | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apstra/blueprint/freeform_alloc_group.go b/apstra/blueprint/freeform_alloc_group.go index 0d28df44..53b3323b 100644 --- a/apstra/blueprint/freeform_alloc_group.go +++ b/apstra/blueprint/freeform_alloc_group.go @@ -3,12 +3,12 @@ package blueprint import ( "context" "fmt" - "github.com/Juniper/terraform-provider-apstra/apstra/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "regexp" "strings" "github.com/Juniper/apstra-go-sdk/apstra" + "github.com/Juniper/terraform-provider-apstra/apstra/utils" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" dataSourceSchema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" diff --git a/apstra/resource_freeform_alloc_group.go b/apstra/resource_freeform_alloc_group.go index 37a420a6..6ce1ab6a 100644 --- a/apstra/resource_freeform_alloc_group.go +++ b/apstra/resource_freeform_alloc_group.go @@ -3,6 +3,7 @@ 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" diff --git a/apstra/resource_freeform_alloc_group_integration_test.go b/apstra/resource_freeform_alloc_group_integration_test.go index ccf19926..07ca8265 100644 --- a/apstra/resource_freeform_alloc_group_integration_test.go +++ b/apstra/resource_freeform_alloc_group_integration_test.go @@ -5,17 +5,17 @@ package tfapstra_test import ( "context" "fmt" - "github.com/Juniper/terraform-provider-apstra/apstra/utils" - "github.com/stretchr/testify/require" "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 (