diff --git a/apstra/data_source_freeform_resource_group_generator.go b/apstra/data_source_freeform_resource_group_generator.go new file mode 100644 index 00000000..a881b8e8 --- /dev/null +++ b/apstra/data_source_freeform_resource_group_generator.go @@ -0,0 +1,101 @@ +package tfapstra + +import ( + "context" + "fmt" + + "github.com/Juniper/apstra-go-sdk/apstra" + "github.com/Juniper/terraform-provider-apstra/apstra/freeform" + "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 = &dataSourceFreeformGroupGenerator{} + _ datasourceWithSetFfBpClientFunc = &dataSourceFreeformGroupGenerator{} +) + +type dataSourceFreeformGroupGenerator struct { + getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) +} + +func (o *dataSourceFreeformGroupGenerator) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_freeform_resource_group_generator" +} + +func (o *dataSourceFreeformGroupGenerator) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + configureDataSource(ctx, o, req, resp) +} + +func (o *dataSourceFreeformGroupGenerator) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: docCategoryFreeform + "This data source provides details of a specific Freeform Group Generator.\n\n" + + "At least one optional attribute is required.", + Attributes: freeform.GroupGenerator{}.DataSourceAttributes(), + } +} + +func (o *dataSourceFreeformGroupGenerator) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config freeform.GroupGenerator + 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.FreeformGroupGenerator + switch { + case !config.Id.IsNull(): + api, err = bp.GetGroupGenerator(ctx, apstra.ObjectId(config.Id.ValueString())) + if utils.IsApstra404(err) { + resp.Diagnostics.AddAttributeError( + path.Root("id"), + "Freeform Group Generator not found", + fmt.Sprintf("Freeform Group Generator with ID %s not found", config.Id)) + return + } + case !config.Name.IsNull(): + api, err = bp.GetGroupGeneratorByName(ctx, config.Name.ValueString()) + if utils.IsApstra404(err) { + resp.Diagnostics.AddAttributeError( + path.Root("name"), + "Freeform Group Generator not found", + fmt.Sprintf("Freeform Group Generator with Name %s not found", config.Name)) + return + } + } + if err != nil { + resp.Diagnostics.AddError("failed reading Freeform Group Generator", err.Error()) + return + } + if api.Data == nil { + resp.Diagnostics.AddError("failed reading Freeform Group Generator", "api response has no payload") + return + } + + config.Id = types.StringValue(api.Id.String()) + config.LoadApiData(ctx, api.Data, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // Set state + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) +} + +func (o *dataSourceFreeformGroupGenerator) 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 e134e71c..6a83a741 100644 --- a/apstra/export_test.go +++ b/apstra/export_test.go @@ -21,6 +21,7 @@ var ( ResourceFreeformBlueprint = resourceFreeformBlueprint{} ResourceFreeformConfigTemplate = resourceFreeformConfigTemplate{} ResourceFreeformDeviceProfile = resourceFreeformDeviceProfile{} + ResourceFreeformGroupGenerator = resourceFreeformGroupGenerator{} ResourceFreeformLink = resourceFreeformLink{} ResourceFreeformPropertySet = resourceFreeformPropertySet{} ResourceFreeformResourceGenerator = resourceFreeformResourceGenerator{} diff --git a/apstra/freeform/resource_group_generator.go b/apstra/freeform/resource_group_generator.go new file mode 100644 index 00000000..25753639 --- /dev/null +++ b/apstra/freeform/resource_group_generator.go @@ -0,0 +1,114 @@ +package freeform + +import ( + "context" + "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 GroupGenerator struct { + Id types.String `tfsdk:"id"` + BlueprintId types.String `tfsdk:"blueprint_id"` + GroupId types.String `tfsdk:"group_id"` + Name types.String `tfsdk:"name"` + Scope types.String `tfsdk:"scope"` +} + +func (o GroupGenerator) DataSourceAttributes() map[string]dataSourceSchema.Attribute { + return map[string]dataSourceSchema.Attribute{ + "id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Populate this field to look up the Freeform Group Generator by ID. Required when `name` is omitted.", + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + stringvalidator.ExactlyOneOf(path.Expressions{ + path.MatchRelative(), + path.MatchRoot("name"), + }...), + }, + }, + "blueprint_id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Apstra Blueprint ID. Used to identify " + + "the Blueprint where the Group lives.", + Required: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "group_id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Resource Group the Group Generator belongs to.", + Computed: true, + }, + "name": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Populate this field to look up Group Generator by Name. Required when `id` is omitted.", + Optional: true, + Computed: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "scope": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Scope is a graph query which selects target nodes for which Groups should be generated.\n" + + "Example: `node('system', name='target', label=aeq('*prod*'))`", + Computed: true, + }, + } +} + +func (o GroupGenerator) ResourceAttributes() map[string]resourceSchema.Attribute { + return map[string]resourceSchema.Attribute{ + "id": resourceSchema.StringAttribute{ + MarkdownDescription: "ID of the Group Generator within the Freeform Blueprint.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "blueprint_id": resourceSchema.StringAttribute{ + MarkdownDescription: "Apstra Blueprint ID.", + Required: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "group_id": resourceSchema.StringAttribute{ + MarkdownDescription: "Resource Group the Group Generator belongs to. Omit to create at the `root` level.", + Optional: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "name": resourceSchema.StringAttribute{ + MarkdownDescription: "Freeform Group Generator name as shown in the Web UI.", + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile("^[a-zA-Z0-9.-_]+$"), + "name may consist only of the following characters : a-zA-Z0-9.-_", + ), + }, + }, + "scope": resourceSchema.StringAttribute{ + MarkdownDescription: "Scope is a graph query which selects target nodes for which Group Generators should " + + "be generated.\nExample: `node('system', name='target', label=aeq('*prod*'))`", + Required: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + } +} + +func (o *GroupGenerator) Request(_ context.Context, _ *diag.Diagnostics) *apstra.FreeformGroupGeneratorData { + return &apstra.FreeformGroupGeneratorData{ + ParentId: (*apstra.ObjectId)(o.GroupId.ValueStringPointer()), + Label: o.Name.ValueString(), + Scope: o.Scope.ValueString(), + } +} + +func (o *GroupGenerator) LoadApiData(_ context.Context, in *apstra.FreeformGroupGeneratorData, _ *diag.Diagnostics) { + o.GroupId = types.StringPointerValue((*string)(in.ParentId)) + o.Name = types.StringValue(in.Label) + o.Scope = types.StringValue(in.Scope) +} diff --git a/apstra/provider.go b/apstra/provider.go index d91ecdee..570b3c9c 100644 --- a/apstra/provider.go +++ b/apstra/provider.go @@ -555,6 +555,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource func() datasource.DataSource { return &dataSourceFreeformAllocGroup{} }, func() datasource.DataSource { return &dataSourceFreeformBlueprint{} }, func() datasource.DataSource { return &dataSourceFreeformConfigTemplate{} }, + func() datasource.DataSource { return &dataSourceFreeformGroupGenerator{} }, func() datasource.DataSource { return &dataSourceFreeformLink{} }, func() datasource.DataSource { return &dataSourceFreeformPropertySet{} }, func() datasource.DataSource { return &dataSourceFreeformResourceGenerator{} }, @@ -623,6 +624,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { func() resource.Resource { return &resourceFreeformBlueprint{} }, func() resource.Resource { return &resourceFreeformConfigTemplate{} }, func() resource.Resource { return &resourceFreeformDeviceProfile{} }, + func() resource.Resource { return &resourceFreeformGroupGenerator{} }, func() resource.Resource { return &resourceFreeformLink{} }, func() resource.Resource { return &resourceFreeformPropertySet{} }, func() resource.Resource { return &resourceFreeformResourceGenerator{} }, diff --git a/apstra/resource_freeform_resource_generator.go b/apstra/resource_freeform_resource_generator.go index 8e761d97..aa00cb30 100644 --- a/apstra/resource_freeform_resource_generator.go +++ b/apstra/resource_freeform_resource_generator.go @@ -223,7 +223,7 @@ func (o *resourceFreeformResourceGenerator) Delete(ctx context.Context, req reso return } - // Delete Config Template by calling API + // Delete Resource Generator by calling API err = bp.DeleteResourceGenerator(ctx, apstra.ObjectId(state.Id.ValueString())) if err != nil { if utils.IsApstra404(err) { diff --git a/apstra/resource_freeform_resource_group_generator.go b/apstra/resource_freeform_resource_group_generator.go new file mode 100644 index 00000000..f51eaa3e --- /dev/null +++ b/apstra/resource_freeform_resource_group_generator.go @@ -0,0 +1,212 @@ +package tfapstra + +import ( + "context" + "fmt" + + "github.com/Juniper/apstra-go-sdk/apstra" + "github.com/Juniper/terraform-provider-apstra/apstra/freeform" + "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 = &resourceFreeformGroupGenerator{} + _ resourceWithSetFfBpClientFunc = &resourceFreeformGroupGenerator{} + _ resourceWithSetBpLockFunc = &resourceFreeformGroupGenerator{} +) + +type resourceFreeformGroupGenerator struct { + getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) + lockFunc func(context.Context, string) error +} + +func (o *resourceFreeformGroupGenerator) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_freeform_resource_group_generator" +} + +func (o *resourceFreeformGroupGenerator) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + configureResource(ctx, o, req, resp) +} + +func (o *resourceFreeformGroupGenerator) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: docCategoryFreeform + "This resource creates a Group Generator in a Freeform Blueprint.", + Attributes: freeform.GroupGenerator{}.ResourceAttributes(), + } +} + +func (o *resourceFreeformGroupGenerator) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan freeform.GroupGenerator + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // get a client for the Freeform reference design + bp, err := o.getBpClientFunc(ctx, plan.BlueprintId.ValueString()) + if err != nil { + if utils.IsApstra404(err) { + resp.Diagnostics.AddError(fmt.Sprintf("blueprint %s not found", plan.BlueprintId), err.Error()) + return + } + resp.Diagnostics.AddError("failed to create blueprint client", err.Error()) + return + } + + // Lock the blueprint mutex. + err = o.lockFunc(ctx, plan.BlueprintId.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("error locking blueprint %q mutex", plan.BlueprintId.ValueString()), + err.Error()) + return + } + + // Convert the plan into an API Request + request := plan.Request(ctx, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // Create the resource + id, err := bp.CreateGroupGenerator(ctx, request) + if err != nil { + resp.Diagnostics.AddError("error creating new Group Generator", err.Error()) + return + } + + // set state + plan.Id = types.StringValue(id.String()) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (o *resourceFreeformGroupGenerator) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state freeform.GroupGenerator + 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.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("failed to create blueprint client", err.Error()) + return + } + + api, err := bp.GetGroupGenerator(ctx, apstra.ObjectId(state.Id.ValueString())) + if err != nil { + if utils.IsApstra404(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("Error retrieving Freeform Group Generator", 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 *resourceFreeformGroupGenerator) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Get plan values + var plan freeform.GroupGenerator + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // get a client for the Freeform reference design + bp, err := o.getBpClientFunc(ctx, plan.BlueprintId.ValueString()) + if err != nil { + if utils.IsApstra404(err) { + resp.Diagnostics.AddError(fmt.Sprintf("blueprint %s not found", plan.BlueprintId), err.Error()) + return + } + resp.Diagnostics.AddError("failed to create blueprint client", err.Error()) + return + } + + // Lock the blueprint mutex. + err = o.lockFunc(ctx, plan.BlueprintId.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("error locking blueprint %q mutex", plan.BlueprintId.ValueString()), + err.Error()) + return + } + + // Convert the plan into an API Request + request := plan.Request(ctx, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // Update the Group + err = bp.UpdateGroupGenerator(ctx, apstra.ObjectId(plan.Id.ValueString()), request) + if err != nil { + resp.Diagnostics.AddError("error updating Freeform Group Generator", err.Error()) + return + } + + // set state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (o *resourceFreeformGroupGenerator) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state freeform.GroupGenerator + 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 Resource Generator by calling API + err = bp.DeleteGroupGenerator(ctx, apstra.ObjectId(state.Id.ValueString())) + if err != nil { + if utils.IsApstra404(err) { + return // 404 is okay + } + resp.Diagnostics.AddError("error deleting Freeform Group", err.Error()) + return + } +} + +func (o *resourceFreeformGroupGenerator) setBpClientFunc(f func(context.Context, string) (*apstra.FreeformClient, error)) { + o.getBpClientFunc = f +} + +func (o *resourceFreeformGroupGenerator) setBpLockFunc(f func(context.Context, string) error) { + o.lockFunc = f +} diff --git a/apstra/resource_freeform_resource_group_generator_integration_test.go b/apstra/resource_freeform_resource_group_generator_integration_test.go new file mode 100644 index 00000000..b451bca8 --- /dev/null +++ b/apstra/resource_freeform_resource_group_generator_integration_test.go @@ -0,0 +1,239 @@ +//go:build integration + +package tfapstra_test + +import ( + "context" + "fmt" + "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" + "github.com/stretchr/testify/require" +) + +const ( + resourceFreeformGroupGeneratorHcl = ` +resource %q %q { + blueprint_id = %q + group_id = %s + name = %q + scope = %q + } +` +) + +type resourceFreeformGroupGenerator struct { + blueprintId string + groupId *apstra.ObjectId + name string + scope string +} + +func (o resourceFreeformGroupGenerator) render(rType, rName string) string { + return fmt.Sprintf(resourceFreeformGroupGeneratorHcl, + rType, rName, + o.blueprintId, + stringPtrOrNull(o.groupId), + o.name, + o.scope, + ) +} + +func (o resourceFreeformGroupGenerator) testChecks(t testing.TB, rType, rName string) testChecks { + result := newTestChecks(rType + "." + rName) + + // required and computed attributes can always be checked + result.append(t, "TestCheckResourceAttrSet", "id") + result.append(t, "TestCheckResourceAttr", "blueprint_id", o.blueprintId) + result.append(t, "TestCheckResourceAttr", "name", o.name) + result.append(t, "TestCheckResourceAttr", "scope", o.scope) + + if o.groupId == nil { + result.append(t, "TestCheckNoResourceAttr", "group_id") + } else { + result.append(t, "TestCheckResourceAttr", "group_id", o.groupId.String()) + } + + return result +} + +func TestResourceFreeformGroupGenerator(t *testing.T) { + ctx := context.Background() + client := testutils.GetTestClient(t, ctx) + apiVersion := version.Must(version.NewVersion(client.ApiVersion())) + + resourceGroupCount := 2 + + // create a blueprint + bp := testutils.FfBlueprintA(t, ctx) + + var err error + + resourceGroupIds := make([]apstra.ObjectId, resourceGroupCount) + for i := range resourceGroupCount { + resourceGroupIds[i], err = bp.CreateRaGroup(ctx, &apstra.FreeformRaGroupData{Label: acctest.RandString(6)}) + require.NoError(t, err) + } + + type testStep struct { + config resourceFreeformGroupGenerator + } + + type testCase struct { + apiVersionConstraints version.Constraints + steps []testStep + } + + testCases := map[string]testCase{ + "root": { + steps: []testStep{ + { + config: resourceFreeformGroupGenerator{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + scope: fmt.Sprintf(`node('system', label='%s', name='target')`, acctest.RandString(6)), + }, + }, + { + config: resourceFreeformGroupGenerator{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + scope: fmt.Sprintf(`node('system', label='%s', name='target')`, acctest.RandString(6)), + }, + }, + }, + }, + "group": { + steps: []testStep{ + { + config: resourceFreeformGroupGenerator{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + scope: fmt.Sprintf(`node('system', label='%s', name='target')`, acctest.RandString(6)), + groupId: &resourceGroupIds[0], + }, + }, + { + config: resourceFreeformGroupGenerator{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + scope: fmt.Sprintf(`node('system', label='%s', name='target')`, acctest.RandString(6)), + groupId: &resourceGroupIds[0], + }, + }, + }, + }, + "change_group": { + steps: []testStep{ + { + config: resourceFreeformGroupGenerator{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + scope: fmt.Sprintf(`node('system', label='%s', name='target')`, acctest.RandString(6)), + groupId: &resourceGroupIds[0], + }, + }, + { + config: resourceFreeformGroupGenerator{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + scope: fmt.Sprintf(`node('system', label='%s', name='target')`, acctest.RandString(6)), + groupId: &resourceGroupIds[1], + }, + }, + }, + }, + "root_then_group": { + steps: []testStep{ + { + config: resourceFreeformGroupGenerator{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + scope: fmt.Sprintf(`node('system', label='%s', name='target')`, acctest.RandString(6)), + }, + }, + { + config: resourceFreeformGroupGenerator{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + scope: fmt.Sprintf(`node('system', label='%s', name='target')`, acctest.RandString(6)), + groupId: &resourceGroupIds[1], + }, + }, + { + config: resourceFreeformGroupGenerator{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + scope: fmt.Sprintf(`node('system', label='%s', name='target')`, acctest.RandString(6)), + }, + }, + }, + }, + "group_then_root": { + steps: []testStep{ + { + config: resourceFreeformGroupGenerator{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + scope: fmt.Sprintf(`node('system', label='%s', name='target')`, acctest.RandString(6)), + groupId: &resourceGroupIds[1], + }, + }, + { + config: resourceFreeformGroupGenerator{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + scope: fmt.Sprintf(`node('system', label='%s', name='target')`, acctest.RandString(6)), + }, + }, + { + config: resourceFreeformGroupGenerator{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + scope: fmt.Sprintf(`node('system', label='%s', name='target')`, acctest.RandString(6)), + groupId: &resourceGroupIds[1], + }, + }, + }, + }, + } + + resourceType := tfapstra.ResourceName(ctx, &tfapstra.ResourceFreeformGroupGenerator) + + 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/test_helpers_test.go b/apstra/test_helpers_test.go index a66baaa6..e883126e 100644 --- a/apstra/test_helpers_test.go +++ b/apstra/test_helpers_test.go @@ -56,11 +56,11 @@ func systemIds(ctx context.Context, t *testing.T, bp *apstra.TwoStageL3ClosClien return ids } -func stringPtrOrNull(in *string) string { +func stringPtrOrNull[S ~string](in *S) string { if in == nil { return "null" } - return `"` + *in + `"` + return fmt.Sprintf(`%q`, *in) } func stringOrNull(in string) string { diff --git a/docs/data-sources/freeform_resource_group_generator.md b/docs/data-sources/freeform_resource_group_generator.md new file mode 100644 index 00000000..e00a5790 --- /dev/null +++ b/docs/data-sources/freeform_resource_group_generator.md @@ -0,0 +1,75 @@ +--- +page_title: "apstra_freeform_resource_group_generator Data Source - terraform-provider-apstra" +subcategory: "Reference Design: Freeform" +description: |- + This data source provides details of a specific Freeform Group Generator. + At least one optional attribute is required. +--- + +# apstra_freeform_resource_group_generator (Data Source) + +This data source provides details of a specific Freeform Group Generator. + +At least one optional attribute is required. + + +## Example Usage + +```terraform +# This example creates a Resource Group Generator under a +# Resource Group in a Freeform Blueprint. +# +# After creating the Group Generator, the data source is invoked to look up +# the details. + +# Create a resource group in a preexisting blueprint. +resource "apstra_freeform_resource_group" "fizz_grp" { + blueprint_id = "f1b86583-9139-49ed-8a3c-0490253e006e" + name = "fizz_grp" +} + +# Create a resource generator scoped to target all systems in the blueprint. +resource "apstra_freeform_resource_group_generator" "test_group_gen" { + blueprint_id = "f1b86583-9139-49ed-8a3c-0490253e006e" + group_id = apstra_freeform_resource_group.fizz_grp.id + name = "test_res_gen" + scope = "node('system', name='target')" +} + +# Invoke the resource group generator data source +data "apstra_freeform_resource_group_generator" "test_group_gen" { + blueprint_id = "f1b86583-9139-49ed-8a3c-0490253e006e" + id = apstra_freeform_resource_group_generator.test_group_gen.id +} + +# Output the data source so that it prints on screen +output "test_resource_group_generator_out" { + value = data.apstra_freeform_resource_group_generator.test_group_gen +} + +# The output looks like this: +# test_resource_group_generator_out = { +# "blueprint_id" = "f1b86583-9139-49ed-8a3c-0490253e006e" +# "id" = "wOQS9qZezzRJCgyvxwY" +# "name" = "test_res_gen" +# "scope" = "node('system', name='target')" +# } +``` + + +## Schema + +### Required + +- `blueprint_id` (String) Apstra Blueprint ID. Used to identify the Blueprint where the Group lives. + +### Optional + +- `id` (String) Populate this field to look up the Freeform Group Generator by ID. Required when `name` is omitted. +- `name` (String) Populate this field to look up Group Generator by Name. Required when `id` is omitted. + +### Read-Only + +- `group_id` (String) Resource Group the Group Generator belongs to. +- `scope` (String) Scope is a graph query which selects target nodes for which Groups should be generated. +Example: `node('system', name='target', label=aeq('*prod*'))` diff --git a/docs/resources/freeform_resource_group_generator.md b/docs/resources/freeform_resource_group_generator.md new file mode 100644 index 00000000..10effc06 --- /dev/null +++ b/docs/resources/freeform_resource_group_generator.md @@ -0,0 +1,75 @@ +--- +page_title: "apstra_freeform_resource_group_generator Resource - terraform-provider-apstra" +subcategory: "Reference Design: Freeform" +description: |- + This resource creates a Group Generator in a Freeform Blueprint. +--- + +# apstra_freeform_resource_group_generator (Resource) + +This resource creates a Group Generator in a Freeform Blueprint. + + +## Example Usage + +```terraform +# This example creates a Resource Group Generator under a +# Resource Group in a Freeform Blueprint. +# +# After creating the Group Generator, the data source is invoked to look up +# the details. + +# Create a resource group in a preexisting blueprint. +resource "apstra_freeform_resource_group" "fizz_grp" { + blueprint_id = "f1b86583-9139-49ed-8a3c-0490253e006e" + name = "fizz_grp" +} + +# Create a resource generator scoped to target all systems in the blueprint. +resource "apstra_freeform_resource_group_generator" "test_group_gen" { + blueprint_id = "f1b86583-9139-49ed-8a3c-0490253e006e" + group_id = apstra_freeform_resource_group.fizz_grp.id + name = "test_res_gen" + scope = "node('system', name='target')" +} + +# Invoke the resource group generator data source +data "apstra_freeform_resource_group_generator" "test_group_gen" { + blueprint_id = "f1b86583-9139-49ed-8a3c-0490253e006e" + id = apstra_freeform_resource_group_generator.test_group_gen.id +} + +# Output the data source so that it prints on screen +output "test_resource_group_generator_out" { + value = data.apstra_freeform_resource_group_generator.test_group_gen +} + +# The output looks like this: +# test_resource_group_generator_out = { +# "blueprint_id" = "f1b86583-9139-49ed-8a3c-0490253e006e" +# "id" = "wOQS9qZezzRJCgyvxwY" +# "name" = "test_res_gen" +# "scope" = "node('system', name='target')" +# } +``` + + +## Schema + +### Required + +- `blueprint_id` (String) Apstra Blueprint ID. +- `name` (String) Freeform Group Generator name as shown in the Web UI. +- `scope` (String) Scope is a graph query which selects target nodes for which Group Generators should be generated. +Example: `node('system', name='target', label=aeq('*prod*'))` + +### Optional + +- `group_id` (String) Resource Group the Group Generator belongs to. Omit to create at the `root` level. + +### Read-Only + +- `id` (String) ID of the Group Generator within the Freeform Blueprint. + + + diff --git a/examples/data-sources/apstra_freeform_resource_group_generator/example.tf b/examples/data-sources/apstra_freeform_resource_group_generator/example.tf new file mode 100644 index 00000000..aac9d850 --- /dev/null +++ b/examples/data-sources/apstra_freeform_resource_group_generator/example.tf @@ -0,0 +1,39 @@ +# This example creates a Resource Group Generator under a +# Resource Group in a Freeform Blueprint. +# +# After creating the Group Generator, the data source is invoked to look up +# the details. + +# Create a resource group in a preexisting blueprint. +resource "apstra_freeform_resource_group" "fizz_grp" { + blueprint_id = "f1b86583-9139-49ed-8a3c-0490253e006e" + name = "fizz_grp" +} + +# Create a resource generator scoped to target all systems in the blueprint. +resource "apstra_freeform_resource_group_generator" "test_group_gen" { + blueprint_id = "f1b86583-9139-49ed-8a3c-0490253e006e" + group_id = apstra_freeform_resource_group.fizz_grp.id + name = "test_res_gen" + scope = "node('system', name='target')" +} + +# Invoke the resource group generator data source +data "apstra_freeform_resource_group_generator" "test_group_gen" { + blueprint_id = "f1b86583-9139-49ed-8a3c-0490253e006e" + id = apstra_freeform_resource_group_generator.test_group_gen.id +} + +# Output the data source so that it prints on screen +output "test_resource_group_generator_out" { + value = data.apstra_freeform_resource_group_generator.test_group_gen +} + +# The output looks like this: +# test_resource_group_generator_out = { +# "blueprint_id" = "f1b86583-9139-49ed-8a3c-0490253e006e" +# "id" = "wOQS9qZezzRJCgyvxwY" +# "name" = "test_res_gen" +# "scope" = "node('system', name='target')" +# } + diff --git a/examples/resources/apstra_freeform_resource_group_generator/example.tf b/examples/resources/apstra_freeform_resource_group_generator/example.tf new file mode 100644 index 00000000..aac9d850 --- /dev/null +++ b/examples/resources/apstra_freeform_resource_group_generator/example.tf @@ -0,0 +1,39 @@ +# This example creates a Resource Group Generator under a +# Resource Group in a Freeform Blueprint. +# +# After creating the Group Generator, the data source is invoked to look up +# the details. + +# Create a resource group in a preexisting blueprint. +resource "apstra_freeform_resource_group" "fizz_grp" { + blueprint_id = "f1b86583-9139-49ed-8a3c-0490253e006e" + name = "fizz_grp" +} + +# Create a resource generator scoped to target all systems in the blueprint. +resource "apstra_freeform_resource_group_generator" "test_group_gen" { + blueprint_id = "f1b86583-9139-49ed-8a3c-0490253e006e" + group_id = apstra_freeform_resource_group.fizz_grp.id + name = "test_res_gen" + scope = "node('system', name='target')" +} + +# Invoke the resource group generator data source +data "apstra_freeform_resource_group_generator" "test_group_gen" { + blueprint_id = "f1b86583-9139-49ed-8a3c-0490253e006e" + id = apstra_freeform_resource_group_generator.test_group_gen.id +} + +# Output the data source so that it prints on screen +output "test_resource_group_generator_out" { + value = data.apstra_freeform_resource_group_generator.test_group_gen +} + +# The output looks like this: +# test_resource_group_generator_out = { +# "blueprint_id" = "f1b86583-9139-49ed-8a3c-0490253e006e" +# "id" = "wOQS9qZezzRJCgyvxwY" +# "name" = "test_res_gen" +# "scope" = "node('system', name='target')" +# } +