Skip to content

Commit

Permalink
Merge pull request #1028 from Juniper/1026-add-support-for-section_co…
Browse files Browse the repository at this point in the history
…ndition-to-apstra_datacenter_configlet-resource

Add support for `section_condition` to `apstra_datacenter_configlet` data source and resource
  • Loading branch information
chrismarget-j authored Feb 2, 2025
2 parents 564800d + 4c40b80 commit 012f942
Show file tree
Hide file tree
Showing 17 changed files with 1,023 additions and 341 deletions.
73 changes: 51 additions & 22 deletions apstra/blueprint/configlet.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package blueprint

import (
"context"

"github.com/Juniper/apstra-go-sdk/apstra"
"github.com/Juniper/terraform-provider-apstra/apstra/design"
"github.com/Juniper/terraform-provider-apstra/apstra/utils"
apstravalidator "github.com/Juniper/terraform-provider-apstra/apstra/validator"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
dataSourceSchema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
Expand All @@ -20,9 +22,9 @@ import (
type DatacenterConfiglet struct {
BlueprintId types.String `tfsdk:"blueprint_id"`
Id types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Condition types.String `tfsdk:"condition"`
CatalogConfigletID types.String `tfsdk:"catalog_configlet_id"`
Name types.String `tfsdk:"name"`
Generators types.List `tfsdk:"generators"`
}

Expand Down Expand Up @@ -58,14 +60,14 @@ func (o DatacenterConfiglet) DataSourceAttributes() map[string]dataSourceSchema.
Computed: true,
},
"catalog_configlet_id": dataSourceSchema.StringAttribute{
MarkdownDescription: "Will be null in the data source",
MarkdownDescription: "This attribute is always `null` in data source context. Ignore.",
Computed: true,
},
"generators": dataSourceSchema.ListNestedAttribute{
MarkdownDescription: "Ordered list of Generators",
Computed: true,
NestedObject: dataSourceSchema.NestedAttributeObject{
Attributes: design.ConfigletGenerator{}.DataSourceAttributesNested(),
Attributes: ConfigletGenerator{}.DataSourceAttributes(),
},
},
}
Expand All @@ -85,12 +87,15 @@ func (o DatacenterConfiglet) ResourceAttributes() map[string]resourceSchema.Attr
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
},
"name": resourceSchema.StringAttribute{
MarkdownDescription: "Configlet name. When omitted, the name found in the catalog configlet will be used." +
" Required when the `generators` attribute is specified.",
MarkdownDescription: "Configlet name. When omitted, the name found in the catalog configlet will be used. " +
"Required when the `generators` attribute is specified.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
apstravalidator.RequiredWhenValueNull(path.MatchRoot("catalog_configlet_id")),
},
},
"condition": resourceSchema.StringAttribute{
MarkdownDescription: "Condition determines where the Configlet is applied.",
Expand Down Expand Up @@ -120,7 +125,7 @@ func (o DatacenterConfiglet) ResourceAttributes() map[string]resourceSchema.Attr
Optional: true,
Computed: true,
NestedObject: resourceSchema.NestedAttributeObject{
Attributes: design.ConfigletGenerator{}.ResourceAttributesNested(),
Attributes: ConfigletGenerator{}.ResourceAttributes(),
},
PlanModifiers: []planmodifier.List{listplanmodifier.UseStateForUnknown()},
Validators: []validator.List{listvalidator.SizeAtLeast(1)},
Expand All @@ -129,31 +134,55 @@ func (o DatacenterConfiglet) ResourceAttributes() map[string]resourceSchema.Attr
}

