diff --git a/apstra/blueprint/freeform_resource_group.go b/apstra/blueprint/freeform_resource_group.go new file mode 100644 index 00000000..01c94a49 --- /dev/null +++ b/apstra/blueprint/freeform_resource_group.go @@ -0,0 +1,158 @@ +package blueprint + +import ( + "context" + "encoding/json" + "regexp" + + "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + + "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 FreeformResourceGroup struct { + BlueprintId types.String `tfsdk:"blueprint_id"` + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + ParentId types.String `tfsdk:"parent_id"` + // Tags types.Set `tfsdk:"tags"` + Data jsontypes.Normalized `tfsdk:"data"` + GeneratorId types.String `tfsdk:"generator_id"` +} + +func (o FreeformResourceGroup) 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 Allocation Group lives.", + Required: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Populate this field to look up the Freeform Allocation Group 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 the Freeform Allocation Group by Name. Required when `id` is omitted.", + Optional: true, + Computed: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "parent_id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "ID of the group node that is present as a parent of the current one in a " + + "parent/child relationship. If this is a top-level (root) node, then `parent_id` will be `null`.", + Computed: true, + }, + //"tags": dataSourceSchema.SetAttribute{ + // MarkdownDescription: "Set of Tag labels", + // ElementType: types.StringType, + // Computed: true, + //}, + "data": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Arbitrary key-value mapping that is useful in a context of this group. " + + "For example, you can store some VRF-related data there or add properties that are useful " + + "only in context of resource allocation, but not systems or interfaces.", + Computed: true, + CustomType: jsontypes.NormalizedType{}, + }, + "generator_id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "ID of the group generator that created the group, if any.", + Computed: true, + }, + } +} + +func (o FreeformResourceGroup) 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 Resource Allocation Group.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "name": resourceSchema.StringAttribute{ + MarkdownDescription: "Freeform Resource Allocation Group 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.-_"), + }, + }, + "parent_id": resourceSchema.StringAttribute{ + MarkdownDescription: "ID of the parent Freeform Resource Allocation Group, if this group is to be nested.", + Optional: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + //"tags": resourceSchema.SetAttribute{ + // MarkdownDescription: "Set of Tag labels", + // ElementType: types.StringType, + // Optional: true, + // Validators: []validator.Set{setvalidator.SizeAtLeast(1)}, + //}, + "data": resourceSchema.StringAttribute{ + MarkdownDescription: "Arbitrary JSON-encoded key-value mapping that is useful in a context of this " + + "group. For example, you can store some VRF-related data there or add properties that are useful " + + "only in context of resource allocation, but not systems or interfaces.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString("{}"), + CustomType: jsontypes.NormalizedType{}, + }, + "generator_id": resourceSchema.StringAttribute{ + MarkdownDescription: "ID of the Generator that created Resource Allocation Group. " + + "Always `null` because groups created via resource declaration were not generated.", + Computed: true, + }, + } +} + +func (o *FreeformResourceGroup) Request(_ context.Context, _ *diag.Diagnostics) *apstra.FreeformRaGroupData { + //var tags []string + //diags.Append(o.Tags.ElementsAs(ctx, &tags, false)...) + //if diags.HasError() { + // return nil + //} + + return &apstra.FreeformRaGroupData{ + ParentId: (*apstra.ObjectId)(o.ParentId.ValueStringPointer()), + Label: o.Name.ValueString(), + // Tags: tags, + Data: json.RawMessage(o.Data.ValueString()), + GeneratorId: (*apstra.ObjectId)(o.GeneratorId.ValueStringPointer()), + } +} + +func (o *FreeformResourceGroup) LoadApiData(_ context.Context, in *apstra.FreeformRaGroupData, _ *diag.Diagnostics) { + o.Name = types.StringValue(in.Label) + if in.ParentId != nil { + o.ParentId = types.StringValue(string(*in.ParentId)) + } + + o.Data = jsontypes.NewNormalizedValue(string(in.Data)) + // o.Tags = utils.SetValueOrNull(ctx, types.StringType, in.Tags, diags) // safe to ignore diagnostic here + o.GeneratorId = types.StringPointerValue((*string)(in.GeneratorId)) +} diff --git a/apstra/datasource_freeform_resource_group.go b/apstra/datasource_freeform_resource_group.go new file mode 100644 index 00000000..91333d70 --- /dev/null +++ b/apstra/datasource_freeform_resource_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 = &dataSourceFreeformResourceGroup{} + _ datasourceWithSetFfBpClientFunc = &dataSourceFreeformResourceGroup{} +) + +type dataSourceFreeformResourceGroup struct { + getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) +} + +func (o *dataSourceFreeformResourceGroup) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_freeform_resource_group" +} + +func (o *dataSourceFreeformResourceGroup) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + configureDataSource(ctx, o, req, resp) +} + +func (o *dataSourceFreeformResourceGroup) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: docCategoryFreeform + "This data source provides details of a specific Freeform Resource Allocation Group.\n\n" + + "At least one optional attribute is required.", + Attributes: blueprint.FreeformResourceGroup{}.DataSourceAttributes(), + } +} + +func (o *dataSourceFreeformResourceGroup) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config blueprint.FreeformResourceGroup + 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.FreeformRaGroup + switch { + case !config.Id.IsNull(): + api, err = bp.GetRaGroup(ctx, apstra.ObjectId(config.Id.ValueString())) + if utils.IsApstra404(err) { + resp.Diagnostics.AddAttributeError( + path.Root("id"), + "Freeform Resource Allocation Group not found", + fmt.Sprintf("Freeform Resource Allocation Group with ID %s not found", config.Id)) + return + } + case !config.Name.IsNull(): + api, err = bp.GetRaGroupByName(ctx, config.Name.ValueString()) + if utils.IsApstra404(err) { + resp.Diagnostics.AddAttributeError( + path.Root("name"), + "Freeform Resource Allocation Group not found", + fmt.Sprintf("Freeform resource allocation group with Name %s not found", config.Name)) + return + } + } + if err != nil { + resp.Diagnostics.AddError("failed reading Freeform Resource Allocation Group", err.Error()) + return + } + if api.Data == nil { + resp.Diagnostics.AddError("failed reading Freeform Resource 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 *dataSourceFreeformResourceGroup) setBpClientFunc(f func(context.Context, string) (*apstra.FreeformClient, error)) { + o.getBpClientFunc = f +} diff --git a/apstra/export_test.go b/apstra/export_test.go index 658d31c6..697a024e 100644 --- a/apstra/export_test.go +++ b/apstra/export_test.go @@ -14,6 +14,7 @@ var ( ResourceFreeformLink = resourceFreeformLink{} ResourceFreeformSystem = resourceFreeformSystem{} ResourceFreeformPropertySet = resourceFreeformPropertySet{} + ResourceFreeformRaGroup = resourceFreeformResourceGroup{} ResourceIpv4Pool = resourceIpv4Pool{} ResourceTemplatePodBased = resourceTemplatePodBased{} ResourceTemplateCollapsed = resourceTemplateCollapsed{} diff --git a/apstra/provider.go b/apstra/provider.go index 815157a6..4b8ae107 100644 --- a/apstra/provider.go +++ b/apstra/provider.go @@ -551,6 +551,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 &dataSourceFreeformResourceGroup{} }, func() datasource.DataSource { return &dataSourceFreeformSystem{} }, func() datasource.DataSource { return &dataSourceIntegerPool{} }, func() datasource.DataSource { return &dataSourceInterfacesByLinkTag{} }, @@ -609,6 +610,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 &resourceFreeformResourceGroup{} }, func() resource.Resource { return &resourceFreeformSystem{} }, func() resource.Resource { return &resourceIntegerPool{} }, func() resource.Resource { return &resourceInterfaceMap{} }, diff --git a/apstra/resource_freeform_resource_group.go b/apstra/resource_freeform_resource_group.go new file mode 100644 index 00000000..b09b38e5 --- /dev/null +++ b/apstra/resource_freeform_resource_group.go @@ -0,0 +1,213 @@ +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 = &resourceFreeformResourceGroup{} + _ resourceWithSetFfBpClientFunc = &resourceFreeformResourceGroup{} + _ resourceWithSetBpLockFunc = &resourceFreeformResourceGroup{} +) + +type resourceFreeformResourceGroup struct { + getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) + lockFunc func(context.Context, string) error +} + +func (o *resourceFreeformResourceGroup) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_freeform_resource_group" +} + +func (o *resourceFreeformResourceGroup) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + configureResource(ctx, o, req, resp) +} + +func (o *resourceFreeformResourceGroup) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: docCategoryFreeform + "This resource creates a Resource Allocation Group in a Freeform Blueprint.", + Attributes: blueprint.FreeformResourceGroup{}.ResourceAttributes(), + } +} + +func (o *resourceFreeformResourceGroup) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan blueprint.FreeformResourceGroup + 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.CreateRaGroup(ctx, request) + if err != nil { + resp.Diagnostics.AddError("error creating new Freeform Resource Allocation Group", err.Error()) + return + } + + plan.Id = types.StringValue(id.String()) + plan.GeneratorId = types.StringNull() + + // set state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (o *resourceFreeformResourceGroup) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state blueprint.FreeformResourceGroup + 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.GetRaGroup(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 Allocation Group", 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 *resourceFreeformResourceGroup) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Get plan values + var plan blueprint.FreeformResourceGroup + 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.UpdateRaGroup(ctx, apstra.ObjectId(plan.Id.ValueString()), request) + if err != nil { + resp.Diagnostics.AddError("error updating Freeform Resource Allocation Group", err.Error()) + return + } + + plan.GeneratorId = types.StringNull() + + // set state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (o *resourceFreeformResourceGroup) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state blueprint.FreeformResourceGroup + 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.DeleteRaGroup(ctx, apstra.ObjectId(state.Id.ValueString())) + if err != nil { + if utils.IsApstra404(err) { + return // 404 is okay + } + resp.Diagnostics.AddError("error deleting Freeform Resource Allocation Group", err.Error()) + return + } +} + +func (o *resourceFreeformResourceGroup) setBpClientFunc(f func(context.Context, string) (*apstra.FreeformClient, error)) { + o.getBpClientFunc = f +} + +func (o *resourceFreeformResourceGroup) setBpLockFunc(f func(context.Context, string) error) { + o.lockFunc = f +} diff --git a/apstra/resource_freeform_resource_group_integration_test.go b/apstra/resource_freeform_resource_group_integration_test.go new file mode 100644 index 00000000..4b79a2a8 --- /dev/null +++ b/apstra/resource_freeform_resource_group_integration_test.go @@ -0,0 +1,211 @@ +//go:build integration + +package tfapstra_test + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/Juniper/apstra-go-sdk/apstra" + "github.com/stretchr/testify/require" + + 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 ( + resourceFreeformResourceGroupHcl = ` +resource %q %q { + blueprint_id = %q + name = %q + parent_id = %s + data = %s +} +` +) + +type resourceFreeformResourceGroup struct { + blueprintId string + name string + parentId string + data json.RawMessage + // tags []string +} + +func (o resourceFreeformResourceGroup) render(rType, rName string) string { + data := "null" + if o.data != nil { + data = fmt.Sprintf("%q", string(o.data)) + } + + return fmt.Sprintf(resourceFreeformResourceGroupHcl, + rType, rName, + o.blueprintId, + o.name, + stringOrNull(o.parentId), + data, + ) +} + +func (o resourceFreeformResourceGroup) 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) + + //if len(o.tags) > 0 { + // result.append(t, "TestCheckResourceAttr", "tags.#", strconv.Itoa(len(o.tags))) + // for _, tag := range o.tags { + // result.append(t, "TestCheckTypeSetElemAttr", "tags.*", tag) + // } + //} + + if len(o.data) > 0 { + result.append(t, "TestCheckResourceAttr", "data", string(o.data)) + } else { + result.append(t, "TestCheckResourceAttr", "data", "{}") + } + + if len(o.parentId) > 0 { + result.append(t, "TestCheckResourceAttr", "parent_id", o.parentId) + } else { + result.append(t, "TestCheckNoResourceAttr", "parent_id") + } + + return result +} + +func TestResourceFreeformResourceGroup(t *testing.T) { + ctx := context.Background() + client := testutils.GetTestClient(t, ctx) + apiVersion := version.Must(version.NewVersion(client.ApiVersion())) + + // create a blueprint + bp := testutils.FfBlueprintA(t, ctx) + + makeParentResourceGroup := func(t testing.TB, ctx context.Context) apstra.ObjectId { + id, err := bp.CreateRaGroup(ctx, &apstra.FreeformRaGroupData{Label: acctest.RandString(6)}) + require.NoError(t, err) + return id + } + + type testStep struct { + config resourceFreeformResourceGroup + } + type testCase struct { + apiVersionConstraints version.Constraints + steps []testStep + } + + testCases := map[string]testCase{ + "start_minimal": { + steps: []testStep{ + { + config: resourceFreeformResourceGroup{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + }, + }, + { + config: resourceFreeformResourceGroup{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + parentId: makeParentResourceGroup(t, ctx).String(), + data: randomJson(t, 6, 12, 4), + }, + }, + { + config: resourceFreeformResourceGroup{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + }, + }, + }, + }, + "start_maximal": { + steps: []testStep{ + { + config: resourceFreeformResourceGroup{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + parentId: makeParentResourceGroup(t, ctx).String(), + data: randomJson(t, 6, 12, 4), + }, + }, + { + config: resourceFreeformResourceGroup{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + }, + }, + { + config: resourceFreeformResourceGroup{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + parentId: makeParentResourceGroup(t, ctx).String(), + data: randomJson(t, 6, 12, 4), + }, + }, + }, + }, + "swap_parents": { + steps: []testStep{ + { + config: resourceFreeformResourceGroup{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + parentId: makeParentResourceGroup(t, ctx).String(), + }, + }, + { + config: resourceFreeformResourceGroup{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + parentId: makeParentResourceGroup(t, ctx).String(), + }, + }, + }, + }, + } + + resourceType := tfapstra.ResourceName(ctx, &tfapstra.ResourceFreeformRaGroup) + + 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 ------\n%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/docs/data-sources/freeform_resource_group.md b/docs/data-sources/freeform_resource_group.md new file mode 100644 index 00000000..b5b9607c --- /dev/null +++ b/docs/data-sources/freeform_resource_group.md @@ -0,0 +1,67 @@ +--- +page_title: "apstra_freeform_resource_group Data Source - terraform-provider-apstra" +subcategory: "Reference Design: Freeform" +description: |- + This data source provides details of a specific Freeform Resource Allocation Group. + At least one optional attribute is required. +--- + +# apstra_freeform_resource_group (Data Source) + +This data source provides details of a specific Freeform Resource Allocation Group. + +At least one optional attribute is required. + + +## Example Usage + +```terraform +# This example defines a Freeform Resource Allocation Group in a blueprint + +resource "apstra_freeform_resource_group" "test" { + blueprint_id = "freeform_blueprint-d8c1fabf" + name = "test_resource_group_fizz" + data = jsonencode({ + foo = "bar" + clown = 2 + }) +} + +# here we retrieve the freeform resource_group + +data "apstra_freeform_resource_group" "test" { + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + id = apstra_freeform_resource_group.test.id +} + +# here we build an output bock to display it + +output "test_resource_group_out" {value = data.apstra_freeform_resource_group.test} + +//test_resource_group_out = { +// "blueprint_id" = "043c5787-66e8-41c7-8925-c7e52fbe6e32" +// "data" = "{\"clown\": 2, \"foo\": \"bar\"}" +// "generator_id" = tostring(null) +// "id" = "98ubU5cuRj7WsT159L4" +// "name" = "test_resource_group_fizz" +// "parent_id" = tostring(null) +//} +``` + + +## Schema + +### Required + +- `blueprint_id` (String) Apstra Blueprint ID. Used to identify the Blueprint where the Resource Allocation Group lives. + +### Optional + +- `id` (String) Populate this field to look up the Freeform Allocation Group by ID. Required when `name` is omitted. +- `name` (String) Populate this field to look up the Freeform Allocation Group by Name. Required when `id` is omitted. + +### Read-Only + +- `data` (String) Arbitrary key-value mapping that is useful in a context of this group. For example, you can store some VRF-related data there or add properties that are useful only in context of resource allocation, but not systems or interfaces. +- `generator_id` (String) ID of the group generator that created the group, if any. +- `parent_id` (String) ID of the group node that is present as a parent of the current one in a parent/child relationship. If this is a top-level (root) node, then `parent_id` will be `null`. diff --git a/docs/resources/freeform_resource_group.md b/docs/resources/freeform_resource_group.md new file mode 100644 index 00000000..103ba124 --- /dev/null +++ b/docs/resources/freeform_resource_group.md @@ -0,0 +1,67 @@ +--- +page_title: "apstra_freeform_resource_group Resource - terraform-provider-apstra" +subcategory: "Reference Design: Freeform" +description: |- + This resource creates a Resource Allocation Group in a Freeform Blueprint. +--- + +# apstra_freeform_resource_group (Resource) + +This resource creates a Resource Allocation Group in a Freeform Blueprint. + + +## Example Usage + +```terraform +# This example defines a Freeform Resource Allocation Group in a blueprint + +resource "apstra_resource_freeform_resource_group" "test" { + blueprint_id = "freeform_blueprint-d8c1fabf" + name = "test_resource_group_fizz" + data = jsonencode({ + foo = "bar" + clown = 2 + }) +} + +# here we retrieve the freeform resource_group + +data "apstra_resource_freeform_resource_group" "test" { + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + id = apstra_resource_freeform_resource_group.test.id +} + +# here we build an output bock to display it + +output "test_resource_out" {value = data.apstra_resource_freeform_resource_group.test} + +//test_resource_out = { +// "blueprint_id" = "043c5787-66e8-41c7-8925-c7e52fbe6e32" +// "data" = "{\"clown\": 2, \"foo\": \"bar\"}" +// "generator_id" = tostring(null) +// "id" = "98ubU5cuRj7WsT159L4" +// "name" = "test_resource_group_fizz" +// "parent_id" = tostring(null) +//} +``` + + +## Schema + +### Required + +- `blueprint_id` (String) Apstra Blueprint ID. +- `name` (String) Freeform Resource Allocation Group name as shown in the Web UI. + +### Optional + +- `data` (String) Arbitrary JSON-encoded key-value mapping that is useful in a context of this group. For example, you can store some VRF-related data there or add properties that are useful only in context of resource allocation, but not systems or interfaces. +- `parent_id` (String) ID of the parent Freeform Resource Allocation Group, if this group is to be nested. + +### Read-Only + +- `generator_id` (String) ID of the Generator that created Resource Allocation Group. Always `null` because groups created via resource declaration were not generated. +- `id` (String) ID of the Freeform Resource Allocation Group. + + + diff --git a/examples/data-sources/apstra_freeform_resource_group/example.tf b/examples/data-sources/apstra_freeform_resource_group/example.tf new file mode 100644 index 00000000..f5644b44 --- /dev/null +++ b/examples/data-sources/apstra_freeform_resource_group/example.tf @@ -0,0 +1,31 @@ +# This example defines a Freeform Resource Allocation Group in a blueprint + +resource "apstra_freeform_resource_group" "test" { + blueprint_id = "freeform_blueprint-d8c1fabf" + name = "test_resource_group_fizz" + data = jsonencode({ + foo = "bar" + clown = 2 + }) +} + +# here we retrieve the freeform resource_group + +data "apstra_freeform_resource_group" "test" { + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + id = apstra_freeform_resource_group.test.id +} + +# here we build an output bock to display it + +output "test_resource_group_out" {value = data.apstra_freeform_resource_group.test} + +//test_resource_group_out = { +// "blueprint_id" = "043c5787-66e8-41c7-8925-c7e52fbe6e32" +// "data" = "{\"clown\": 2, \"foo\": \"bar\"}" +// "generator_id" = tostring(null) +// "id" = "98ubU5cuRj7WsT159L4" +// "name" = "test_resource_group_fizz" +// "parent_id" = tostring(null) +//} + diff --git a/examples/resources/apstra_freeform_resource_group/example.tf b/examples/resources/apstra_freeform_resource_group/example.tf new file mode 100644 index 00000000..2594dcd7 --- /dev/null +++ b/examples/resources/apstra_freeform_resource_group/example.tf @@ -0,0 +1,30 @@ +# This example defines a Freeform Resource Allocation Group in a blueprint + +resource "apstra_resource_freeform_resource_group" "test" { + blueprint_id = "freeform_blueprint-d8c1fabf" + name = "test_resource_group_fizz" + data = jsonencode({ + foo = "bar" + clown = 2 + }) +} + +# here we retrieve the freeform resource_group + +data "apstra_resource_freeform_resource_group" "test" { + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + id = apstra_resource_freeform_resource_group.test.id +} + +# here we build an output bock to display it + +output "test_resource_out" {value = data.apstra_resource_freeform_resource_group.test} + +//test_resource_out = { +// "blueprint_id" = "043c5787-66e8-41c7-8925-c7e52fbe6e32" +// "data" = "{\"clown\": 2, \"foo\": \"bar\"}" +// "generator_id" = tostring(null) +// "id" = "98ubU5cuRj7WsT159L4" +// "name" = "test_resource_group_fizz" +// "parent_id" = tostring(null) +//} diff --git a/go.mod b/go.mod index b76ba5c5..2bb97d17 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-20240716232834-1bab0a8d9e68 + github.com/Juniper/apstra-go-sdk v0.0.0-20240722155513-01f6e5e4e91a github.com/apparentlymart/go-cidr v1.1.0 github.com/chrismarget-j/go-licenses v0.0.0-20240224210557-f22f3e06d3d4 github.com/google/go-cmp v0.6.0 diff --git a/go.sum b/go.sum index 5d490fa9..1765f45d 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-20240716232834-1bab0a8d9e68 h1:7IIlWVoCJPPAazUdL522E2TJ40OKJ+dTKKV1If/M5Qg= -github.com/Juniper/apstra-go-sdk v0.0.0-20240716232834-1bab0a8d9e68/go.mod h1:Xwj3X8v/jRZWv28o6vQAqD4lz2JmzaSYLZ2ch1SS89w= +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/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=