func (o *DatacenterConfiglet) LoadApiData(ctx context.Context, in *apstra.TwoStageL3ClosConfigletData, diags *diag.Diagnostics) {
var configlet design.Configlet
configlet.LoadApiData(ctx, in.Data, diags)
if diags.HasError() {
return
generators := make([]ConfigletGenerator, len(in.Data.Generators))
for i, generator := range in.Data.Generators {
generators[i].LoadApiData(ctx, &generator, diags)
}

o.Condition = types.StringValue(in.Condition)
o.Name = types.StringValue(in.Label)
o.Generators = configlet.Generators
o.Generators = utils.ListValueOrNull(ctx, types.ObjectType{AttrTypes: ConfigletGenerator{}.AttrTypes()}, generators, diags)
}

func (o *DatacenterConfiglet) Request(ctx context.Context, diags *diag.Diagnostics) *apstra.TwoStageL3ClosConfigletData {
var c apstra.TwoStageL3ClosConfigletData
func (o *DatacenterConfiglet) LoadCatalogConfigletData(ctx context.Context, in *apstra.ConfigletData, diags *diag.Diagnostics) {
if o.Name.IsUnknown() {
o.Name = types.StringValue(in.DisplayName)
}

generators := make([]ConfigletGenerator, len(in.Generators))
for i, generator := range in.Generators {
generators[i].LoadApiData(ctx, &generator, diags)
}
if diags.HasError() {
return
}

c.Label = o.Name.ValueString()
c.Condition = o.Condition.ValueString()
o.Generators = utils.ListValueOrNull(ctx, types.ObjectType{AttrTypes: ConfigletGenerator{}.AttrTypes()}, generators, diags)
if diags.HasError() {
return
}
}

cfglet := design.Configlet{
Name: o.Name,
Generators: o.Generators,
func (o *DatacenterConfiglet) Request(ctx context.Context, diags *diag.Diagnostics) *apstra.TwoStageL3ClosConfigletData {
result := apstra.TwoStageL3ClosConfigletData{
Label: o.Name.ValueString(),
Condition: o.Condition.ValueString(),
Data: &apstra.ConfigletData{
DisplayName: o.Name.ValueString(),
Generators: nil, // handled below
},
}
c.Data = cfglet.Request(ctx, diags)

var generators []ConfigletGenerator
diags.Append(o.Generators.ElementsAs(ctx, &generators, false)...)
if diags.HasError() {
return nil
}
return &c

result.Data.Generators = make([]apstra.ConfigletGenerator, len(generators))
for i, generator := range generators {
result.Data.Generators[i] = *generator.Request(ctx, diags)
}

return &result
}
217 changes: 217 additions & 0 deletions apstra/blueprint/configlet_generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package blueprint

import (
"context"
"fmt"
"regexp"
"strings"

"github.com/Juniper/apstra-go-sdk/apstra"
"github.com/Juniper/apstra-go-sdk/apstra/enum"
"github.com/Juniper/terraform-provider-apstra/apstra/utils"
apstravalidator "github.com/Juniper/terraform-provider-apstra/apstra/validator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
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/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
)

type ConfigletGenerator struct {
ConfigStyle types.String `tfsdk:"config_style"`
Section types.String `tfsdk:"section"`
SectionCondition types.String `tfsdk:"section_condition"`
TemplateText types.String `tfsdk:"template_text"`
NegationTemplateText types.String `tfsdk:"negation_template_text"`
FileName types.String `tfsdk:"filename"`
}

func (o ConfigletGenerator) DataSourceAttributes() map[string]dataSourceSchema.Attribute {
return map[string]dataSourceSchema.Attribute{
"config_style": dataSourceSchema.StringAttribute{
MarkdownDescription: "Indicates Platform Specific Configuration Style",
Computed: true,
},
"section": dataSourceSchema.StringAttribute{
MarkdownDescription: "Config Section",
Computed: true,
},
"section_condition": dataSourceSchema.StringAttribute{
MarkdownDescription: "Section Condition",
Computed: true,
},
"template_text": dataSourceSchema.StringAttribute{
MarkdownDescription: "Template Text",
Computed: true,
},
"negation_template_text": dataSourceSchema.StringAttribute{
MarkdownDescription: "Negation Template Text",
Computed: true,
},
"filename": dataSourceSchema.StringAttribute{
MarkdownDescription: "FileName",
Computed: true,
},
}
}

func (o ConfigletGenerator) ResourceAttributes() map[string]resourceSchema.Attribute {
return map[string]resourceSchema.Attribute{
"config_style": resourceSchema.StringAttribute{
MarkdownDescription: fmt.Sprintf("Specifies the switch platform, must be one of '%s'.",
strings.Join(utils.AllPlatformOSNames(), "', '")),
Required: true,
Validators: []validator.String{stringvalidator.OneOf(utils.AllPlatformOSNames()...)},
},
"section": resourceSchema.StringAttribute{
MarkdownDescription: fmt.Sprintf("Specifies where in the target device the configlet should be "+
"applied. Varies by network OS:\n\n%s", utils.ValidSectionsAsTable()),
Required: true,
Validators: []validator.String{stringvalidator.OneOf(utils.AllConfigletSectionNames()...)},
},
"section_condition": resourceSchema.StringAttribute{
MarkdownDescription: fmt.Sprintf("Used to select interfaces for configlets used in sections "+
"`%s`, `%s` and `%s`. See references to *Advanced Condition Editor* in the [Apstra User Guide]"+
"(https://www.juniper.net/documentation/us/en/software/apstra5.0/apstra-user-guide/topics/task/configlet-import-blueprint.html). "+
"e.g. `role in [\"spine_leaf\"]`",
utils.StringersToFriendlyString(enum.ConfigletSectionInterface),
utils.StringersToFriendlyString(enum.ConfigletSectionSetBasedInterface),
utils.StringersToFriendlyString(enum.ConfigletSectionDeleteBasedInterface),
),
Optional: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
apstravalidator.ForbiddenWhenValueIs(
path.MatchRelative().AtParent().AtName("section"),
types.StringValue(utils.StringersToFriendlyString(enum.ConfigletSectionFile)),
),
apstravalidator.ForbiddenWhenValueIs(
path.MatchRelative().AtParent().AtName("section"),
types.StringValue(utils.StringersToFriendlyString(enum.ConfigletSectionFrr)),
),
apstravalidator.ForbiddenWhenValueIs(
path.MatchRelative().AtParent().AtName("section"),
types.StringValue(utils.StringersToFriendlyString(enum.ConfigletSectionOspf)),
),
apstravalidator.ForbiddenWhenValueIs(
path.MatchRelative().AtParent().AtName("section"),
types.StringValue(utils.StringersToFriendlyString(enum.ConfigletSectionSetBasedSystem)),
),
apstravalidator.ForbiddenWhenValueIs(
path.MatchRelative().AtParent().AtName("section"),
types.StringValue(utils.StringersToFriendlyString(enum.ConfigletSectionSystem)),
),
apstravalidator.ForbiddenWhenValueIs(
path.MatchRelative().AtParent().AtName("section"),
types.StringValue(utils.StringersToFriendlyString(enum.ConfigletSectionSystemTop)),
),
},
},
"template_text": resourceSchema.StringAttribute{
MarkdownDescription: "Template Text",
Required: true,
Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
},
"negation_template_text": resourceSchema.StringAttribute{
MarkdownDescription: "Negation Template Text",
Optional: true,
Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
},
"filename": resourceSchema.StringAttribute{
MarkdownDescription: "FileName",
Optional: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
stringvalidator.RegexMatches(regexp.MustCompile("^/etc/"), "Only files in /etc/ are supported for configlets"),

// required by section file
apstravalidator.RequiredWhenValueIs(
path.MatchRelative().AtParent().AtName("section"),
types.StringValue(utils.StringersToFriendlyString(enum.ConfigletSectionFile)),
),

// incompatible with sections other than file
apstravalidator.ForbiddenWhenValueIs(
path.MatchRelative().AtParent().AtName("section"),
types.StringValue(utils.StringersToFriendlyString(enum.ConfigletSectionDeleteBasedInterface)),
),
apstravalidator.ForbiddenWhenValueIs(
path.MatchRelative().AtParent().AtName("section"),
types.StringValue(utils.StringersToFriendlyString(enum.ConfigletSectionInterface)),
),
apstravalidator.ForbiddenWhenValueIs(
path.MatchRelative().AtParent().AtName("section"),
types.StringValue(utils.StringersToFriendlyString(enum.ConfigletSectionFrr)),
),
apstravalidator.ForbiddenWhenValueIs(
path.MatchRelative().AtParent().AtName("section"),
types.StringValue(utils.StringersToFriendlyString(enum.ConfigletSectionOspf)),
),
apstravalidator.ForbiddenWhenValueIs(
path.MatchRelative().AtParent().AtName("section"),
types.StringValue(utils.StringersToFriendlyString(enum.ConfigletSectionSetBasedInterface)),
),
apstravalidator.ForbiddenWhenValueIs(
path.MatchRelative().AtParent().AtName("section"),
types.StringValue(utils.StringersToFriendlyString(enum.ConfigletSectionSetBasedSystem)),
),
apstravalidator.ForbiddenWhenValueIs(
path.MatchRelative().AtParent().AtName("section"),
types.StringValue(utils.StringersToFriendlyString(enum.ConfigletSectionSystem)),
),
apstravalidator.ForbiddenWhenValueIs(
path.MatchRelative().AtParent().AtName("section"),
types.StringValue(utils.StringersToFriendlyString(enum.ConfigletSectionSystemTop)),
),
},
},
}
}

func (o ConfigletGenerator) AttrTypes() map[string]attr.Type {
return map[string]attr.Type{
"config_style": types.StringType,
"section": types.StringType,
"section_condition": types.StringType,
"template_text": types.StringType,
"negation_template_text": types.StringType,
"filename": types.StringType,
}
}

func (o *ConfigletGenerator) LoadApiData(ctx context.Context, in *apstra.ConfigletGenerator, diags *diag.Diagnostics) {
o.ConfigStyle = types.StringValue(utils.StringersToFriendlyString(in.ConfigStyle))
o.Section = types.StringValue(utils.StringersToFriendlyString(in.Section, in.ConfigStyle))
o.SectionCondition = utils.StringValueOrNull(ctx, in.SectionCondition, diags)
o.TemplateText = types.StringValue(in.TemplateText)
o.NegationTemplateText = utils.StringValueOrNull(ctx, in.NegationTemplateText, diags)
o.FileName = utils.StringValueOrNull(ctx, in.Filename, diags)
}

func (o *ConfigletGenerator) Request(_ context.Context, diags *diag.Diagnostics) *apstra.ConfigletGenerator {
var err error

var configStyle enum.ConfigletStyle
err = configStyle.FromString(o.ConfigStyle.ValueString())
if err != nil {
diags.AddError(fmt.Sprintf("error parsing configlet config_style %q", o.ConfigStyle.ValueString()), err.Error())
}

var section enum.ConfigletSection
err = utils.ApiStringerFromFriendlyString(&section, o.Section.ValueString(), o.ConfigStyle.ValueString())
if err != nil {
diags.AddError(fmt.Sprintf("error parsing configlet section %q", o.Section.ValueString()), err.Error())
}

return &apstra.ConfigletGenerator{
ConfigStyle: configStyle,
Section: section,
SectionCondition: o.SectionCondition.ValueString(),
TemplateText: o.TemplateText.ValueString(),
NegationTemplateText: o.NegationTemplateText.ValueString(),
Filename: o.FileName.ValueString(),
}
}
11 changes: 9 additions & 2 deletions apstra/data_source_datacenter_configlet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -12,8 +13,10 @@ import (
"github.com/hashicorp/terraform-plugin-framework/types"
)

var _ datasource.DataSourceWithConfigure = &dataSourceDatacenterConfiglet{}
var _ datasourceWithSetDcBpClientFunc = &dataSourceDatacenterConfiglet{}
var (
_ datasource.DataSourceWithConfigure = &dataSourceDatacenterConfiglet{}
_ datasourceWithSetDcBpClientFunc = &dataSourceDatacenterConfiglet{}
)

type dataSourceDatacenterConfiglet struct {
getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error)
Expand Down Expand Up @@ -87,6 +90,10 @@ func (o *dataSourceDatacenterConfiglet) Read(ctx context.Context, req datasource
return
}
}
if api == nil {
resp.Diagnostics.AddError("invalid API response", "configlet payload is nil")
return
}

config.Id = types.StringValue(api.Id.String())
config.LoadApiData(ctx, api.Data, &resp.Diagnostics)
Expand Down
Loading

0 comments on commit 012f942

Please sign in to comment.