From feaccd9583901d335117bb6324295878f80e0677 Mon Sep 17 00:00:00 2001 From: bwJuniper <98770420+bwJuniper@users.noreply.github.com> Date: Fri, 28 Jun 2024 15:47:59 +0200 Subject: [PATCH 01/18] first stab at freeform_property_sets resource and data source --- apstra/blueprint/freeform_property_set.go | 116 ++++++++++ apstra/configure_resource.go | 17 +- apstra/data_source_freeform_property_set.go | 71 ++++++ apstra/provider.go | 38 +++- apstra/resource_blueprint_iba_dashboard.go | 2 +- apstra/resource_blueprint_iba_probe.go | 2 +- apstra/resource_blueprint_iba_widget.go | 2 +- apstra/resource_datacenter_blueprint.go | 2 +- apstra/resource_datacenter_configlet.go | 2 +- ...source_datacenter_connectivity_template.go | 2 +- ...center_connectivity_template_assignment.go | 2 +- ...enter_connectivity_template_assignments.go | 2 +- ...enter_connectivity_templates_assignment.go | 2 +- .../resource_datacenter_device_allocation.go | 2 +- .../resource_datacenter_external_gateway.go | 2 +- apstra/resource_datacenter_generic_system.go | 2 +- apstra/resource_datacenter_property_set.go | 2 +- apstra/resource_datacenter_rack.go | 2 +- ...rce_datacenter_resource_pool_allocation.go | 2 +- apstra/resource_datacenter_routing_policy.go | 2 +- apstra/resource_datacenter_routing_zone.go | 2 +- apstra/resource_datacenter_security_policy.go | 2 +- apstra/resource_datacenter_virtual_network.go | 2 +- apstra/resource_freeform_property_set.go | 210 ++++++++++++++++++ go.mod | 2 +- go.sum | 4 +- 26 files changed, 468 insertions(+), 28 deletions(-) create mode 100644 apstra/blueprint/freeform_property_set.go create mode 100644 apstra/data_source_freeform_property_set.go create mode 100644 apstra/resource_freeform_property_set.go diff --git a/apstra/blueprint/freeform_property_set.go b/apstra/blueprint/freeform_property_set.go new file mode 100644 index 00000000..5f68d11b --- /dev/null +++ b/apstra/blueprint/freeform_property_set.go @@ -0,0 +1,116 @@ +package blueprint + +import ( + "context" + "encoding/json" + "github.com/Juniper/apstra-go-sdk/apstra" + "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" + "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 FreeformPropertySet struct { + Id types.String `tfsdk:"id"` + BlueprintId types.String `tfsdk:"blueprint_id"` + Name types.String `tfsdk:"name"` + SystemId types.String `tfsdk:"system_id"` + Values jsontypes.Normalized `tfsdk:"values"` +} + +//func (o FreeformPropertySet) AttrTypes() map[string]attr.Type { +// return map[string]attr.Type{ +// "blueprint_id": types.StringType, +// "id": types.StringType, +// "name": types.StringType, +// "data": types.StringType, +// } +//} + +func (o FreeformPropertySet) DataSourceAttributes() map[string]dataSourceSchema.Attribute { + return map[string]dataSourceSchema.Attribute{ + "blueprint_id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Apstra Blueprint ID. Used to identify " + + "the Blueprint that the Property Set has been imported into.", + Required: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Populate this field to look up an imported Property Set 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"), + }...), + }, + }, + "system_id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Populate this field to look up an imported Property Set by `name`. Required when `id` is omitted.", + Optional: true, + Computed: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "name": dataSourceSchema.StringAttribute{ + MarkdownDescription: "A map of values in the Property Set in JSON format", + Computed: true, + }, + "values": dataSourceSchema.SetAttribute{ + MarkdownDescription: "List of Keys that have been imported.", + Computed: true, + ElementType: types.StringType, + }, + } +} + +func (o FreeformPropertySet) ResourceAttributes() map[string]resourceSchema.Attribute { + return map[string]resourceSchema.Attribute{ + "blueprint_id": resourceSchema.StringAttribute{ + MarkdownDescription: "Apstra Blueprint ID. Used to identify the Blueprint that the Property Set is imported into.", + Required: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "id": resourceSchema.StringAttribute{ + MarkdownDescription: "ID of the Property Set ID to be imported.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "name": resourceSchema.StringAttribute{ + MarkdownDescription: "Property Set name as shown in the Web UI.", + Computed: true, + }, + "values": resourceSchema.StringAttribute{ + MarkdownDescription: "A map of values in the Property Set in JSON format.", + CustomType: jsontypes.NormalizedType{}, + Computed: true, + }, + } +} + +func (o *FreeformPropertySet) Request(_ context.Context, _ *diag.Diagnostics) *apstra.FreeformPropertySetData { + return &apstra.FreeformPropertySetData{ + SystemId: (*apstra.ObjectId)(o.SystemId.ValueStringPointer()), + Label: o.Name.ValueString(), + Values: json.RawMessage(o.Values.ValueString()), + } +} + +func (o *FreeformPropertySet) LoadApiData(_ context.Context, in *apstra.FreeformPropertySetData, _ *diag.Diagnostics) { + o.Name = types.StringValue(in.Label) + o.Values = jsontypes.NewNormalizedValue(string(in.Values)) + if in.SystemId != nil { + o.SystemId = types.StringValue(string(*in.SystemId)) + } +} diff --git a/apstra/configure_resource.go b/apstra/configure_resource.go index 8b1de434..e85598f6 100644 --- a/apstra/configure_resource.go +++ b/apstra/configure_resource.go @@ -12,11 +12,16 @@ type resourceWithSetClient interface { setClient(*apstra.Client) } -type resourceWithSetBpClientFunc interface { +type resourceWithSetDcBpClientFunc interface { resource.ResourceWithConfigure setBpClientFunc(func(context.Context, string) (*apstra.TwoStageL3ClosClient, error)) } +type resourceWithSetFfBpClientFunc interface { + resource.ResourceWithConfigure + setBpClientFunc(func(context.Context, string) (*apstra.FreeformClient, error)) +} + type resourceWithSetBpLockFunc interface { resource.ResourceWithConfigure setBpLockFunc(func(context.Context, string) error) @@ -61,10 +66,18 @@ func configureResource(_ context.Context, rs resource.ResourceWithConfigure, req rs.setClient(pd.client) } - if rs, ok := rs.(resourceWithSetBpClientFunc); ok { + if rs, ok := rs.(resourceWithSetDcBpClientFunc); ok { + rs.setBpClientFunc(pd.getTwoStageL3ClosClient) + } + + if rs, ok := rs.(resourceWithSetDcBpClientFunc); ok { rs.setBpClientFunc(pd.getTwoStageL3ClosClient) } + if rs, ok := rs.(resourceWithSetFfBpClientFunc); ok { + rs.setBpClientFunc(pd.getFreeformClient) + } + if rs, ok := rs.(resourceWithSetBpLockFunc); ok { rs.setBpLockFunc(pd.bpLockFunc) } diff --git a/apstra/data_source_freeform_property_set.go b/apstra/data_source_freeform_property_set.go new file mode 100644 index 00000000..6480f6b4 --- /dev/null +++ b/apstra/data_source_freeform_property_set.go @@ -0,0 +1,71 @@ +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" +) + +var _ datasource.DataSourceWithConfigure = &dataSourceFreeformPropertySet{} +var _ datasourceWithSetClient = &dataSourceFreeformPropertySet{} + +type dataSourceFreeformPropertySet struct { + getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) + client *apstra.Client +} + +func (o *dataSourceFreeformPropertySet) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_freeform_property_set" +} + +func (o *dataSourceFreeformPropertySet) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + configureDataSource(ctx, o, req, resp) +} + +func (o *dataSourceFreeformPropertySet) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: docCategoryFreeform + "This data source provides details of a specific Freeform PropertySet.\n\n" + + "At least one optional attribute is required.", + Attributes: blueprint.FreeformPropertySet{}.DataSourceAttributes(), + } +} +func (o *dataSourceFreeformPropertySet) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var state blueprint.FreeformPropertySet + + // 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.GetPropertySet(ctx, apstra.ObjectId(state.Id.ValueString())) + if err != nil { + if utils.IsApstra404(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("Error retrieving PropertySet", 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 *dataSourceFreeformPropertySet) setClient(client *apstra.Client) { + o.client = client +} diff --git a/apstra/provider.go b/apstra/provider.go index 59f1af84..17c88051 100644 --- a/apstra/provider.go +++ b/apstra/provider.go @@ -84,11 +84,12 @@ var blueprintMutexes map[string]apstra.Mutex // mutex which we use to control access to blueprintMutexes var blueprintMutexesMutex sync.Mutex -// map of blueprint clients keyed by blueprint ID +// maps of blueprint clients keyed by blueprint ID var twoStageL3ClosClients map[string]apstra.TwoStageL3ClosClient +var freeformClients map[string]apstra.FreeformClient // mutex which we use to control access to twoStageL3ClosClients -var twoStageL3ClosClientsMutex sync.Mutex +var blueprintClientsMutex sync.Mutex // Provider fulfils the provider.Provider interface type Provider struct { @@ -106,6 +107,7 @@ type providerData struct { bpLockFunc func(context.Context, string) error bpUnlockFunc func(context.Context, string) error getTwoStageL3ClosClient func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) + getFreeformClient func(context.Context, string) (*apstra.FreeformClient, error) experimental bool } @@ -411,8 +413,8 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, // resource or data source. getTwoStageL3ClosClient := func(ctx context.Context, bpId string) (*apstra.TwoStageL3ClosClient, error) { // ensure exclusive access to the blueprint client cache - twoStageL3ClosClientsMutex.Lock() - defer twoStageL3ClosClientsMutex.Unlock() + blueprintClientsMutex.Lock() + defer blueprintClientsMutex.Unlock() // do we already have this client? if twoStageL3ClosClient, ok := twoStageL3ClosClients[bpId]; ok { @@ -436,6 +438,33 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, return twoStageL3ClosClient, nil } + getFreeformClient := func(ctx context.Context, bpId string) (*apstra.FreeformClient, error) { + // ensure exclusive access to the blueprint client cache + blueprintClientsMutex.Lock() + defer blueprintClientsMutex.Unlock() + + // do we already have this client? + if freeformClient, ok := freeformClients[bpId]; ok { + return &freeformClient, nil // client found. return it. + } + + // create new client (this is the expensive-ish API call we're trying to avoid) + freeformClient, err := client.NewFreeformClient(ctx, apstra.ObjectId(bpId)) + if err != nil { + return nil, err + } + + // create the cache if necessary + if freeformClients == nil { + freeformClients = make(map[string]apstra.FreeformClient) + } + + // save a copy of the client in the map / cache + freeformClients[bpId] = *freeformClient + + return freeformClient, nil + } + // data passed to Resource and DataSource Configure() methods pd := &providerData{ client: client, @@ -444,6 +473,7 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, bpLockFunc: bpLockFunc, bpUnlockFunc: bpUnlockFunc, getTwoStageL3ClosClient: getTwoStageL3ClosClient, + getFreeformClient: getFreeformClient, experimental: config.Experimental.ValueBool(), } resp.ResourceData = pd diff --git a/apstra/resource_blueprint_iba_dashboard.go b/apstra/resource_blueprint_iba_dashboard.go index 3213be9a..337c7119 100644 --- a/apstra/resource_blueprint_iba_dashboard.go +++ b/apstra/resource_blueprint_iba_dashboard.go @@ -11,7 +11,7 @@ import ( ) var _ resource.ResourceWithConfigure = &resourceBlueprintIbaDashboard{} -var _ resourceWithSetBpClientFunc = &resourceBlueprintIbaDashboard{} +var _ resourceWithSetDcBpClientFunc = &resourceBlueprintIbaDashboard{} type resourceBlueprintIbaDashboard struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/resource_blueprint_iba_probe.go b/apstra/resource_blueprint_iba_probe.go index 73a88428..74bbe4cb 100644 --- a/apstra/resource_blueprint_iba_probe.go +++ b/apstra/resource_blueprint_iba_probe.go @@ -11,7 +11,7 @@ import ( ) var _ resource.ResourceWithConfigure = &resourceBlueprintIbaProbe{} -var _ resourceWithSetBpClientFunc = &resourceBlueprintIbaProbe{} +var _ resourceWithSetDcBpClientFunc = &resourceBlueprintIbaProbe{} type resourceBlueprintIbaProbe struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/resource_blueprint_iba_widget.go b/apstra/resource_blueprint_iba_widget.go index 418afeb0..4988bbdd 100644 --- a/apstra/resource_blueprint_iba_widget.go +++ b/apstra/resource_blueprint_iba_widget.go @@ -12,7 +12,7 @@ import ( ) var _ resource.ResourceWithConfigure = &resourceBlueprintIbaWidget{} -var _ resourceWithSetBpClientFunc = &resourceBlueprintIbaWidget{} +var _ resourceWithSetDcBpClientFunc = &resourceBlueprintIbaWidget{} type resourceBlueprintIbaWidget struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/resource_datacenter_blueprint.go b/apstra/resource_datacenter_blueprint.go index 7c74b908..8437005e 100644 --- a/apstra/resource_datacenter_blueprint.go +++ b/apstra/resource_datacenter_blueprint.go @@ -18,7 +18,7 @@ var ( _ resource.ResourceWithConfigure = &resourceDatacenterBlueprint{} _ resource.ResourceWithValidateConfig = &resourceDatacenterBlueprint{} _ resourceWithSetClient = &resourceDatacenterBlueprint{} - _ resourceWithSetBpClientFunc = &resourceDatacenterBlueprint{} + _ resourceWithSetDcBpClientFunc = &resourceDatacenterBlueprint{} _ resourceWithSetBpLockFunc = &resourceDatacenterBlueprint{} _ resourceWithSetBpUnlockFunc = &resourceDatacenterBlueprint{} ) diff --git a/apstra/resource_datacenter_configlet.go b/apstra/resource_datacenter_configlet.go index 6611ec8a..a29bb027 100644 --- a/apstra/resource_datacenter_configlet.go +++ b/apstra/resource_datacenter_configlet.go @@ -14,7 +14,7 @@ import ( ) var _ resource.ResourceWithConfigure = &resourceDatacenterConfiglet{} -var _ resourceWithSetBpClientFunc = &resourceDatacenterConfiglet{} +var _ resourceWithSetDcBpClientFunc = &resourceDatacenterConfiglet{} var _ resourceWithSetBpLockFunc = &resourceDatacenterConfiglet{} type resourceDatacenterConfiglet struct { diff --git a/apstra/resource_datacenter_connectivity_template.go b/apstra/resource_datacenter_connectivity_template.go index ab006890..dff1fa0d 100644 --- a/apstra/resource_datacenter_connectivity_template.go +++ b/apstra/resource_datacenter_connectivity_template.go @@ -12,7 +12,7 @@ import ( ) var _ resource.ResourceWithConfigure = &resourceDatacenterConnectivityTemplate{} -var _ resourceWithSetBpClientFunc = &resourceDatacenterConnectivityTemplate{} +var _ resourceWithSetDcBpClientFunc = &resourceDatacenterConnectivityTemplate{} var _ resourceWithSetBpLockFunc = &resourceDatacenterConnectivityTemplate{} type resourceDatacenterConnectivityTemplate struct { diff --git a/apstra/resource_datacenter_connectivity_template_assignment.go b/apstra/resource_datacenter_connectivity_template_assignment.go index 6c25d7bb..46a4f483 100644 --- a/apstra/resource_datacenter_connectivity_template_assignment.go +++ b/apstra/resource_datacenter_connectivity_template_assignment.go @@ -12,7 +12,7 @@ import ( ) var _ resource.ResourceWithConfigure = &resourceDatacenterConnectivityTemplateAssignment{} -var _ resourceWithSetBpClientFunc = &resourceDatacenterConnectivityTemplateAssignment{} +var _ resourceWithSetDcBpClientFunc = &resourceDatacenterConnectivityTemplateAssignment{} var _ resourceWithSetBpLockFunc = &resourceDatacenterConnectivityTemplateAssignment{} type resourceDatacenterConnectivityTemplateAssignment struct { diff --git a/apstra/resource_datacenter_connectivity_template_assignments.go b/apstra/resource_datacenter_connectivity_template_assignments.go index 95122eb5..db6c294f 100644 --- a/apstra/resource_datacenter_connectivity_template_assignments.go +++ b/apstra/resource_datacenter_connectivity_template_assignments.go @@ -14,7 +14,7 @@ import ( ) var _ resource.ResourceWithConfigure = &resourceDatacenterConnectivityTemplateAssignments{} -var _ resourceWithSetBpClientFunc = &resourceDatacenterConnectivityTemplateAssignments{} +var _ resourceWithSetDcBpClientFunc = &resourceDatacenterConnectivityTemplateAssignments{} var _ resourceWithSetBpLockFunc = &resourceDatacenterConnectivityTemplateAssignments{} type resourceDatacenterConnectivityTemplateAssignments struct { diff --git a/apstra/resource_datacenter_connectivity_templates_assignment.go b/apstra/resource_datacenter_connectivity_templates_assignment.go index 0665c1f8..d91f9791 100644 --- a/apstra/resource_datacenter_connectivity_templates_assignment.go +++ b/apstra/resource_datacenter_connectivity_templates_assignment.go @@ -12,7 +12,7 @@ import ( ) var _ resource.ResourceWithConfigure = &resourceDatacenterConnectivityTemplatesAssignment{} -var _ resourceWithSetBpClientFunc = &resourceDatacenterConnectivityTemplatesAssignment{} +var _ resourceWithSetDcBpClientFunc = &resourceDatacenterConnectivityTemplatesAssignment{} var _ resourceWithSetBpLockFunc = &resourceDatacenterConnectivityTemplatesAssignment{} type resourceDatacenterConnectivityTemplatesAssignment struct { diff --git a/apstra/resource_datacenter_device_allocation.go b/apstra/resource_datacenter_device_allocation.go index a0c1cb56..7af8ccda 100644 --- a/apstra/resource_datacenter_device_allocation.go +++ b/apstra/resource_datacenter_device_allocation.go @@ -18,7 +18,7 @@ import ( var ( _ resource.ResourceWithConfigure = &resourceDeviceAllocation{} _ resource.ResourceWithValidateConfig = &resourceDeviceAllocation{} - _ resourceWithSetBpClientFunc = &resourceDeviceAllocation{} + _ resourceWithSetDcBpClientFunc = &resourceDeviceAllocation{} _ resourceWithSetBpLockFunc = &resourceDeviceAllocation{} _ resourceWithSetExperimental = &resourceDeviceAllocation{} ) diff --git a/apstra/resource_datacenter_external_gateway.go b/apstra/resource_datacenter_external_gateway.go index 8fb2026b..9ff550de 100644 --- a/apstra/resource_datacenter_external_gateway.go +++ b/apstra/resource_datacenter_external_gateway.go @@ -14,7 +14,7 @@ import ( var _ resource.ResourceWithConfigure = &resourceDatacenterExternalGateway{} var _ resource.ResourceWithImportState = &resourceDatacenterExternalGateway{} -var _ resourceWithSetBpClientFunc = &resourceDatacenterExternalGateway{} +var _ resourceWithSetDcBpClientFunc = &resourceDatacenterExternalGateway{} var _ resourceWithSetBpLockFunc = &resourceDatacenterExternalGateway{} type resourceDatacenterExternalGateway struct { diff --git a/apstra/resource_datacenter_generic_system.go b/apstra/resource_datacenter_generic_system.go index 59f2f6a7..4aa6f463 100644 --- a/apstra/resource_datacenter_generic_system.go +++ b/apstra/resource_datacenter_generic_system.go @@ -18,7 +18,7 @@ import ( var _ resource.ResourceWithConfigure = &resourceDatacenterGenericSystem{} var _ resource.ResourceWithValidateConfig = &resourceDatacenterGenericSystem{} -var _ resourceWithSetBpClientFunc = &resourceDatacenterGenericSystem{} +var _ resourceWithSetDcBpClientFunc = &resourceDatacenterGenericSystem{} var _ resourceWithSetBpLockFunc = &resourceDatacenterGenericSystem{} type resourceDatacenterGenericSystem struct { diff --git a/apstra/resource_datacenter_property_set.go b/apstra/resource_datacenter_property_set.go index d936d2b0..45433949 100644 --- a/apstra/resource_datacenter_property_set.go +++ b/apstra/resource_datacenter_property_set.go @@ -15,7 +15,7 @@ import ( ) var _ resource.ResourceWithConfigure = &resourceDatacenterPropertySet{} -var _ resourceWithSetBpClientFunc = &resourceDatacenterPropertySet{} +var _ resourceWithSetDcBpClientFunc = &resourceDatacenterPropertySet{} var _ resourceWithSetBpLockFunc = &resourceDatacenterPropertySet{} type resourceDatacenterPropertySet struct { diff --git a/apstra/resource_datacenter_rack.go b/apstra/resource_datacenter_rack.go index 02867f42..d92f811c 100644 --- a/apstra/resource_datacenter_rack.go +++ b/apstra/resource_datacenter_rack.go @@ -12,7 +12,7 @@ import ( ) var _ resource.ResourceWithConfigure = &resourceDatacenterRack{} -var _ resourceWithSetBpClientFunc = &resourceDatacenterRack{} +var _ resourceWithSetDcBpClientFunc = &resourceDatacenterRack{} var _ resourceWithSetBpLockFunc = &resourceDatacenterRack{} type resourceDatacenterRack struct { diff --git a/apstra/resource_datacenter_resource_pool_allocation.go b/apstra/resource_datacenter_resource_pool_allocation.go index 9f626d1c..a637b392 100644 --- a/apstra/resource_datacenter_resource_pool_allocation.go +++ b/apstra/resource_datacenter_resource_pool_allocation.go @@ -12,7 +12,7 @@ import ( ) var _ resource.ResourceWithConfigure = &resourceResourcePoolAllocation{} -var _ resourceWithSetBpClientFunc = &resourceResourcePoolAllocation{} +var _ resourceWithSetDcBpClientFunc = &resourceResourcePoolAllocation{} var _ resourceWithSetBpLockFunc = &resourceResourcePoolAllocation{} type resourceResourcePoolAllocation struct { diff --git a/apstra/resource_datacenter_routing_policy.go b/apstra/resource_datacenter_routing_policy.go index d8c51d30..45a0149d 100644 --- a/apstra/resource_datacenter_routing_policy.go +++ b/apstra/resource_datacenter_routing_policy.go @@ -12,7 +12,7 @@ import ( ) var _ resource.ResourceWithConfigure = &resourceDatacenterRoutingPolicy{} -var _ resourceWithSetBpClientFunc = &resourceDatacenterRoutingPolicy{} +var _ resourceWithSetDcBpClientFunc = &resourceDatacenterRoutingPolicy{} var _ resourceWithSetBpLockFunc = &resourceDatacenterRoutingPolicy{} type resourceDatacenterRoutingPolicy struct { diff --git a/apstra/resource_datacenter_routing_zone.go b/apstra/resource_datacenter_routing_zone.go index 7a2f9867..0ea38565 100644 --- a/apstra/resource_datacenter_routing_zone.go +++ b/apstra/resource_datacenter_routing_zone.go @@ -13,7 +13,7 @@ import ( var _ resource.ResourceWithConfigure = &resourceDatacenterRoutingZone{} var _ resource.ResourceWithModifyPlan = &resourceDatacenterRoutingZone{} -var _ resourceWithSetBpClientFunc = &resourceDatacenterRoutingZone{} +var _ resourceWithSetDcBpClientFunc = &resourceDatacenterRoutingZone{} var _ resourceWithSetBpLockFunc = &resourceDatacenterRoutingZone{} type resourceDatacenterRoutingZone struct { diff --git a/apstra/resource_datacenter_security_policy.go b/apstra/resource_datacenter_security_policy.go index 8b41b707..b3ec2574 100644 --- a/apstra/resource_datacenter_security_policy.go +++ b/apstra/resource_datacenter_security_policy.go @@ -14,7 +14,7 @@ import ( var _ resource.ResourceWithConfigure = &resourceDatacenterSecurityPolicy{} var _ resource.ResourceWithImportState = &resourceDatacenterSecurityPolicy{} -var _ resourceWithSetBpClientFunc = &resourceDatacenterSecurityPolicy{} +var _ resourceWithSetDcBpClientFunc = &resourceDatacenterSecurityPolicy{} var _ resourceWithSetBpLockFunc = &resourceDatacenterSecurityPolicy{} type resourceDatacenterSecurityPolicy struct { diff --git a/apstra/resource_datacenter_virtual_network.go b/apstra/resource_datacenter_virtual_network.go index ef1c13be..5b19c8c7 100644 --- a/apstra/resource_datacenter_virtual_network.go +++ b/apstra/resource_datacenter_virtual_network.go @@ -17,7 +17,7 @@ import ( var _ resource.ResourceWithConfigure = &resourceDatacenterVirtualNetwork{} var _ resource.ResourceWithModifyPlan = &resourceDatacenterVirtualNetwork{} var _ resource.ResourceWithValidateConfig = &resourceDatacenterVirtualNetwork{} -var _ resourceWithSetBpClientFunc = &resourceDatacenterVirtualNetwork{} +var _ resourceWithSetDcBpClientFunc = &resourceDatacenterVirtualNetwork{} var _ resourceWithSetBpLockFunc = &resourceDatacenterVirtualNetwork{} type resourceDatacenterVirtualNetwork struct { diff --git a/apstra/resource_freeform_property_set.go b/apstra/resource_freeform_property_set.go new file mode 100644 index 00000000..e546ad77 --- /dev/null +++ b/apstra/resource_freeform_property_set.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 = &resourceFreeformPropertySet{} +var _ resourceWithSetFfBpClientFunc = &resourceFreeformPropertySet{} +var _ resourceWithSetBpLockFunc = &resourceFreeformPropertySet{} + +type resourceFreeformPropertySet struct { + getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) + lockFunc func(context.Context, string) error +} + +func (o *resourceFreeformPropertySet) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_freeform_property_set" +} + +func (o *resourceFreeformPropertySet) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + configureResource(ctx, o, req, resp) +} + +func (o *resourceFreeformPropertySet) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: docCategoryFreeform + "This resource creates a Property Set in a Freeform Blueprint.", + Attributes: blueprint.FreeformPropertySet{}.ResourceAttributes(), + } +} + +func (o *resourceFreeformPropertySet) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan blueprint.FreeformPropertySet + 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.CreatePropertySet(ctx, request) + if err != nil { + resp.Diagnostics.AddError("error creating new PropertySet", err.Error()) + return + } + + plan.Id = types.StringValue(id.String()) + + // set state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (o *resourceFreeformPropertySet) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state blueprint.FreeformPropertySet + 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.GetPropertySet(ctx, apstra.ObjectId(state.Id.ValueString())) + if err != nil { + if utils.IsApstra404(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("Error retrieving PropertySet", 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)...) +} + +// Update resource +func (o *resourceFreeformPropertySet) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Get plan values + var plan blueprint.FreeformPropertySet + 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 Property Set + err = bp.UpdatePropertySet(ctx, apstra.ObjectId(plan.Id.ValueString()), request) + if err != nil { + resp.Diagnostics.AddError("error updating Property Set", err.Error()) + return + } + // set state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +// Delete resource +func (o *resourceFreeformPropertySet) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state blueprint.FreeformPropertySet + 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 + } + + // 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 Property Set by calling API + err = bp.DeletePropertySet(ctx, apstra.ObjectId(state.Id.ValueString())) + if err != nil { + if utils.IsApstra404(err) { + return // 404 is okay + } + resp.Diagnostics.AddError("error deleting Property Set", err.Error()) + return + } +} + +func (o *resourceFreeformPropertySet) setBpClientFunc(f func(context.Context, string) (*apstra.FreeformClient, error)) { + o.getBpClientFunc = f +} + +func (o *resourceFreeformPropertySet) setBpLockFunc(f func(context.Context, string) error) { + o.lockFunc = f +} diff --git a/go.mod b/go.mod index 8e51038a..b31a1cd8 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ toolchain go1.21.1 require ( github.com/IBM/netaddr v1.5.0 - github.com/Juniper/apstra-go-sdk v0.0.0-20240626014411-5b0eff26054c + github.com/Juniper/apstra-go-sdk v0.0.0-20240627200318-fbae131ae35e 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 247745c4..a733710c 100644 --- a/go.sum +++ b/go.sum @@ -108,8 +108,8 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= 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-20240626014411-5b0eff26054c h1:7aqgHRKpIlHFZo4H4FKAbwhIP4T7tmu8m22SAHQ4zkg= -github.com/Juniper/apstra-go-sdk v0.0.0-20240626014411-5b0eff26054c/go.mod h1:Xwj3X8v/jRZWv28o6vQAqD4lz2JmzaSYLZ2ch1SS89w= +github.com/Juniper/apstra-go-sdk v0.0.0-20240627200318-fbae131ae35e h1:zVfrVnQ1vISjQshfQ+sAPxiSnOk0K98aKbzv2jZqn8M= +github.com/Juniper/apstra-go-sdk v0.0.0-20240627200318-fbae131ae35e/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 242da37b991c4dcd97a3d1bf48933f1b3aee103c Mon Sep 17 00:00:00 2001 From: bwJuniper <98770420+bwJuniper@users.noreply.github.com> Date: Fri, 28 Jun 2024 22:31:18 +0200 Subject: [PATCH 02/18] initial freeform prop set - working --- apstra/blueprint/freeform_property_set.go | 47 ++++++++--------- apstra/configure_data_source.go | 14 +++++- apstra/data_source_blueprint_iba_dashboard.go | 2 +- .../data_source_blueprint_iba_dashboards.go | 2 +- ...a_source_blueprint_iba_predefined_probe.go | 2 +- apstra/data_source_blueprint_iba_widget.go | 2 +- apstra/data_source_datacenter_blueprint.go | 2 +- apstra/data_source_datacenter_configlet.go | 2 +- apstra/data_source_datacenter_configlets.go | 2 +- ...data_source_datacenter_external_gateway.go | 2 +- ...ata_source_datacenter_external_gateways.go | 2 +- ...ource_datacenter_interfaces_by_link_tag.go | 2 +- ..._source_datacenter_interfaces_by_system.go | 2 +- apstra/data_source_datacenter_property_set.go | 2 +- .../data_source_datacenter_property_sets.go | 2 +- ...data_source_datacenter_routing_policies.go | 2 +- .../data_source_datacenter_routing_policy.go | 2 +- apstra/data_source_datacenter_routing_zone.go | 4 +- .../data_source_datacenter_routing_zones.go | 2 +- ...ata_source_datacenter_security_policies.go | 2 +- .../data_source_datacenter_security_policy.go | 2 +- apstra/data_source_datacenter_svi_map.go | 2 +- .../data_source_datacenter_virtual_network.go | 2 +- ...ter_virtual_network_binding_constructor.go | 2 +- ...data_source_datacenter_virtual_networks.go | 2 +- apstra/data_source_freeform_property_set.go | 50 ++++++++++++++----- apstra/data_source_iba_widgets.go | 2 +- apstra/provider.go | 8 +-- go.mod | 2 +- go.sum | 4 +- 30 files changed, 103 insertions(+), 72 deletions(-) diff --git a/apstra/blueprint/freeform_property_set.go b/apstra/blueprint/freeform_property_set.go index 5f68d11b..d746f321 100644 --- a/apstra/blueprint/freeform_property_set.go +++ b/apstra/blueprint/freeform_property_set.go @@ -24,25 +24,16 @@ type FreeformPropertySet struct { Values jsontypes.Normalized `tfsdk:"values"` } -//func (o FreeformPropertySet) AttrTypes() map[string]attr.Type { -// return map[string]attr.Type{ -// "blueprint_id": types.StringType, -// "id": types.StringType, -// "name": types.StringType, -// "data": types.StringType, -// } -//} - func (o FreeformPropertySet) DataSourceAttributes() map[string]dataSourceSchema.Attribute { return map[string]dataSourceSchema.Attribute{ "blueprint_id": dataSourceSchema.StringAttribute{ MarkdownDescription: "Apstra Blueprint ID. Used to identify " + - "the Blueprint that the Property Set has been imported into.", + "the Blueprint where the Property Set lives.", Required: true, Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, "id": dataSourceSchema.StringAttribute{ - MarkdownDescription: "Populate this field to look up an imported Property Set by ID. Required when `name` is omitted.", + MarkdownDescription: "Populate this field to look up a Freeform Property Set by `id`. Required when `name` is omitted.", Optional: true, Computed: true, Validators: []validator.String{ @@ -54,19 +45,20 @@ func (o FreeformPropertySet) DataSourceAttributes() map[string]dataSourceSchema. }, }, "system_id": dataSourceSchema.StringAttribute{ - MarkdownDescription: "Populate this field to look up an imported Property Set by `name`. Required when `id` is omitted.", - Optional: true, + MarkdownDescription: "The system ID where the Property Set is associated.", Computed: true, Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, "name": dataSourceSchema.StringAttribute{ - MarkdownDescription: "A map of values in the Property Set in JSON format", + MarkdownDescription: "Populate this field to look up an imported Property Set by `name`. Required when `id` is omitted.", + Optional: true, Computed: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, - "values": dataSourceSchema.SetAttribute{ - MarkdownDescription: "List of Keys that have been imported.", + "values": dataSourceSchema.StringAttribute{ + MarkdownDescription: "A map of values in the Property Set in JSON format.", + CustomType: jsontypes.NormalizedType{}, Computed: true, - ElementType: types.StringType, }, } } @@ -74,27 +66,30 @@ func (o FreeformPropertySet) DataSourceAttributes() map[string]dataSourceSchema. func (o FreeformPropertySet) ResourceAttributes() map[string]resourceSchema.Attribute { return map[string]resourceSchema.Attribute{ "blueprint_id": resourceSchema.StringAttribute{ - MarkdownDescription: "Apstra Blueprint ID. Used to identify the Blueprint that the Property Set is imported into.", + MarkdownDescription: "Apstra Blueprint ID.", Required: true, Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, }, "id": resourceSchema.StringAttribute{ - MarkdownDescription: "ID of the Property Set ID to be imported.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + MarkdownDescription: "ID of the Property Set.", + Computed: true, + }, + "system_id": resourceSchema.StringAttribute{ + MarkdownDescription: "The system ID where the Property Set is associated.", + Optional: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, }, "name": resourceSchema.StringAttribute{ MarkdownDescription: "Property Set name as shown in the Web UI.", - Computed: true, + Required: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, "values": resourceSchema.StringAttribute{ MarkdownDescription: "A map of values in the Property Set in JSON format.", CustomType: jsontypes.NormalizedType{}, - Computed: true, + Required: true, }, } } diff --git a/apstra/configure_data_source.go b/apstra/configure_data_source.go index 7441dd3a..62dd343f 100644 --- a/apstra/configure_data_source.go +++ b/apstra/configure_data_source.go @@ -12,11 +12,16 @@ type datasourceWithSetClient interface { setClient(*apstra.Client) } -type datasourceWithSetBpClientFunc interface { +type datasourceWithSetDcBpClientFunc interface { datasource.DataSourceWithConfigure setBpClientFunc(func(context.Context, string) (*apstra.TwoStageL3ClosClient, error)) } +type datasourceWithSetFfBpClientFunc interface { + datasource.DataSourceWithConfigure + setBpClientFunc(func(context.Context, string) (*apstra.FreeformClient, error)) +} + func configureDataSource(_ context.Context, ds datasource.DataSourceWithConfigure, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { if req.ProviderData == nil { return // cannot continue @@ -36,7 +41,12 @@ func configureDataSource(_ context.Context, ds datasource.DataSourceWithConfigur ds.setClient(pd.client) } - if ds, ok := ds.(datasourceWithSetBpClientFunc); ok { + if ds, ok := ds.(datasourceWithSetDcBpClientFunc); ok { ds.setBpClientFunc(pd.getTwoStageL3ClosClient) } + + if ds, ok := ds.(datasourceWithSetFfBpClientFunc); ok { + ds.setBpClientFunc(pd.getFreeformClient) + } + } diff --git a/apstra/data_source_blueprint_iba_dashboard.go b/apstra/data_source_blueprint_iba_dashboard.go index e0d3049e..f1c25470 100644 --- a/apstra/data_source_blueprint_iba_dashboard.go +++ b/apstra/data_source_blueprint_iba_dashboard.go @@ -12,7 +12,7 @@ import ( ) var _ datasource.DataSourceWithConfigure = &dataSourceBlueprintIbaDashboard{} -var _ datasourceWithSetBpClientFunc = &dataSourceBlueprintIbaDashboard{} +var _ datasourceWithSetDcBpClientFunc = &dataSourceBlueprintIbaDashboard{} type dataSourceBlueprintIbaDashboard struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/data_source_blueprint_iba_dashboards.go b/apstra/data_source_blueprint_iba_dashboards.go index a841432f..19a14b90 100644 --- a/apstra/data_source_blueprint_iba_dashboards.go +++ b/apstra/data_source_blueprint_iba_dashboards.go @@ -14,7 +14,7 @@ import ( ) var _ datasource.DataSourceWithConfigure = &dataSourceBlueprintIbaDashboards{} -var _ datasourceWithSetBpClientFunc = &dataSourceBlueprintIbaDashboards{} +var _ datasourceWithSetDcBpClientFunc = &dataSourceBlueprintIbaDashboards{} type dataSourceBlueprintIbaDashboards struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/data_source_blueprint_iba_predefined_probe.go b/apstra/data_source_blueprint_iba_predefined_probe.go index e6d4fae2..7a9eee52 100644 --- a/apstra/data_source_blueprint_iba_predefined_probe.go +++ b/apstra/data_source_blueprint_iba_predefined_probe.go @@ -12,7 +12,7 @@ import ( ) var _ datasource.DataSourceWithConfigure = &dataSourceBlueprintIbaPredefinedProbe{} -var _ datasourceWithSetBpClientFunc = &dataSourceBlueprintIbaPredefinedProbe{} +var _ datasourceWithSetDcBpClientFunc = &dataSourceBlueprintIbaPredefinedProbe{} type dataSourceBlueprintIbaPredefinedProbe struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/data_source_blueprint_iba_widget.go b/apstra/data_source_blueprint_iba_widget.go index d438a588..95704bad 100644 --- a/apstra/data_source_blueprint_iba_widget.go +++ b/apstra/data_source_blueprint_iba_widget.go @@ -12,7 +12,7 @@ import ( ) var _ datasource.DataSourceWithConfigure = &dataSourceBlueprintIbaWidget{} -var _ datasourceWithSetBpClientFunc = &dataSourceBlueprintIbaWidget{} +var _ datasourceWithSetDcBpClientFunc = &dataSourceBlueprintIbaWidget{} type dataSourceBlueprintIbaWidget struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/data_source_datacenter_blueprint.go b/apstra/data_source_datacenter_blueprint.go index ae91cfd6..861caade 100644 --- a/apstra/data_source_datacenter_blueprint.go +++ b/apstra/data_source_datacenter_blueprint.go @@ -17,7 +17,7 @@ import ( var ( _ datasource.DataSourceWithConfigure = &dataSourceDatacenterBlueprint{} _ datasourceWithSetClient = &dataSourceDatacenterBlueprint{} - _ datasourceWithSetBpClientFunc = &dataSourceDatacenterBlueprint{} + _ datasourceWithSetDcBpClientFunc = &dataSourceDatacenterBlueprint{} ) type dataSourceDatacenterBlueprint struct { diff --git a/apstra/data_source_datacenter_configlet.go b/apstra/data_source_datacenter_configlet.go index ff8551a2..27f31e7c 100644 --- a/apstra/data_source_datacenter_configlet.go +++ b/apstra/data_source_datacenter_configlet.go @@ -13,7 +13,7 @@ import ( ) var _ datasource.DataSourceWithConfigure = &dataSourceDatacenterConfiglet{} -var _ datasourceWithSetBpClientFunc = &dataSourceDatacenterConfiglet{} +var _ datasourceWithSetDcBpClientFunc = &dataSourceDatacenterConfiglet{} type dataSourceDatacenterConfiglet struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/data_source_datacenter_configlets.go b/apstra/data_source_datacenter_configlets.go index b6fce396..abd5840a 100644 --- a/apstra/data_source_datacenter_configlets.go +++ b/apstra/data_source_datacenter_configlets.go @@ -14,7 +14,7 @@ import ( ) var _ datasource.DataSourceWithConfigure = &dataSourceDatacenterConfiglets{} -var _ datasourceWithSetBpClientFunc = &dataSourceDatacenterConfiglets{} +var _ datasourceWithSetDcBpClientFunc = &dataSourceDatacenterConfiglets{} type dataSourceDatacenterConfiglets struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/data_source_datacenter_external_gateway.go b/apstra/data_source_datacenter_external_gateway.go index 261ccb9d..6c431302 100644 --- a/apstra/data_source_datacenter_external_gateway.go +++ b/apstra/data_source_datacenter_external_gateway.go @@ -12,7 +12,7 @@ import ( ) var _ datasource.DataSourceWithConfigure = &dataSourceDatacenterExternalGateway{} -var _ datasourceWithSetBpClientFunc = &dataSourceDatacenterExternalGateway{} +var _ datasourceWithSetDcBpClientFunc = &dataSourceDatacenterExternalGateway{} type dataSourceDatacenterExternalGateway struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/data_source_datacenter_external_gateways.go b/apstra/data_source_datacenter_external_gateways.go index bec11213..38f65567 100644 --- a/apstra/data_source_datacenter_external_gateways.go +++ b/apstra/data_source_datacenter_external_gateways.go @@ -17,7 +17,7 @@ import ( ) var _ datasource.DataSourceWithConfigure = &dataSourceDatacenterExternalGateways{} -var _ datasourceWithSetBpClientFunc = &dataSourceDatacenterExternalGateways{} +var _ datasourceWithSetDcBpClientFunc = &dataSourceDatacenterExternalGateways{} type dataSourceDatacenterExternalGateways struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/data_source_datacenter_interfaces_by_link_tag.go b/apstra/data_source_datacenter_interfaces_by_link_tag.go index 704cb612..92bf0447 100644 --- a/apstra/data_source_datacenter_interfaces_by_link_tag.go +++ b/apstra/data_source_datacenter_interfaces_by_link_tag.go @@ -12,7 +12,7 @@ import ( ) var _ datasource.DataSourceWithConfigure = &dataSourceInterfacesByLinkTag{} -var _ datasourceWithSetBpClientFunc = &dataSourceInterfacesByLinkTag{} +var _ datasourceWithSetDcBpClientFunc = &dataSourceInterfacesByLinkTag{} type dataSourceInterfacesByLinkTag struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/data_source_datacenter_interfaces_by_system.go b/apstra/data_source_datacenter_interfaces_by_system.go index cf86c2a8..5f8783ef 100644 --- a/apstra/data_source_datacenter_interfaces_by_system.go +++ b/apstra/data_source_datacenter_interfaces_by_system.go @@ -12,7 +12,7 @@ import ( ) var _ datasource.DataSourceWithConfigure = &dataSourceInterfacesBySystem{} -var _ datasourceWithSetBpClientFunc = &dataSourceInterfacesBySystem{} +var _ datasourceWithSetDcBpClientFunc = &dataSourceInterfacesBySystem{} type dataSourceInterfacesBySystem struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/data_source_datacenter_property_set.go b/apstra/data_source_datacenter_property_set.go index 77d835ee..e73afba5 100644 --- a/apstra/data_source_datacenter_property_set.go +++ b/apstra/data_source_datacenter_property_set.go @@ -12,7 +12,7 @@ import ( ) var _ datasource.DataSourceWithConfigure = &dataSourceDatacenterPropertySet{} -var _ datasourceWithSetBpClientFunc = &dataSourceDatacenterPropertySet{} +var _ datasourceWithSetDcBpClientFunc = &dataSourceDatacenterPropertySet{} type dataSourceDatacenterPropertySet struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/data_source_datacenter_property_sets.go b/apstra/data_source_datacenter_property_sets.go index 1e3fe86a..6d8de5c8 100644 --- a/apstra/data_source_datacenter_property_sets.go +++ b/apstra/data_source_datacenter_property_sets.go @@ -14,7 +14,7 @@ import ( ) var _ datasource.DataSourceWithConfigure = &dataSourceDatacenterPropertySets{} -var _ datasourceWithSetBpClientFunc = &dataSourceDatacenterPropertySets{} +var _ datasourceWithSetDcBpClientFunc = &dataSourceDatacenterPropertySets{} type dataSourceDatacenterPropertySets struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/data_source_datacenter_routing_policies.go b/apstra/data_source_datacenter_routing_policies.go index 79a1cdfc..871a89b1 100644 --- a/apstra/data_source_datacenter_routing_policies.go +++ b/apstra/data_source_datacenter_routing_policies.go @@ -17,7 +17,7 @@ import ( ) var _ datasource.DataSourceWithConfigure = &dataSourceDatacenterRoutingPolicies{} -var _ datasourceWithSetBpClientFunc = &dataSourceDatacenterRoutingPolicies{} +var _ datasourceWithSetDcBpClientFunc = &dataSourceDatacenterRoutingPolicies{} type dataSourceDatacenterRoutingPolicies struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/data_source_datacenter_routing_policy.go b/apstra/data_source_datacenter_routing_policy.go index afe89861..26fcfc14 100644 --- a/apstra/data_source_datacenter_routing_policy.go +++ b/apstra/data_source_datacenter_routing_policy.go @@ -13,7 +13,7 @@ import ( ) var _ datasource.DataSourceWithConfigure = &dataSourceDatacenterRoutingPolicy{} -var _ datasourceWithSetBpClientFunc = &dataSourceDatacenterRoutingPolicy{} +var _ datasourceWithSetDcBpClientFunc = &dataSourceDatacenterRoutingPolicy{} type dataSourceDatacenterRoutingPolicy struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/data_source_datacenter_routing_zone.go b/apstra/data_source_datacenter_routing_zone.go index 21117b5e..86fddc57 100644 --- a/apstra/data_source_datacenter_routing_zone.go +++ b/apstra/data_source_datacenter_routing_zone.go @@ -13,7 +13,7 @@ import ( ) var _ datasource.DataSourceWithConfigure = &dataSourceDatacenterRoutingZone{} -var _ datasourceWithSetBpClientFunc = &dataSourceDatacenterRoutingZone{} +var _ datasourceWithSetDcBpClientFunc = &dataSourceDatacenterRoutingZone{} type dataSourceDatacenterRoutingZone struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) @@ -61,7 +61,7 @@ func (o *dataSourceDatacenterRoutingZone) Read(ctx context.Context, req datasour if utils.IsApstra404(err) { resp.Diagnostics.AddAttributeError( path.Root("id"), - "Routing Zone not found", + "Routing Zone not found", fmt.Sprintf("Routing Zone with ID %s not found", config.Id)) return } diff --git a/apstra/data_source_datacenter_routing_zones.go b/apstra/data_source_datacenter_routing_zones.go index 0f67a221..b80c3af1 100644 --- a/apstra/data_source_datacenter_routing_zones.go +++ b/apstra/data_source_datacenter_routing_zones.go @@ -21,7 +21,7 @@ import ( var ( _ datasource.DataSourceWithConfigure = &dataSourceDatacenterRoutingZones{} - _ datasourceWithSetBpClientFunc = &dataSourceDatacenterRoutingZones{} + _ datasourceWithSetDcBpClientFunc = &dataSourceDatacenterRoutingZones{} ) type dataSourceDatacenterRoutingZones struct { diff --git a/apstra/data_source_datacenter_security_policies.go b/apstra/data_source_datacenter_security_policies.go index 8a6830c9..6e0cbf51 100644 --- a/apstra/data_source_datacenter_security_policies.go +++ b/apstra/data_source_datacenter_security_policies.go @@ -18,7 +18,7 @@ import ( ) var _ datasource.DataSourceWithConfigure = &dataSourceDatacenterSecurityPolicies{} -var _ datasourceWithSetBpClientFunc = &dataSourceDatacenterSecurityPolicies{} +var _ datasourceWithSetDcBpClientFunc = &dataSourceDatacenterSecurityPolicies{} type dataSourceDatacenterSecurityPolicies struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/data_source_datacenter_security_policy.go b/apstra/data_source_datacenter_security_policy.go index 3908e806..f9f18cce 100644 --- a/apstra/data_source_datacenter_security_policy.go +++ b/apstra/data_source_datacenter_security_policy.go @@ -12,7 +12,7 @@ import ( ) var _ datasource.DataSourceWithConfigure = &dataSourceDatacenterSecurityPolicy{} -var _ datasourceWithSetBpClientFunc = &dataSourceDatacenterSecurityPolicy{} +var _ datasourceWithSetDcBpClientFunc = &dataSourceDatacenterSecurityPolicy{} type dataSourceDatacenterSecurityPolicy struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/data_source_datacenter_svi_map.go b/apstra/data_source_datacenter_svi_map.go index 1d7bc169..6a6d1198 100644 --- a/apstra/data_source_datacenter_svi_map.go +++ b/apstra/data_source_datacenter_svi_map.go @@ -13,7 +13,7 @@ import ( ) var _ datasource.DataSourceWithConfigure = &dataSourceDatacenterSvis{} -var _ datasourceWithSetBpClientFunc = &dataSourceDatacenterSvis{} +var _ datasourceWithSetDcBpClientFunc = &dataSourceDatacenterSvis{} type dataSourceDatacenterSvis struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/data_source_datacenter_virtual_network.go b/apstra/data_source_datacenter_virtual_network.go index f799c27d..50a1f176 100644 --- a/apstra/data_source_datacenter_virtual_network.go +++ b/apstra/data_source_datacenter_virtual_network.go @@ -13,7 +13,7 @@ import ( ) var _ datasource.DataSourceWithConfigure = &dataSourceDatacenterVirtualNetwork{} -var _ datasourceWithSetBpClientFunc = &dataSourceDatacenterVirtualNetwork{} +var _ datasourceWithSetDcBpClientFunc = &dataSourceDatacenterVirtualNetwork{} type dataSourceDatacenterVirtualNetwork struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/data_source_datacenter_virtual_network_binding_constructor.go b/apstra/data_source_datacenter_virtual_network_binding_constructor.go index 8a491c51..54791074 100644 --- a/apstra/data_source_datacenter_virtual_network_binding_constructor.go +++ b/apstra/data_source_datacenter_virtual_network_binding_constructor.go @@ -12,7 +12,7 @@ import ( ) var _ datasource.DataSourceWithConfigure = &dataSourceVirtualNetworkBindingConstructor{} -var _ datasourceWithSetBpClientFunc = &dataSourceVirtualNetworkBindingConstructor{} +var _ datasourceWithSetDcBpClientFunc = &dataSourceVirtualNetworkBindingConstructor{} type dataSourceVirtualNetworkBindingConstructor struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/data_source_datacenter_virtual_networks.go b/apstra/data_source_datacenter_virtual_networks.go index 004ad5ee..15bdfce5 100644 --- a/apstra/data_source_datacenter_virtual_networks.go +++ b/apstra/data_source_datacenter_virtual_networks.go @@ -21,7 +21,7 @@ import ( ) var _ datasource.DataSourceWithConfigure = &dataSourceDatacenterVirtualNetworks{} -var _ datasourceWithSetBpClientFunc = &dataSourceDatacenterVirtualNetworks{} +var _ datasourceWithSetDcBpClientFunc = &dataSourceDatacenterVirtualNetworks{} type dataSourceDatacenterVirtualNetworks struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/data_source_freeform_property_set.go b/apstra/data_source_freeform_property_set.go index 6480f6b4..72bae6cb 100644 --- a/apstra/data_source_freeform_property_set.go +++ b/apstra/data_source_freeform_property_set.go @@ -8,14 +8,14 @@ import ( "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" ) var _ datasource.DataSourceWithConfigure = &dataSourceFreeformPropertySet{} -var _ datasourceWithSetClient = &dataSourceFreeformPropertySet{} +var _ datasourceWithSetFfBpClientFunc = &dataSourceFreeformPropertySet{} type dataSourceFreeformPropertySet struct { getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) - client *apstra.Client } func (o *dataSourceFreeformPropertySet) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { @@ -34,38 +34,62 @@ func (o *dataSourceFreeformPropertySet) Schema(_ context.Context, _ datasource.S } } func (o *dataSourceFreeformPropertySet) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - var state blueprint.FreeformPropertySet + var config blueprint.FreeformPropertySet + 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, state.BlueprintId.ValueString()) + bp, err := o.getBpClientFunc(ctx, config.BlueprintId.ValueString()) if err != nil { if utils.IsApstra404(err) { - resp.Diagnostics.AddError(fmt.Sprintf("blueprint %s not found", state.BlueprintId), err.Error()) + 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 } - api, err := bp.GetPropertySet(ctx, apstra.ObjectId(state.Id.ValueString())) - if err != nil { + var api *apstra.FreeformPropertySet + switch { + case !config.Id.IsNull(): + api, err = bp.GetPropertySet(ctx, apstra.ObjectId(config.Id.ValueString())) if utils.IsApstra404(err) { - resp.State.RemoveResource(ctx) + resp.Diagnostics.AddAttributeError( + path.Root("id"), + "Property Set not found", + fmt.Sprintf("Property Set with ID %s not found", config.Id)) return } - resp.Diagnostics.AddError("Error retrieving PropertySet", err.Error()) + case !config.Name.IsNull(): + api, err = bp.GetPropertySetByName(ctx, config.Name.ValueString()) + if utils.IsApstra404(err) { + resp.Diagnostics.AddAttributeError( + path.Root("name"), + "Property Set not found", + fmt.Sprintf("Property Set with Name %s not found", config.Name)) + return + } + } + if err != nil { + resp.Diagnostics.AddError("failed reading Property Set", err.Error()) + return + } + if api.Data == nil { + resp.Diagnostics.AddError("failed reading Property Set", "api response has no payload") return } - state.LoadApiData(ctx, api.Data, &resp.Diagnostics) + config.LoadApiData(ctx, api.Data, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } // Set state - resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) } -func (o *dataSourceFreeformPropertySet) setClient(client *apstra.Client) { - o.client = client +func (o *dataSourceFreeformPropertySet) setBpClientFunc(f func(context.Context, string) (*apstra.FreeformClient, error)) { + o.getBpClientFunc = f } diff --git a/apstra/data_source_iba_widgets.go b/apstra/data_source_iba_widgets.go index 228fde39..f25db902 100644 --- a/apstra/data_source_iba_widgets.go +++ b/apstra/data_source_iba_widgets.go @@ -14,7 +14,7 @@ import ( ) var _ datasource.DataSourceWithConfigure = &dataSourceBlueprintIbaWidgets{} -var _ datasourceWithSetBpClientFunc = &dataSourceBlueprintIbaWidgets{} +var _ datasourceWithSetDcBpClientFunc = &dataSourceBlueprintIbaWidgets{} type dataSourceBlueprintIbaWidgets struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/provider.go b/apstra/provider.go index 17c88051..f905251b 100644 --- a/apstra/provider.go +++ b/apstra/provider.go @@ -529,6 +529,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 &dataSourceFreeformPropertySet{} }, func() datasource.DataSource { return &dataSourceIntegerPool{} }, func() datasource.DataSource { return &dataSourceInterfacesByLinkTag{} }, func() datasource.DataSource { return &dataSourceInterfacesBySystem{} }, @@ -561,6 +562,9 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { func() resource.Resource { return &resourceAgentProfile{} }, func() resource.Resource { return &resourceAsnPool{} }, func() resource.Resource { return &resourceBlueprintDeploy{} }, + func() resource.Resource { return &resourceBlueprintIbaDashboard{} }, + func() resource.Resource { return &resourceBlueprintIbaProbe{} }, + func() resource.Resource { return &resourceBlueprintIbaWidget{} }, func() resource.Resource { return &resourceConfiglet{} }, func() resource.Resource { return &resourceDatacenterBlueprint{} }, func() resource.Resource { return &resourceDatacenterConfiglet{} }, @@ -577,9 +581,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { func() resource.Resource { return &resourceDatacenterSecurityPolicy{} }, func() resource.Resource { return &resourceDatacenterVirtualNetwork{} }, func() resource.Resource { return &resourceDeviceAllocation{} }, - func() resource.Resource { return &resourceBlueprintIbaDashboard{} }, - func() resource.Resource { return &resourceBlueprintIbaProbe{} }, - func() resource.Resource { return &resourceBlueprintIbaWidget{} }, + func() resource.Resource { return &resourceFreeformPropertySet{} }, func() resource.Resource { return &resourceIntegerPool{} }, func() resource.Resource { return &resourceInterfaceMap{} }, func() resource.Resource { return &resourceIpv4Pool{} }, diff --git a/go.mod b/go.mod index b31a1cd8..2892d184 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ toolchain go1.21.1 require ( github.com/IBM/netaddr v1.5.0 - github.com/Juniper/apstra-go-sdk v0.0.0-20240627200318-fbae131ae35e + github.com/Juniper/apstra-go-sdk v0.0.0-20240628202621-f1748f44d60c 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 a733710c..cf0a3fff 100644 --- a/go.sum +++ b/go.sum @@ -108,8 +108,8 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= 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-20240627200318-fbae131ae35e h1:zVfrVnQ1vISjQshfQ+sAPxiSnOk0K98aKbzv2jZqn8M= -github.com/Juniper/apstra-go-sdk v0.0.0-20240627200318-fbae131ae35e/go.mod h1:Xwj3X8v/jRZWv28o6vQAqD4lz2JmzaSYLZ2ch1SS89w= +github.com/Juniper/apstra-go-sdk v0.0.0-20240628202621-f1748f44d60c h1:QV5sZaDgkxa7BZxWs+dlCrZd6fvn3B+uc071sS7V5Lk= +github.com/Juniper/apstra-go-sdk v0.0.0-20240628202621-f1748f44d60c/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 35366c247355f0e8e355697bb7e281b6999989d9 Mon Sep 17 00:00:00 2001 From: bwJuniper <98770420+bwJuniper@users.noreply.github.com> Date: Mon, 1 Jul 2024 20:35:25 +0200 Subject: [PATCH 03/18] working config_template & examples & make docs --- apstra/blueprint/freeform_config_template.go | 130 +++++++++++ .../data_source_freeform_config_template.go | 95 ++++++++ apstra/provider.go | 2 + apstra/resource_freeform_config_template.go | 210 ++++++++++++++++++ docs/data-sources/freeform_config_template.md | 50 +++++ docs/data-sources/freeform_property_set.md | 66 ++++++ docs/resources/freeform_config_template.md | 50 +++++ docs/resources/freeform_property_set.md | 66 ++++++ .../example.tf | 13 ++ .../apstra_freeform_property_set/example.tf | 31 +++ .../example.tf | 13 ++ .../apstra_freeform_property_set/example.tf | 30 +++ go.mod | 2 +- go.sum | 4 +- 14 files changed, 759 insertions(+), 3 deletions(-) create mode 100644 apstra/blueprint/freeform_config_template.go create mode 100644 apstra/data_source_freeform_config_template.go create mode 100644 apstra/resource_freeform_config_template.go create mode 100644 docs/data-sources/freeform_config_template.md create mode 100644 docs/data-sources/freeform_property_set.md create mode 100644 docs/resources/freeform_config_template.md create mode 100644 docs/resources/freeform_property_set.md create mode 100644 examples/data-sources/apstra_freeform_config_template/example.tf create mode 100644 examples/data-sources/apstra_freeform_property_set/example.tf create mode 100644 examples/resources/apstra_freeform_config_template/example.tf create mode 100644 examples/resources/apstra_freeform_property_set/example.tf diff --git a/apstra/blueprint/freeform_config_template.go b/apstra/blueprint/freeform_config_template.go new file mode 100644 index 00000000..1d3239e6 --- /dev/null +++ b/apstra/blueprint/freeform_config_template.go @@ -0,0 +1,130 @@ +package blueprint + +import ( + "context" + "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" + "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 FreeformConfigTemplate struct { + Id types.String `tfsdk:"id"` + BlueprintId types.String `tfsdk:"blueprint_id"` + Name types.String `tfsdk:"name"` + Text types.String `tfsdk:"text"` + TemplateId types.String `tfsdk:"template_id"` + Tags types.Set `tfsdk:"tags"` +} + +func (o FreeformConfigTemplate) 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 Config Template lives.", + Required: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Populate this field to look up the Config Template 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"), + }...), + }, + }, + "template_id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "The Template ID of the configuration template in global catalog\n", + Computed: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "name": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Populate this field to look up an imported Config Template by `name`. Required when `id` is omitted.", + Optional: true, + Computed: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "text": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Configuration Jinja2 template text", + Computed: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "tags": dataSourceSchema.SetAttribute{ + MarkdownDescription: "Set of Tag labels", + ElementType: types.StringType, + Optional: true, + Validators: []validator.Set{setvalidator.SizeAtLeast(1)}, + }, + } +} + +func (o FreeformConfigTemplate) 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 Config Template.", + Computed: true, + }, + "template_id": resourceSchema.StringAttribute{ + MarkdownDescription: "The template ID of the config template in the global catalog.", + Optional: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "name": resourceSchema.StringAttribute{ + MarkdownDescription: "Config Template name as shown in the Web UI.", + Required: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "text": resourceSchema.StringAttribute{ + MarkdownDescription: "Configuration Jinja2 template text", + Required: 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)}, + }, + } +} + +func (o *FreeformConfigTemplate) Request(ctx context.Context, diags *diag.Diagnostics) *apstra.ConfigTemplateData { + var tags []string + diags.Append(o.Tags.ElementsAs(ctx, &tags, false)...) + if diags.HasError() { + return nil + } + + return &apstra.ConfigTemplateData{ + Label: o.Name.ValueString(), + Text: o.Text.ValueString(), + Tags: tags, + TemplateId: apstra.ObjectId(o.TemplateId.ValueString()), + } +} + +func (o *FreeformConfigTemplate) LoadApiData(ctx context.Context, in *apstra.ConfigTemplateData, diags *diag.Diagnostics) { + o.Name = types.StringValue(in.Label) + o.Text = types.StringValue(in.Text) + o.TemplateId = types.StringValue(string(in.TemplateId)) + o.Tags = utils.SetValueOrNull(ctx, types.StringType, in.Tags, diags) // safe to ignore diagnostic here +} diff --git a/apstra/data_source_freeform_config_template.go b/apstra/data_source_freeform_config_template.go new file mode 100644 index 00000000..58644d8c --- /dev/null +++ b/apstra/data_source_freeform_config_template.go @@ -0,0 +1,95 @@ +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" +) + +var _ datasource.DataSourceWithConfigure = &dataSourceFreeformConfigTemplate{} +var _ datasourceWithSetFfBpClientFunc = &dataSourceFreeformConfigTemplate{} + +type dataSourceFreeformConfigTemplate struct { + getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) +} + +func (o *dataSourceFreeformConfigTemplate) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_freeform_config_template" +} + +func (o *dataSourceFreeformConfigTemplate) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + configureDataSource(ctx, o, req, resp) +} + +func (o *dataSourceFreeformConfigTemplate) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: docCategoryFreeform + "This data source provides details of a specific Freeform Config Template.\n\n" + + "At least one optional attribute is required.", + Attributes: blueprint.FreeformConfigTemplate{}.DataSourceAttributes(), + } +} +func (o *dataSourceFreeformConfigTemplate) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config blueprint.FreeformConfigTemplate + 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.ConfigTemplate + switch { + case !config.Id.IsNull(): + api, err = bp.GetConfigTemplate(ctx, apstra.ObjectId(config.Id.ValueString())) + if utils.IsApstra404(err) { + resp.Diagnostics.AddAttributeError( + path.Root("id"), + "Config Template not found", + fmt.Sprintf("Config Template with ID %s not found", config.Id)) + return + } + case !config.Name.IsNull(): + api, err = bp.GetConfigTemplateByName(ctx, config.Name.ValueString()) + if utils.IsApstra404(err) { + resp.Diagnostics.AddAttributeError( + path.Root("name"), + "Config Template not found", + fmt.Sprintf("Config Template with Name %s not found", config.Name)) + return + } + } + if err != nil { + resp.Diagnostics.AddError("failed reading Config Template", err.Error()) + return + } + if api.Data == nil { + resp.Diagnostics.AddError("failed reading Config Template", "api response has no payload") + return + } + + config.LoadApiData(ctx, api.Data, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // Set state + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) +} + +func (o *dataSourceFreeformConfigTemplate) setBpClientFunc(f func(context.Context, string) (*apstra.FreeformClient, error)) { + o.getBpClientFunc = f +} diff --git a/apstra/provider.go b/apstra/provider.go index f905251b..8c95ffe9 100644 --- a/apstra/provider.go +++ b/apstra/provider.go @@ -529,6 +529,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 &dataSourceFreeformConfigTemplate{} }, func() datasource.DataSource { return &dataSourceFreeformPropertySet{} }, func() datasource.DataSource { return &dataSourceIntegerPool{} }, func() datasource.DataSource { return &dataSourceInterfacesByLinkTag{} }, @@ -581,6 +582,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { func() resource.Resource { return &resourceDatacenterSecurityPolicy{} }, func() resource.Resource { return &resourceDatacenterVirtualNetwork{} }, func() resource.Resource { return &resourceDeviceAllocation{} }, + func() resource.Resource { return &resourceFreeformConfigTemplate{} }, func() resource.Resource { return &resourceFreeformPropertySet{} }, func() resource.Resource { return &resourceIntegerPool{} }, func() resource.Resource { return &resourceInterfaceMap{} }, diff --git a/apstra/resource_freeform_config_template.go b/apstra/resource_freeform_config_template.go new file mode 100644 index 00000000..8b43145b --- /dev/null +++ b/apstra/resource_freeform_config_template.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 = &resourceFreeformConfigTemplate{} +var _ resourceWithSetFfBpClientFunc = &resourceFreeformConfigTemplate{} +var _ resourceWithSetBpLockFunc = &resourceFreeformConfigTemplate{} + +type resourceFreeformConfigTemplate struct { + getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) + lockFunc func(context.Context, string) error +} + +func (o *resourceFreeformConfigTemplate) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_freeform_config_template" +} + +func (o *resourceFreeformConfigTemplate) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + configureResource(ctx, o, req, resp) +} + +func (o *resourceFreeformConfigTemplate) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: docCategoryFreeform + "This resource creates a Config Template in a Freeform Blueprint.", + Attributes: blueprint.FreeformConfigTemplate{}.ResourceAttributes(), + } +} + +func (o *resourceFreeformConfigTemplate) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan blueprint.FreeformConfigTemplate + 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.CreateConfigTemplate(ctx, request) + if err != nil { + resp.Diagnostics.AddError("error creating new ConfigTemplate", err.Error()) + return + } + + plan.Id = types.StringValue(id.String()) + + // set state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (o *resourceFreeformConfigTemplate) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state blueprint.FreeformConfigTemplate + 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.GetConfigTemplate(ctx, apstra.ObjectId(state.Id.ValueString())) + if err != nil { + if utils.IsApstra404(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("Error retrieving ConfigTemplate", 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)...) +} + +// Update resource +func (o *resourceFreeformConfigTemplate) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Get plan values + var plan blueprint.FreeformConfigTemplate + 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.UpdateConfigTemplate(ctx, apstra.ObjectId(plan.Id.ValueString()), request) + if err != nil { + resp.Diagnostics.AddError("error updating Config Template", err.Error()) + return + } + // set state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +// Delete resource +func (o *resourceFreeformConfigTemplate) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state blueprint.FreeformConfigTemplate + 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 + } + + // 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.DeleteConfigTemplate(ctx, apstra.ObjectId(state.Id.ValueString())) + if err != nil { + if utils.IsApstra404(err) { + return // 404 is okay + } + resp.Diagnostics.AddError("error deleting Config Template", err.Error()) + return + } +} + +func (o *resourceFreeformConfigTemplate) setBpClientFunc(f func(context.Context, string) (*apstra.FreeformClient, error)) { + o.getBpClientFunc = f +} + +func (o *resourceFreeformConfigTemplate) setBpLockFunc(f func(context.Context, string) error) { + o.lockFunc = f +} diff --git a/docs/data-sources/freeform_config_template.md b/docs/data-sources/freeform_config_template.md new file mode 100644 index 00000000..05401840 --- /dev/null +++ b/docs/data-sources/freeform_config_template.md @@ -0,0 +1,50 @@ +--- +page_title: "apstra_freeform_config_template Data Source - terraform-provider-apstra" +subcategory: "Reference Design: Freeform" +description: |- + This data source provides details of a specific Freeform Config Template. + At least one optional attribute is required. +--- + +# apstra_freeform_config_template (Data Source) + +This data source provides details of a specific Freeform Config Template. + +At least one optional attribute is required. + + +## Example Usage + +```terraform +resource "apstra_freeform_config_template" "test" { + blueprint_id = "freeform_blueprint-5ba09d07" + name = "test_conf_template.jinja" + tags = ["a", "b", "c"] + text = "this is a test for a config template." +} + +data "apstra_freeform_config_template" "test" { + blueprint_id = "freeform_blueprint-5ba09d07" + id = apstra_freeform_config_template.test.id +} + +output "test_out" {value = data.apstra_freeform_config_template.test} +``` + + +## Schema + +### Required + +- `blueprint_id` (String) Apstra Blueprint ID. Used to identify the Blueprint where the Config Template lives. + +### Optional + +- `id` (String) Populate this field to look up the Config Template by `id`. Required when `name` is omitted. +- `name` (String) Populate this field to look up an imported Config Template by `name`. Required when `id` is omitted. +- `tags` (Set of String) Set of Tag labels + +### Read-Only + +- `template_id` (String) The Template ID of the configuration template in global catalog +- `text` (String) Configuration Jinja2 template text diff --git a/docs/data-sources/freeform_property_set.md b/docs/data-sources/freeform_property_set.md new file mode 100644 index 00000000..5c0c1684 --- /dev/null +++ b/docs/data-sources/freeform_property_set.md @@ -0,0 +1,66 @@ +--- +page_title: "apstra_freeform_property_set Data Source - terraform-provider-apstra" +subcategory: "Reference Design: Freeform" +description: |- + This data source provides details of a specific Freeform PropertySet. + At least one optional attribute is required. +--- + +# apstra_freeform_property_set (Data Source) + +This data source provides details of a specific Freeform PropertySet. + +At least one optional attribute is required. + + +## Example Usage + +```terraform +# This example retrieves one property set from a blueprint + +# first we create a property set so we can use a data source to retrieve it. +resource "apstra_freeform_property_set" "prop_set_foo" { + blueprint_id = "freeform_blueprint-5ba09d07" + name = "prop_set_foo" + values = jsonencode({ + foo = "bar" + clown = 2 + }) +} + +# here we retrieve the property_set. + +data "apstra_freeform_property_set" "foo" { + blueprint_id = "freeform_blueprint-5ba09d07" + name = apstra_freeform_property_set.prop_set_foo.name +} + +#here we build an output block to display it. +output "foo" {value = data.apstra_freeform_property_set.foo} + +#Output looks like this +#foo = { +# "blueprint_id" = "freeform_blueprint-5ba09d07" +# "id" = tostring(null) +# "name" = "prop_set_foo" +# "system_id" = tostring(null) +# "values" = "{\"clown\": 2, \"foo\": \"bar\"}" +#} +``` + + +## Schema + +### Required + +- `blueprint_id` (String) Apstra Blueprint ID. Used to identify the Blueprint where the Property Set lives. + +### Optional + +- `id` (String) Populate this field to look up a Freeform Property Set by `id`. Required when `name` is omitted. +- `name` (String) Populate this field to look up an imported Property Set by `name`. Required when `id` is omitted. + +### Read-Only + +- `system_id` (String) The system ID where the Property Set is associated. +- `values` (String) A map of values in the Property Set in JSON format. diff --git a/docs/resources/freeform_config_template.md b/docs/resources/freeform_config_template.md new file mode 100644 index 00000000..e44293f0 --- /dev/null +++ b/docs/resources/freeform_config_template.md @@ -0,0 +1,50 @@ +--- +page_title: "apstra_freeform_config_template Resource - terraform-provider-apstra" +subcategory: "Reference Design: Freeform" +description: |- + This resource creates a Config Template in a Freeform Blueprint. +--- + +# apstra_freeform_config_template (Resource) + +This resource creates a Config Template in a Freeform Blueprint. + + +## Example Usage + +```terraform +resource "apstra_freeform_config_template" "test" { + blueprint_id = "freeform_blueprint-5ba09d07" + name = "test_conf_template.jinja" + tags = ["a", "b", "c"] + text = "this is a test for a config template." +} + +data "apstra_freeform_config_template" "test" { + blueprint_id = "freeform_blueprint-5ba09d07" + id = apstra_freeform_config_template.test.id +} + +output "test_out" {value = data.apstra_freeform_config_template.test} +``` + + +## Schema + +### Required + +- `blueprint_id` (String) Apstra Blueprint ID. +- `name` (String) Config Template name as shown in the Web UI. +- `text` (String) Configuration Jinja2 template text + +### Optional + +- `tags` (Set of String) Set of Tag labels +- `template_id` (String) The template ID of the config template in the global catalog. + +### Read-Only + +- `id` (String) ID of the Config Template. + + + diff --git a/docs/resources/freeform_property_set.md b/docs/resources/freeform_property_set.md new file mode 100644 index 00000000..2a70cc13 --- /dev/null +++ b/docs/resources/freeform_property_set.md @@ -0,0 +1,66 @@ +--- +page_title: "apstra_freeform_property_set Resource - terraform-provider-apstra" +subcategory: "Reference Design: Freeform" +description: |- + This resource creates a Property Set in a Freeform Blueprint. +--- + +# apstra_freeform_property_set (Resource) + +This resource creates a Property Set in a Freeform Blueprint. + + +## Example Usage + +```terraform +# Create a freeform property set resource. + +resource "apstra_freeform_property_set" "prop_set_foo" { + blueprint_id = "freeform_blueprint-5ba09d07" + name = "prop_set_foo" + values = jsonencode({ + foo = "bar" + clown = 2 + }) +} + +# Read the property set back with a data source. + +data "apstra_freeform_property_set" "foods" { + blueprint_id = "freeform_blueprint-5ba09d07" + name = apstra_freeform_property_set.prop_set_foo.name +} + +# Output the property set. + +output "foo" {value = data.apstra_freeform_property_set.foods} + +# Output should look like: +#foo = { +# "blueprint_id" = "freeform_blueprint-5ba09d07" +# "id" = tostring(null) +# "name" = "prop_set_foo" +# "system_id" = tostring(null) +# "values" = "{\"clown\": 2, \"foo\": \"bar\"}" +#} +``` + + +## Schema + +### Required + +- `blueprint_id` (String) Apstra Blueprint ID. +- `name` (String) Property Set name as shown in the Web UI. +- `values` (String) A map of values in the Property Set in JSON format. + +### Optional + +- `system_id` (String) The system ID where the Property Set is associated. + +### Read-Only + +- `id` (String) ID of the Property Set. + + + diff --git a/examples/data-sources/apstra_freeform_config_template/example.tf b/examples/data-sources/apstra_freeform_config_template/example.tf new file mode 100644 index 00000000..9b28335d --- /dev/null +++ b/examples/data-sources/apstra_freeform_config_template/example.tf @@ -0,0 +1,13 @@ +resource "apstra_freeform_config_template" "test" { + blueprint_id = "freeform_blueprint-5ba09d07" + name = "test_conf_template.jinja" + tags = ["a", "b", "c"] + text = "this is a test for a config template." +} + +data "apstra_freeform_config_template" "test" { + blueprint_id = "freeform_blueprint-5ba09d07" + id = apstra_freeform_config_template.test.id +} + +output "test_out" {value = data.apstra_freeform_config_template.test} diff --git a/examples/data-sources/apstra_freeform_property_set/example.tf b/examples/data-sources/apstra_freeform_property_set/example.tf new file mode 100644 index 00000000..723ac049 --- /dev/null +++ b/examples/data-sources/apstra_freeform_property_set/example.tf @@ -0,0 +1,31 @@ +# This example retrieves one property set from a blueprint + +# first we create a property set so we can use a data source to retrieve it. +resource "apstra_freeform_property_set" "prop_set_foo" { + blueprint_id = "freeform_blueprint-5ba09d07" + name = "prop_set_foo" + values = jsonencode({ + foo = "bar" + clown = 2 + }) +} + +# here we retrieve the property_set. + +data "apstra_freeform_property_set" "foo" { + blueprint_id = "freeform_blueprint-5ba09d07" + name = apstra_freeform_property_set.prop_set_foo.name +} + +#here we build an output block to display it. +output "foo" {value = data.apstra_freeform_property_set.foo} + +#Output looks like this +#foo = { +# "blueprint_id" = "freeform_blueprint-5ba09d07" +# "id" = tostring(null) +# "name" = "prop_set_foo" +# "system_id" = tostring(null) +# "values" = "{\"clown\": 2, \"foo\": \"bar\"}" +#} + diff --git a/examples/resources/apstra_freeform_config_template/example.tf b/examples/resources/apstra_freeform_config_template/example.tf new file mode 100644 index 00000000..9b28335d --- /dev/null +++ b/examples/resources/apstra_freeform_config_template/example.tf @@ -0,0 +1,13 @@ +resource "apstra_freeform_config_template" "test" { + blueprint_id = "freeform_blueprint-5ba09d07" + name = "test_conf_template.jinja" + tags = ["a", "b", "c"] + text = "this is a test for a config template." +} + +data "apstra_freeform_config_template" "test" { + blueprint_id = "freeform_blueprint-5ba09d07" + id = apstra_freeform_config_template.test.id +} + +output "test_out" {value = data.apstra_freeform_config_template.test} diff --git a/examples/resources/apstra_freeform_property_set/example.tf b/examples/resources/apstra_freeform_property_set/example.tf new file mode 100644 index 00000000..6fe62bc0 --- /dev/null +++ b/examples/resources/apstra_freeform_property_set/example.tf @@ -0,0 +1,30 @@ +# Create a freeform property set resource. + +resource "apstra_freeform_property_set" "prop_set_foo" { + blueprint_id = "freeform_blueprint-5ba09d07" + name = "prop_set_foo" + values = jsonencode({ + foo = "bar" + clown = 2 + }) +} + +# Read the property set back with a data source. + +data "apstra_freeform_property_set" "foods" { + blueprint_id = "freeform_blueprint-5ba09d07" + name = apstra_freeform_property_set.prop_set_foo.name +} + +# Output the property set. + +output "foo" {value = data.apstra_freeform_property_set.foods} + +# Output should look like: +#foo = { +# "blueprint_id" = "freeform_blueprint-5ba09d07" +# "id" = tostring(null) +# "name" = "prop_set_foo" +# "system_id" = tostring(null) +# "values" = "{\"clown\": 2, \"foo\": \"bar\"}" +#} diff --git a/go.mod b/go.mod index 2892d184..fb935ded 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ toolchain go1.21.1 require ( github.com/IBM/netaddr v1.5.0 - github.com/Juniper/apstra-go-sdk v0.0.0-20240628202621-f1748f44d60c + github.com/Juniper/apstra-go-sdk v0.0.0-20240701173510-5c1fd83e151f 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 cf0a3fff..9d6b069e 100644 --- a/go.sum +++ b/go.sum @@ -108,8 +108,8 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= 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-20240628202621-f1748f44d60c h1:QV5sZaDgkxa7BZxWs+dlCrZd6fvn3B+uc071sS7V5Lk= -github.com/Juniper/apstra-go-sdk v0.0.0-20240628202621-f1748f44d60c/go.mod h1:Xwj3X8v/jRZWv28o6vQAqD4lz2JmzaSYLZ2ch1SS89w= +github.com/Juniper/apstra-go-sdk v0.0.0-20240701173510-5c1fd83e151f h1:xmPPD2XieSkuu4robT6u5/zVXKJ3JTQ9suFw5GPyzpk= +github.com/Juniper/apstra-go-sdk v0.0.0-20240701173510-5c1fd83e151f/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 5d8617e6302713b53de695b514a20a482d2850d7 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Mon, 1 Jul 2024 20:16:20 -0400 Subject: [PATCH 04/18] tests, etc... --- apstra/blueprint/freeform_config_template.go | 29 ++- .../data_source_freeform_config_template.go | 2 + apstra/export_test.go | 1 + ...eeform_config_template_integration_test.go | 170 ++++++++++++++++++ apstra/test_helpers_test.go | 8 + apstra/test_utils/blueprint.go | 15 ++ go.mod | 2 +- go.sum | 4 +- 8 files changed, 209 insertions(+), 22 deletions(-) create mode 100644 apstra/resource_freeform_config_template_integration_test.go diff --git a/apstra/blueprint/freeform_config_template.go b/apstra/blueprint/freeform_config_template.go index 1d3239e6..b0511d9e 100644 --- a/apstra/blueprint/freeform_config_template.go +++ b/apstra/blueprint/freeform_config_template.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "regexp" ) type FreeformConfigTemplate struct { @@ -21,7 +22,6 @@ type FreeformConfigTemplate struct { BlueprintId types.String `tfsdk:"blueprint_id"` Name types.String `tfsdk:"name"` Text types.String `tfsdk:"text"` - TemplateId types.String `tfsdk:"template_id"` Tags types.Set `tfsdk:"tags"` } @@ -45,11 +45,6 @@ func (o FreeformConfigTemplate) DataSourceAttributes() map[string]dataSourceSche }...), }, }, - "template_id": dataSourceSchema.StringAttribute{ - MarkdownDescription: "The Template ID of the configuration template in global catalog\n", - Computed: true, - Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, - }, "name": dataSourceSchema.StringAttribute{ MarkdownDescription: "Populate this field to look up an imported Config Template by `name`. Required when `id` is omitted.", Optional: true, @@ -81,17 +76,15 @@ func (o FreeformConfigTemplate) ResourceAttributes() map[string]resourceSchema.A "id": resourceSchema.StringAttribute{ MarkdownDescription: "ID of the Config Template.", Computed: true, - }, - "template_id": resourceSchema.StringAttribute{ - MarkdownDescription: "The template ID of the config template in the global catalog.", - Optional: true, - Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, - PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, }, "name": resourceSchema.StringAttribute{ - MarkdownDescription: "Config Template name as shown in the Web UI.", + MarkdownDescription: "Config Template name as shown in the Web UI. Must end with `.jinja`.", Required: true, - Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(7), + stringvalidator.RegexMatches(regexp.MustCompile(".jinja$"), "must end with '.jinja'"), + }, }, "text": resourceSchema.StringAttribute{ MarkdownDescription: "Configuration Jinja2 template text", @@ -115,16 +108,14 @@ func (o *FreeformConfigTemplate) Request(ctx context.Context, diags *diag.Diagno } return &apstra.ConfigTemplateData{ - Label: o.Name.ValueString(), - Text: o.Text.ValueString(), - Tags: tags, - TemplateId: apstra.ObjectId(o.TemplateId.ValueString()), + Label: o.Name.ValueString(), + Text: o.Text.ValueString(), + Tags: tags, } } func (o *FreeformConfigTemplate) LoadApiData(ctx context.Context, in *apstra.ConfigTemplateData, diags *diag.Diagnostics) { o.Name = types.StringValue(in.Label) o.Text = types.StringValue(in.Text) - o.TemplateId = types.StringValue(string(in.TemplateId)) o.Tags = utils.SetValueOrNull(ctx, types.StringType, in.Tags, diags) // safe to ignore diagnostic here } diff --git a/apstra/data_source_freeform_config_template.go b/apstra/data_source_freeform_config_template.go index 58644d8c..ffa11364 100644 --- a/apstra/data_source_freeform_config_template.go +++ b/apstra/data_source_freeform_config_template.go @@ -9,6 +9,7 @@ import ( "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 = &dataSourceFreeformConfigTemplate{} @@ -81,6 +82,7 @@ func (o *dataSourceFreeformConfigTemplate) Read(ctx context.Context, req datasou return } + config.Id = types.StringValue(api.Id.String()) config.LoadApiData(ctx, api.Data, &resp.Diagnostics) if resp.Diagnostics.HasError() { return diff --git a/apstra/export_test.go b/apstra/export_test.go index 01f0ba1d..551baf52 100644 --- a/apstra/export_test.go +++ b/apstra/export_test.go @@ -10,6 +10,7 @@ var ( ResourceAgentProfile = resourceAgentProfile{} ResourceDatacenterGenericSystem = resourceDatacenterGenericSystem{} ResourceDatacenterRoutingZone = resourceDatacenterRoutingZone{} + ResourceFreeformConfigTemplate = resourceFreeformConfigTemplate{} ResourceIpv4Pool = resourceIpv4Pool{} ResourceTemplatePodBased = resourceTemplatePodBased{} ResourceTemplateCollapsed = resourceTemplateCollapsed{} diff --git a/apstra/resource_freeform_config_template_integration_test.go b/apstra/resource_freeform_config_template_integration_test.go new file mode 100644 index 00000000..cb95882f --- /dev/null +++ b/apstra/resource_freeform_config_template_integration_test.go @@ -0,0 +1,170 @@ +//go:build integration + +package tfapstra_test + +import ( + "context" + "fmt" + 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" + "math/rand" + "strconv" + "testing" +) + +const ( + resourceFreeformConfigTemplateHcl = ` +resource %q %q { + blueprint_id = %q + name = %q + text = %q + tags = %s +} +` +) + +type resourceFreeformConfigTemplate struct { + blueprintId string + name string + text string + tags []string +} + +func (o resourceFreeformConfigTemplate) render(rType, rName string) string { + return fmt.Sprintf(resourceFreeformConfigTemplateHcl, + rType, rName, + o.blueprintId, + o.name, + o.text, + stringSetOrNull(o.tags), + ) +} + +func (o resourceFreeformConfigTemplate) 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", "text", o.text) + + 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) + } + } + + return result +} + +func TestResourceFreeformConfigTemplate(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) + + type testStep struct { + config resourceFreeformConfigTemplate + } + type testCase struct { + apiVersionConstraints version.Constraints + steps []testStep + } + + testCases := map[string]testCase{ + "start_with_no_tags": { + steps: []testStep{ + { + config: resourceFreeformConfigTemplate{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6) + ".jinja", + text: acctest.RandString(6), + }, + }, + { + config: resourceFreeformConfigTemplate{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6) + ".jinja", + text: acctest.RandString(6), + tags: randomStrings(rand.Intn(10)+2, 6), + }, + }, + { + config: resourceFreeformConfigTemplate{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6) + ".jinja", + text: acctest.RandString(6), + }, + }, + }, + }, + "start_with_tags": { + steps: []testStep{ + { + config: resourceFreeformConfigTemplate{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6) + ".jinja", + text: acctest.RandString(6), + tags: randomStrings(rand.Intn(10)+2, 6), + }, + }, + { + config: resourceFreeformConfigTemplate{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6) + ".jinja", + text: acctest.RandString(6), + }, + }, + { + config: resourceFreeformConfigTemplate{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6) + ".jinja", + text: acctest.RandString(6), + tags: randomStrings(rand.Intn(10)+2, 6), + }, + }, + }, + }, + } + + resourceType := tfapstra.ResourceName(ctx, &tfapstra.ResourceFreeformConfigTemplate) + + 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/apstra/test_helpers_test.go b/apstra/test_helpers_test.go index 2f8ac6e0..956fe33f 100644 --- a/apstra/test_helpers_test.go +++ b/apstra/test_helpers_test.go @@ -219,6 +219,14 @@ func randomIPs(t testing.TB, n int, ipv4Cidr, ipv6Cidr string) []string { return result } +func randomStrings(strCount int, strLen int) []string { + result := make([]string, strCount) + for i := 0; i < strCount; i++ { + result[i] = acctest.RandString(strLen) + } + return result +} + type lineNumberer struct { lines []string base int diff --git a/apstra/test_utils/blueprint.go b/apstra/test_utils/blueprint.go index dfd54e52..99f03d35 100644 --- a/apstra/test_utils/blueprint.go +++ b/apstra/test_utils/blueprint.go @@ -228,3 +228,18 @@ func BlueprintF(t testing.TB, ctx context.Context) *apstra.TwoStageL3ClosClient return bpClient } + +func FfBlueprintA(t testing.TB, ctx context.Context) *apstra.FreeformClient { + t.Helper() + + client := GetTestClient(t, ctx) + + id, err := client.CreateFreeformBlueprint(ctx, acctest.RandString(6)) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, client.DeleteBlueprint(ctx, id)) }) + + bpClient, err := client.NewFreeformClient(ctx, id) + require.NoError(t, err) + + return bpClient +} diff --git a/go.mod b/go.mod index fb935ded..7791be07 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ toolchain go1.21.1 require ( github.com/IBM/netaddr v1.5.0 - github.com/Juniper/apstra-go-sdk v0.0.0-20240701173510-5c1fd83e151f + github.com/Juniper/apstra-go-sdk v0.0.0-20240701235719-e1497887d8d7 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 9d6b069e..cfa70871 100644 --- a/go.sum +++ b/go.sum @@ -108,8 +108,8 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= 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-20240701173510-5c1fd83e151f h1:xmPPD2XieSkuu4robT6u5/zVXKJ3JTQ9suFw5GPyzpk= -github.com/Juniper/apstra-go-sdk v0.0.0-20240701173510-5c1fd83e151f/go.mod h1:Xwj3X8v/jRZWv28o6vQAqD4lz2JmzaSYLZ2ch1SS89w= +github.com/Juniper/apstra-go-sdk v0.0.0-20240701235719-e1497887d8d7 h1:omV9UgPnuSppETNuMqTqvNBXFXy8ExyG5rZUnMe7UgY= +github.com/Juniper/apstra-go-sdk v0.0.0-20240701235719-e1497887d8d7/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 c00a71166ef932f4b89661807c7d4bcc3873f416 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Mon, 1 Jul 2024 20:38:52 -0400 Subject: [PATCH 05/18] update docs --- docs/data-sources/datacenter_configlet.md | 7 +-- docs/data-sources/freeform_config_template.md | 17 ++----- docs/resources/freeform_config_template.md | 45 ++++++++++++++----- .../apstra_datacenter_configlet/example.tf | 7 +-- .../example.tf | 16 ++----- .../example.tf | 42 +++++++++++++---- 6 files changed, 83 insertions(+), 51 deletions(-) diff --git a/docs/data-sources/datacenter_configlet.md b/docs/data-sources/datacenter_configlet.md index 464182f6..01974ba1 100644 --- a/docs/data-sources/datacenter_configlet.md +++ b/docs/data-sources/datacenter_configlet.md @@ -16,10 +16,11 @@ At least one optional attribute is required. ## Example Usage ```terraform -# This example uses the `apstra_datacenter_configlets` data source to get a list -# of all imported configlets, and then uses the apstra_datacenter_configlet data source -# to inspect the results +data "apstra_freeform_config_template" "interfaces" { + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + name = "interfaces.jinja" +} data "apstra_datacenter_blueprint" "b" { name = "test" diff --git a/docs/data-sources/freeform_config_template.md b/docs/data-sources/freeform_config_template.md index 05401840..15a21057 100644 --- a/docs/data-sources/freeform_config_template.md +++ b/docs/data-sources/freeform_config_template.md @@ -16,19 +16,11 @@ At least one optional attribute is required. ## Example Usage ```terraform -resource "apstra_freeform_config_template" "test" { - blueprint_id = "freeform_blueprint-5ba09d07" - name = "test_conf_template.jinja" - tags = ["a", "b", "c"] - text = "this is a test for a config template." +# The following example retrieves a Config Template from a Freeform Blueprint +data "apstra_freeform_config_template" "interfaces" { + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + name = "interfaces.jinja" } - -data "apstra_freeform_config_template" "test" { - blueprint_id = "freeform_blueprint-5ba09d07" - id = apstra_freeform_config_template.test.id -} - -output "test_out" {value = data.apstra_freeform_config_template.test} ``` @@ -46,5 +38,4 @@ output "test_out" {value = data.apstra_freeform_config_template.test} ### Read-Only -- `template_id` (String) The Template ID of the configuration template in global catalog - `text` (String) Configuration Jinja2 template text diff --git a/docs/resources/freeform_config_template.md b/docs/resources/freeform_config_template.md index e44293f0..46b5c33f 100644 --- a/docs/resources/freeform_config_template.md +++ b/docs/resources/freeform_config_template.md @@ -13,19 +13,43 @@ This resource creates a Config Template in a Freeform Blueprint. ## Example Usage ```terraform -resource "apstra_freeform_config_template" "test" { - blueprint_id = "freeform_blueprint-5ba09d07" - name = "test_conf_template.jinja" - tags = ["a", "b", "c"] - text = "this is a test for a config template." +# This example creates a Config Template from a local jinja file. +locals { + template_filename = "interfaces.jinja" } -data "apstra_freeform_config_template" "test" { - blueprint_id = "freeform_blueprint-5ba09d07" - id = apstra_freeform_config_template.test.id +resource "apstra_freeform_config_template" "test" { + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + name = local.template_filename + tags = ["a", "b", "c"] + text = file("${path.module}/${local.template_filename}") } -output "test_out" {value = data.apstra_freeform_config_template.test} +/* +# Contents of the interfaces.jinja file in this directory follows here: +{% set this_router=hostname %} +interfaces { +{% for interface_name, iface in interfaces.items() %} + replace: {{ interface_name }} { + unit 0 { + description "{{iface['description']}}"; + {% if iface['ipv4_address'] and iface['ipv4_prefixlen'] %} + family inet { + address {{iface['ipv4_address']}}/{{iface['ipv4_prefixlen']}}; + } + {% endif %} + } + } +{% endfor %} + replace: lo0 { + unit 0 { + family inet { + address {{ property_sets.data[this_router | replace('-', '_')]['loopback'] }}/32; + } + } + } +} +*/ ``` @@ -34,13 +58,12 @@ output "test_out" {value = data.apstra_freeform_config_template.test} ### Required - `blueprint_id` (String) Apstra Blueprint ID. -- `name` (String) Config Template name as shown in the Web UI. +- `name` (String) Config Template name as shown in the Web UI. Must end with `.jinja`. - `text` (String) Configuration Jinja2 template text ### Optional - `tags` (Set of String) Set of Tag labels -- `template_id` (String) The template ID of the config template in the global catalog. ### Read-Only diff --git a/examples/data-sources/apstra_datacenter_configlet/example.tf b/examples/data-sources/apstra_datacenter_configlet/example.tf index 28d4dc17..2ba35ac4 100644 --- a/examples/data-sources/apstra_datacenter_configlet/example.tf +++ b/examples/data-sources/apstra_datacenter_configlet/example.tf @@ -1,7 +1,8 @@ -# This example uses the `apstra_datacenter_configlets` data source to get a list -# of all imported configlets, and then uses the apstra_datacenter_configlet data source -# to inspect the results +data "apstra_freeform_config_template" "interfaces" { + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + name = "interfaces.jinja" +} data "apstra_datacenter_blueprint" "b" { name = "test" diff --git a/examples/data-sources/apstra_freeform_config_template/example.tf b/examples/data-sources/apstra_freeform_config_template/example.tf index 9b28335d..08f1b55f 100644 --- a/examples/data-sources/apstra_freeform_config_template/example.tf +++ b/examples/data-sources/apstra_freeform_config_template/example.tf @@ -1,13 +1,5 @@ -resource "apstra_freeform_config_template" "test" { - blueprint_id = "freeform_blueprint-5ba09d07" - name = "test_conf_template.jinja" - tags = ["a", "b", "c"] - text = "this is a test for a config template." +# The following example retrieves a Config Template from a Freeform Blueprint +data "apstra_freeform_config_template" "interfaces" { + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + name = "interfaces.jinja" } - -data "apstra_freeform_config_template" "test" { - blueprint_id = "freeform_blueprint-5ba09d07" - id = apstra_freeform_config_template.test.id -} - -output "test_out" {value = data.apstra_freeform_config_template.test} diff --git a/examples/resources/apstra_freeform_config_template/example.tf b/examples/resources/apstra_freeform_config_template/example.tf index 9b28335d..7223fe6d 100644 --- a/examples/resources/apstra_freeform_config_template/example.tf +++ b/examples/resources/apstra_freeform_config_template/example.tf @@ -1,13 +1,37 @@ -resource "apstra_freeform_config_template" "test" { - blueprint_id = "freeform_blueprint-5ba09d07" - name = "test_conf_template.jinja" - tags = ["a", "b", "c"] - text = "this is a test for a config template." +# This example creates a Config Template from a local jinja file. +locals { + template_filename = "interfaces.jinja" } -data "apstra_freeform_config_template" "test" { - blueprint_id = "freeform_blueprint-5ba09d07" - id = apstra_freeform_config_template.test.id +resource "apstra_freeform_config_template" "test" { + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + name = local.template_filename + tags = ["a", "b", "c"] + text = file("${path.module}/${local.template_filename}") } -output "test_out" {value = data.apstra_freeform_config_template.test} +/* +# Contents of the interfaces.jinja file in this directory follows here: +{% set this_router=hostname %} +interfaces { +{% for interface_name, iface in interfaces.items() %} + replace: {{ interface_name }} { + unit 0 { + description "{{iface['description']}}"; + {% if iface['ipv4_address'] and iface['ipv4_prefixlen'] %} + family inet { + address {{iface['ipv4_address']}}/{{iface['ipv4_prefixlen']}}; + } + {% endif %} + } + } +{% endfor %} + replace: lo0 { + unit 0 { + family inet { + address {{ property_sets.data[this_router | replace('-', '_')]['loopback'] }}/32; + } + } + } +} +*/ \ No newline at end of file From ad0baa3c66542825b2d635cf00b5d5f3c7fb3a09 Mon Sep 17 00:00:00 2001 From: bwJuniper <98770420+bwJuniper@users.noreply.github.com> Date: Tue, 2 Jul 2024 23:04:29 +0200 Subject: [PATCH 06/18] initial working freeform system --- apstra/blueprint/freeform_system.go | 175 +++++++++++++++ apstra/data_source_freeform_system.go | 97 ++++++++ apstra/export_test.go | 1 + apstra/provider.go | 2 + ..._freeform_property_set_integration_test.go | 123 ++++++++++ apstra/resource_freeform_system.go | 210 ++++++++++++++++++ apstra/test_helpers_test.go | 17 ++ go.mod | 2 +- go.sum | 4 +- 9 files changed, 628 insertions(+), 3 deletions(-) create mode 100644 apstra/blueprint/freeform_system.go create mode 100644 apstra/data_source_freeform_system.go create mode 100644 apstra/resource_freeform_property_set_integration_test.go create mode 100644 apstra/resource_freeform_system.go diff --git a/apstra/blueprint/freeform_system.go b/apstra/blueprint/freeform_system.go new file mode 100644 index 00000000..919939ed --- /dev/null +++ b/apstra/blueprint/freeform_system.go @@ -0,0 +1,175 @@ +package blueprint + +import ( + "context" + "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" + "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 FreeformSystem struct { + BlueprintId types.String `tfsdk:"blueprint_id"` + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + DeviceProfileId types.String `tfsdk:"device_profile_id"` + Hostname types.String `tfsdk:"hostname"` + Type types.String `tfsdk:"type"` + SystemId types.String `tfsdk:"system_id"` + DeployMode types.String `tfsdk:"deploy_mode"` + Tags types.Set `tfsdk:"tags"` +} + +func (o FreeformSystem) 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)}, + }, + "hostname": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Hostname of the System", + Computed: true, + }, + "deploy_mode": dataSourceSchema.StringAttribute{ + MarkdownDescription: "deploy mode of the System", + Computed: true, + }, + "type": dataSourceSchema.StringAttribute{ + MarkdownDescription: "type of the System, either Internal or External", + Computed: true, + }, + "device_profile_id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "device profile ID of the System", + Computed: true, + }, + "system_id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Device System ID assigned to the System", + Computed: true, + }, + "tags": dataSourceSchema.SetAttribute{ + MarkdownDescription: "Set of Tag labels", + ElementType: types.StringType, + Optional: true, + }, + } +} + +func (o FreeformSystem) 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.LengthAtLeast(1), + }, + }, + "hostname": resourceSchema.StringAttribute{ + MarkdownDescription: "Hostname of the Freeform System.", + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "deploy_mode": dataSourceSchema.StringAttribute{ + MarkdownDescription: "deploy mode of the System", + Optional: true, + Validators: []validator.String{stringvalidator.OneOf(utils.AllNodeDeployModes()...)}, + }, + "type": dataSourceSchema.StringAttribute{ + MarkdownDescription: "type of the System, either Internal or External", + Required: true, + Validators: []validator.String{stringvalidator.OneOf(apstra.SystemTypeInternal.String(), apstra.SystemTypeExternal.String())}, + }, + "device_profile_id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "device profile ID of the System", + Optional: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "system_id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Device System ID assigned to the System", + 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)}, + }, + } +} + +func (o *FreeformSystem) Request(ctx context.Context, diags *diag.Diagnostics) *apstra.FreeformSystemData { + var tags []string + diags.Append(o.Tags.ElementsAs(ctx, &tags, false)...) + if diags.HasError() { + return nil + } + + var systemType apstra.SystemType + switch o.Type.ValueString() { + case apstra.SystemTypeExternal.String(): + systemType = apstra.SystemTypeExternal + case apstra.SystemTypeInternal.String(): + systemType = apstra.SystemTypeInternal + default: + diags.AddError("unexpected system type", "got: "+o.Type.ValueString()) + } + + return &apstra.FreeformSystemData{ + SystemId: (*apstra.ObjectId)(o.SystemId.ValueStringPointer()), + Type: systemType, + Label: o.Name.ValueString(), + Hostname: o.Hostname.ValueString(), + Tags: tags, + DeviceProfileId: apstra.ObjectId(o.DeviceProfileId.ValueString()), + } +} + +func (o *FreeformSystem) LoadApiData(ctx context.Context, in *apstra.FreeformSystemData, diags *diag.Diagnostics) { + o.Name = types.StringValue(in.Label) + o.Hostname = types.StringValue(in.Hostname) + o.Type = types.StringValue(string(rune(in.Type))) + o.DeviceProfileId = types.StringValue(string(in.DeviceProfileId)) + o.SystemId = types.StringPointerValue((*string)(in.SystemId)) + o.Tags = utils.SetValueOrNull(ctx, types.StringType, in.Tags, diags) // safe to ignore diagnostic here +} diff --git a/apstra/data_source_freeform_system.go b/apstra/data_source_freeform_system.go new file mode 100644 index 00000000..6bb560d2 --- /dev/null +++ b/apstra/data_source_freeform_system.go @@ -0,0 +1,97 @@ +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 = &dataSourceFreeformSystem{} +var _ datasourceWithSetFfBpClientFunc = &dataSourceFreeformSystem{} + +type dataSourceFreeformSystem struct { + getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) +} + +func (o *dataSourceFreeformSystem) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_freeform_system" +} + +func (o *dataSourceFreeformSystem) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + configureDataSource(ctx, o, req, resp) +} + +func (o *dataSourceFreeformSystem) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: docCategoryFreeform + "This data source provides details of a specific Freeform Config Template.\n\n" + + "At least one optional attribute is required.", + Attributes: blueprint.FreeformSystem{}.DataSourceAttributes(), + } +} +func (o *dataSourceFreeformSystem) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config blueprint.FreeformSystem + 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.FreeformSystem + switch { + case !config.Id.IsNull(): + api, err = bp.GetSystem(ctx, apstra.ObjectId(config.Id.ValueString())) + if utils.IsApstra404(err) { + resp.Diagnostics.AddAttributeError( + path.Root("id"), + "Freeform System not found", + fmt.Sprintf("Freeform System with ID %s not found", config.Id)) + return + } + case !config.Name.IsNull(): + api, err = bp.GetSystemByName(ctx, config.Name.ValueString()) + if utils.IsApstra404(err) { + resp.Diagnostics.AddAttributeError( + path.Root("name"), + "Freeform System not found", + fmt.Sprintf("Freeform System with Name %s not found", config.Name)) + return + } + } + if err != nil { + resp.Diagnostics.AddError("failed reading Freeform System", err.Error()) + return + } + if api.Data == nil { + resp.Diagnostics.AddError("failed reading Freeform System", "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 *dataSourceFreeformSystem) 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 551baf52..5e1e15e7 100644 --- a/apstra/export_test.go +++ b/apstra/export_test.go @@ -11,6 +11,7 @@ var ( ResourceDatacenterGenericSystem = resourceDatacenterGenericSystem{} ResourceDatacenterRoutingZone = resourceDatacenterRoutingZone{} ResourceFreeformConfigTemplate = resourceFreeformConfigTemplate{} + ResourceFreeformPropertySet = resourceFreeformPropertySet{} ResourceIpv4Pool = resourceIpv4Pool{} ResourceTemplatePodBased = resourceTemplatePodBased{} ResourceTemplateCollapsed = resourceTemplateCollapsed{} diff --git a/apstra/provider.go b/apstra/provider.go index 8c95ffe9..17405599 100644 --- a/apstra/provider.go +++ b/apstra/provider.go @@ -531,6 +531,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource func() datasource.DataSource { return &dataSourceDeviceConfig{} }, func() datasource.DataSource { return &dataSourceFreeformConfigTemplate{} }, func() datasource.DataSource { return &dataSourceFreeformPropertySet{} }, + func() datasource.DataSource { return &dataSourceFreeformSystem{} }, func() datasource.DataSource { return &dataSourceIntegerPool{} }, func() datasource.DataSource { return &dataSourceInterfacesByLinkTag{} }, func() datasource.DataSource { return &dataSourceInterfacesBySystem{} }, @@ -584,6 +585,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { func() resource.Resource { return &resourceDeviceAllocation{} }, func() resource.Resource { return &resourceFreeformConfigTemplate{} }, func() resource.Resource { return &resourceFreeformPropertySet{} }, + func() resource.Resource { return &resourceFreeformSystem{} }, func() resource.Resource { return &resourceIntegerPool{} }, func() resource.Resource { return &resourceInterfaceMap{} }, func() resource.Resource { return &resourceIpv4Pool{} }, diff --git a/apstra/resource_freeform_property_set_integration_test.go b/apstra/resource_freeform_property_set_integration_test.go new file mode 100644 index 00000000..787afc8d --- /dev/null +++ b/apstra/resource_freeform_property_set_integration_test.go @@ -0,0 +1,123 @@ +//go:build integration + +package tfapstra_test + +import ( + "context" + "encoding/json" + "fmt" + 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" + "testing" +) + +const ( + resourceFreeformPropertySetHcl = ` +resource %q %q { + blueprint_id = %q + system_id = %s + name = %q + values = %q +} +` +) + +type resourceFreeformPropertySet struct { + blueprintId string + name string + system_id string + values json.RawMessage +} + +func (o resourceFreeformPropertySet) render(rType, rName string) string { + return fmt.Sprintf(resourceFreeformPropertySetHcl, + rType, rName, + o.blueprintId, + stringOrNull(o.system_id), + o.name, + string(o.values), + ) +} + +func (o resourceFreeformPropertySet) 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", "values", string(o.values)) + + if o.system_id != "" { + result.append(t, "TestCheckResourceAttr", "system_id", o.system_id) + } + + return result +} + +func TestResourceFreeformPropertySet(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) + + type testStep struct { + config resourceFreeformPropertySet + } + type testCase struct { + apiVersionConstraints version.Constraints + steps []testStep + } + testCases := map[string]testCase{ + "simple_test": { + steps: []testStep{ + { + config: resourceFreeformPropertySet{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + values: randomJson(t, 6, 12, 25), + }, + }, + }, + }, + } + + resourceType := tfapstra.ResourceName(ctx, &tfapstra.ResourceFreeformPropertySet) + + 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/apstra/resource_freeform_system.go b/apstra/resource_freeform_system.go new file mode 100644 index 00000000..bc88c665 --- /dev/null +++ b/apstra/resource_freeform_system.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 = &resourceFreeformSystem{} +var _ resourceWithSetFfBpClientFunc = &resourceFreeformSystem{} +var _ resourceWithSetBpLockFunc = &resourceFreeformSystem{} + +type resourceFreeformSystem struct { + getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) + lockFunc func(context.Context, string) error +} + +func (o *resourceFreeformSystem) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_freeform_system" +} + +func (o *resourceFreeformSystem) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + configureResource(ctx, o, req, resp) +} + +func (o *resourceFreeformSystem) 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.FreeformSystem{}.ResourceAttributes(), + } +} + +func (o *resourceFreeformSystem) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan blueprint.FreeformSystem + 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 *resourceFreeformSystem) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state blueprint.FreeformSystem + 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)...) +} + +// Update resource +func (o *resourceFreeformSystem) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Get plan values + var plan blueprint.FreeformSystem + 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)...) +} + +// Delete resource +func (o *resourceFreeformSystem) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state blueprint.FreeformSystem + 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 + } + + // 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 *resourceFreeformSystem) setBpClientFunc(f func(context.Context, string) (*apstra.FreeformClient, error)) { + o.getBpClientFunc = f +} + +func (o *resourceFreeformSystem) setBpLockFunc(f func(context.Context, string) error) { + o.lockFunc = f +} diff --git a/apstra/test_helpers_test.go b/apstra/test_helpers_test.go index 956fe33f..6f716940 100644 --- a/apstra/test_helpers_test.go +++ b/apstra/test_helpers_test.go @@ -4,6 +4,7 @@ package tfapstra_test import ( "context" + "encoding/json" "fmt" "math" "math/rand" @@ -227,6 +228,22 @@ func randomStrings(strCount int, strLen int) []string { return result } +func randomJson(t testing.TB, maxInt int, strLen int, count int) json.RawMessage { + t.Helper() + preResult := make(map[string]any) + for i := 0; i < count; i++ { + if rand.Int()%2 == 0 { + preResult["a"+acctest.RandString(strLen-1)] = rand.Intn(maxInt) + } else { + preResult["a"+acctest.RandString(strLen-1)] = acctest.RandString(strLen) + } + } + marshalResult, err := json.Marshal(&preResult) + require.NoError(t, err) + + return marshalResult +} + type lineNumberer struct { lines []string base int diff --git a/go.mod b/go.mod index 7791be07..887c0b06 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ toolchain go1.21.1 require ( github.com/IBM/netaddr v1.5.0 - github.com/Juniper/apstra-go-sdk v0.0.0-20240701235719-e1497887d8d7 + github.com/Juniper/apstra-go-sdk v0.0.0-20240702201035-23330826fe39 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 cfa70871..cf63b353 100644 --- a/go.sum +++ b/go.sum @@ -108,8 +108,8 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= 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-20240701235719-e1497887d8d7 h1:omV9UgPnuSppETNuMqTqvNBXFXy8ExyG5rZUnMe7UgY= -github.com/Juniper/apstra-go-sdk v0.0.0-20240701235719-e1497887d8d7/go.mod h1:Xwj3X8v/jRZWv28o6vQAqD4lz2JmzaSYLZ2ch1SS89w= +github.com/Juniper/apstra-go-sdk v0.0.0-20240702201035-23330826fe39 h1:BnX79x6kyLpJg6AT+xI8T8OiWUlmc1mUX+jU9AV+f+w= +github.com/Juniper/apstra-go-sdk v0.0.0-20240702201035-23330826fe39/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 a16065719c88c4ba890889f83060584a5395fd51 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Tue, 2 Jul 2024 21:28:41 -0400 Subject: [PATCH 07/18] brief review --- apstra/blueprint/freeform_config_template.go | 8 ++-- apstra/blueprint/freeform_property_set.go | 5 ++- apstra/blueprint/freeform_system.go | 23 +++++------ apstra/configure_resource.go | 4 -- .../data_source_freeform_config_template.go | 1 + apstra/data_source_freeform_property_set.go | 4 +- apstra/data_source_freeform_system.go | 2 +- apstra/resource_freeform_config_template.go | 5 +-- ...eeform_config_template_integration_test.go | 6 +-- apstra/resource_freeform_property_set.go | 6 +-- ..._freeform_property_set_integration_test.go | 8 ++-- apstra/resource_freeform_system.go | 6 +-- apstra/test_helpers_test.go | 8 ++-- docs/data-sources/freeform_config_template.md | 6 +-- docs/data-sources/freeform_property_set.md | 8 ++-- docs/data-sources/freeform_system.md | 41 +++++++++++++++++++ docs/resources/freeform_system.md | 41 +++++++++++++++++++ .../apstra_freeform_system/example.tf | 1 + .../apstra_freeform_system/example.tf | 1 + 19 files changed, 129 insertions(+), 55 deletions(-) create mode 100644 docs/data-sources/freeform_system.md create mode 100644 docs/resources/freeform_system.md create mode 100644 examples/data-sources/apstra_freeform_system/example.tf create mode 100644 examples/resources/apstra_freeform_system/example.tf diff --git a/apstra/blueprint/freeform_config_template.go b/apstra/blueprint/freeform_config_template.go index b0511d9e..14b4020a 100644 --- a/apstra/blueprint/freeform_config_template.go +++ b/apstra/blueprint/freeform_config_template.go @@ -34,7 +34,7 @@ func (o FreeformConfigTemplate) DataSourceAttributes() map[string]dataSourceSche Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, "id": dataSourceSchema.StringAttribute{ - MarkdownDescription: "Populate this field to look up the Config Template by `id`. Required when `name` is omitted.", + MarkdownDescription: "Populate this field to look up the Config Template by ID. Required when `name` is omitted.", Optional: true, Computed: true, Validators: []validator.String{ @@ -46,7 +46,7 @@ func (o FreeformConfigTemplate) DataSourceAttributes() map[string]dataSourceSche }, }, "name": dataSourceSchema.StringAttribute{ - MarkdownDescription: "Populate this field to look up an imported Config Template by `name`. Required when `id` is omitted.", + MarkdownDescription: "Populate this field to look up an imported Config Template by Name. Required when `id` is omitted.", Optional: true, Computed: true, Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, @@ -54,13 +54,11 @@ func (o FreeformConfigTemplate) DataSourceAttributes() map[string]dataSourceSche "text": dataSourceSchema.StringAttribute{ MarkdownDescription: "Configuration Jinja2 template text", Computed: true, - Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, "tags": dataSourceSchema.SetAttribute{ MarkdownDescription: "Set of Tag labels", ElementType: types.StringType, - Optional: true, - Validators: []validator.Set{setvalidator.SizeAtLeast(1)}, + Computed: true, }, } } diff --git a/apstra/blueprint/freeform_property_set.go b/apstra/blueprint/freeform_property_set.go index d746f321..e0038d1f 100644 --- a/apstra/blueprint/freeform_property_set.go +++ b/apstra/blueprint/freeform_property_set.go @@ -33,7 +33,7 @@ func (o FreeformPropertySet) DataSourceAttributes() map[string]dataSourceSchema. Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, "id": dataSourceSchema.StringAttribute{ - MarkdownDescription: "Populate this field to look up a Freeform Property Set by `id`. Required when `name` is omitted.", + MarkdownDescription: "Populate this field to look up a Freeform Property Set by ID. Required when `name` is omitted.", Optional: true, Computed: true, Validators: []validator.String{ @@ -50,7 +50,7 @@ func (o FreeformPropertySet) DataSourceAttributes() map[string]dataSourceSchema. Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, "name": dataSourceSchema.StringAttribute{ - MarkdownDescription: "Populate this field to look up an imported Property Set by `name`. Required when `id` is omitted.", + MarkdownDescription: "Populate this field to look up an imported Property Set by Name. Required when `id` is omitted.", Optional: true, Computed: true, Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, @@ -74,6 +74,7 @@ func (o FreeformPropertySet) ResourceAttributes() map[string]resourceSchema.Attr "id": resourceSchema.StringAttribute{ MarkdownDescription: "ID of the Property Set.", Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, }, "system_id": resourceSchema.StringAttribute{ MarkdownDescription: "The system ID where the Property Set is associated.", diff --git a/apstra/blueprint/freeform_system.go b/apstra/blueprint/freeform_system.go index 919939ed..f2c7ff7c 100644 --- a/apstra/blueprint/freeform_system.go +++ b/apstra/blueprint/freeform_system.go @@ -2,6 +2,7 @@ package blueprint import ( "context" + "fmt" "github.com/Juniper/apstra-go-sdk/apstra" "github.com/Juniper/terraform-provider-apstra/apstra/utils" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" @@ -37,7 +38,7 @@ func (o FreeformSystem) DataSourceAttributes() map[string]dataSourceSchema.Attri 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 Freeform System by ID. Required when `name` is omitted.", Optional: true, Computed: true, Validators: []validator.String{ @@ -49,7 +50,7 @@ func (o FreeformSystem) DataSourceAttributes() map[string]dataSourceSchema.Attri }, }, "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 System by Name. Required when `id` is omitted.", Optional: true, Computed: true, Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, @@ -77,7 +78,7 @@ func (o FreeformSystem) DataSourceAttributes() map[string]dataSourceSchema.Attri "tags": dataSourceSchema.SetAttribute{ MarkdownDescription: "Set of Tag labels", ElementType: types.StringType, - Optional: true, + Computed: true, }, } } @@ -98,34 +99,30 @@ func (o FreeformSystem) ResourceAttributes() map[string]resourceSchema.Attribute "name": resourceSchema.StringAttribute{ MarkdownDescription: "Freeform System name as shown in the Web UI.", Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, "hostname": resourceSchema.StringAttribute{ MarkdownDescription: "Hostname of the Freeform System.", Optional: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, "deploy_mode": dataSourceSchema.StringAttribute{ - MarkdownDescription: "deploy mode of the System", + MarkdownDescription: "Deploy mode of the System", Optional: true, Validators: []validator.String{stringvalidator.OneOf(utils.AllNodeDeployModes()...)}, }, "type": dataSourceSchema.StringAttribute{ - MarkdownDescription: "type of the System, either Internal or External", + MarkdownDescription: fmt.Sprintf("Type of the System. Must be one of `%s` or `%s`", apstra.SystemTypeInternal, apstra.SystemTypeExternal), Required: true, Validators: []validator.String{stringvalidator.OneOf(apstra.SystemTypeInternal.String(), apstra.SystemTypeExternal.String())}, }, "device_profile_id": dataSourceSchema.StringAttribute{ - MarkdownDescription: "device profile ID of the System", + MarkdownDescription: "Device profile ID of the System", Optional: true, Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, "system_id": dataSourceSchema.StringAttribute{ - MarkdownDescription: "Device System ID assigned to the System", + MarkdownDescription: "ID (usually serial number) of the Managed Device to associate with this System", Optional: true, Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, diff --git a/apstra/configure_resource.go b/apstra/configure_resource.go index e85598f6..4057df90 100644 --- a/apstra/configure_resource.go +++ b/apstra/configure_resource.go @@ -70,10 +70,6 @@ func configureResource(_ context.Context, rs resource.ResourceWithConfigure, req rs.setBpClientFunc(pd.getTwoStageL3ClosClient) } - if rs, ok := rs.(resourceWithSetDcBpClientFunc); ok { - rs.setBpClientFunc(pd.getTwoStageL3ClosClient) - } - if rs, ok := rs.(resourceWithSetFfBpClientFunc); ok { rs.setBpClientFunc(pd.getFreeformClient) } diff --git a/apstra/data_source_freeform_config_template.go b/apstra/data_source_freeform_config_template.go index ffa11364..654fa231 100644 --- a/apstra/data_source_freeform_config_template.go +++ b/apstra/data_source_freeform_config_template.go @@ -34,6 +34,7 @@ func (o *dataSourceFreeformConfigTemplate) Schema(_ context.Context, _ datasourc Attributes: blueprint.FreeformConfigTemplate{}.DataSourceAttributes(), } } + func (o *dataSourceFreeformConfigTemplate) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var config blueprint.FreeformConfigTemplate resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) diff --git a/apstra/data_source_freeform_property_set.go b/apstra/data_source_freeform_property_set.go index 72bae6cb..19c47fbb 100644 --- a/apstra/data_source_freeform_property_set.go +++ b/apstra/data_source_freeform_property_set.go @@ -9,6 +9,7 @@ import ( "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 = &dataSourceFreeformPropertySet{} @@ -28,7 +29,7 @@ func (o *dataSourceFreeformPropertySet) Configure(ctx context.Context, req datas func (o *dataSourceFreeformPropertySet) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ - MarkdownDescription: docCategoryFreeform + "This data source provides details of a specific Freeform PropertySet.\n\n" + + MarkdownDescription: docCategoryFreeform + "This data source provides details of a specific Freeform Property Set.\n\n" + "At least one optional attribute is required.", Attributes: blueprint.FreeformPropertySet{}.DataSourceAttributes(), } @@ -81,6 +82,7 @@ func (o *dataSourceFreeformPropertySet) Read(ctx context.Context, req datasource return } + config.Id = types.StringValue(api.Id.String()) config.LoadApiData(ctx, api.Data, &resp.Diagnostics) if resp.Diagnostics.HasError() { return diff --git a/apstra/data_source_freeform_system.go b/apstra/data_source_freeform_system.go index 6bb560d2..aeae02c0 100644 --- a/apstra/data_source_freeform_system.go +++ b/apstra/data_source_freeform_system.go @@ -29,7 +29,7 @@ func (o *dataSourceFreeformSystem) Configure(ctx context.Context, req datasource func (o *dataSourceFreeformSystem) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ - MarkdownDescription: docCategoryFreeform + "This data source provides details of a specific Freeform Config Template.\n\n" + + MarkdownDescription: docCategoryFreeform + "This data source provides details of a specific Freeform System.\n\n" + "At least one optional attribute is required.", Attributes: blueprint.FreeformSystem{}.DataSourceAttributes(), } diff --git a/apstra/resource_freeform_config_template.go b/apstra/resource_freeform_config_template.go index 8b43145b..9878f686 100644 --- a/apstra/resource_freeform_config_template.go +++ b/apstra/resource_freeform_config_template.go @@ -118,7 +118,6 @@ func (o *resourceFreeformConfigTemplate) Read(ctx context.Context, req resource. resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } -// Update resource func (o *resourceFreeformConfigTemplate) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // Get plan values var plan blueprint.FreeformConfigTemplate @@ -162,7 +161,6 @@ func (o *resourceFreeformConfigTemplate) Update(ctx context.Context, req resourc resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } -// Delete resource func (o *resourceFreeformConfigTemplate) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var state blueprint.FreeformConfigTemplate resp.Diagnostics.Append(req.State.Get(ctx, &state)...) @@ -174,8 +172,7 @@ func (o *resourceFreeformConfigTemplate) Delete(ctx context.Context, req resourc 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 + return // 404 is okay } resp.Diagnostics.AddError("failed to create blueprint client", err.Error()) return diff --git a/apstra/resource_freeform_config_template_integration_test.go b/apstra/resource_freeform_config_template_integration_test.go index cb95882f..9e941d50 100644 --- a/apstra/resource_freeform_config_template_integration_test.go +++ b/apstra/resource_freeform_config_template_integration_test.go @@ -19,9 +19,9 @@ const ( resourceFreeformConfigTemplateHcl = ` resource %q %q { blueprint_id = %q - name = %q - text = %q - tags = %s + name = %q + text = %q + tags = %s } ` ) diff --git a/apstra/resource_freeform_property_set.go b/apstra/resource_freeform_property_set.go index e546ad77..1ad39f56 100644 --- a/apstra/resource_freeform_property_set.go +++ b/apstra/resource_freeform_property_set.go @@ -118,7 +118,6 @@ func (o *resourceFreeformPropertySet) Read(ctx context.Context, req resource.Rea resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } -// Update resource func (o *resourceFreeformPropertySet) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // Get plan values var plan blueprint.FreeformPropertySet @@ -158,11 +157,11 @@ func (o *resourceFreeformPropertySet) Update(ctx context.Context, req resource.U resp.Diagnostics.AddError("error updating Property Set", err.Error()) return } + // set state resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } -// Delete resource func (o *resourceFreeformPropertySet) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var state blueprint.FreeformPropertySet resp.Diagnostics.Append(req.State.Get(ctx, &state)...) @@ -174,8 +173,7 @@ func (o *resourceFreeformPropertySet) Delete(ctx context.Context, req resource.D 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 + return // 404 is okay } resp.Diagnostics.AddError("failed to create blueprint client", err.Error()) return diff --git a/apstra/resource_freeform_property_set_integration_test.go b/apstra/resource_freeform_property_set_integration_test.go index 787afc8d..9bff337d 100644 --- a/apstra/resource_freeform_property_set_integration_test.go +++ b/apstra/resource_freeform_property_set_integration_test.go @@ -28,7 +28,7 @@ resource %q %q { type resourceFreeformPropertySet struct { blueprintId string name string - system_id string + systemId string values json.RawMessage } @@ -36,7 +36,7 @@ func (o resourceFreeformPropertySet) render(rType, rName string) string { return fmt.Sprintf(resourceFreeformPropertySetHcl, rType, rName, o.blueprintId, - stringOrNull(o.system_id), + stringOrNull(o.systemId), o.name, string(o.values), ) @@ -51,8 +51,8 @@ func (o resourceFreeformPropertySet) testChecks(t testing.TB, rType, rName strin result.append(t, "TestCheckResourceAttr", "name", o.name) result.append(t, "TestCheckResourceAttr", "values", string(o.values)) - if o.system_id != "" { - result.append(t, "TestCheckResourceAttr", "system_id", o.system_id) + if o.systemId != "" { + result.append(t, "TestCheckResourceAttr", "system_id", o.systemId) } return result diff --git a/apstra/resource_freeform_system.go b/apstra/resource_freeform_system.go index bc88c665..0e36e620 100644 --- a/apstra/resource_freeform_system.go +++ b/apstra/resource_freeform_system.go @@ -118,7 +118,6 @@ func (o *resourceFreeformSystem) Read(ctx context.Context, req resource.ReadRequ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } -// Update resource func (o *resourceFreeformSystem) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // Get plan values var plan blueprint.FreeformSystem @@ -158,11 +157,11 @@ func (o *resourceFreeformSystem) Update(ctx context.Context, req resource.Update resp.Diagnostics.AddError("error updating Freeform System", err.Error()) return } + // set state resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } -// Delete resource func (o *resourceFreeformSystem) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var state blueprint.FreeformSystem resp.Diagnostics.Append(req.State.Get(ctx, &state)...) @@ -174,8 +173,7 @@ func (o *resourceFreeformSystem) Delete(ctx context.Context, req resource.Delete 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 + return // 404 is okay } resp.Diagnostics.AddError("failed to create blueprint client", err.Error()) return diff --git a/apstra/test_helpers_test.go b/apstra/test_helpers_test.go index 6f716940..8027f587 100644 --- a/apstra/test_helpers_test.go +++ b/apstra/test_helpers_test.go @@ -230,7 +230,8 @@ func randomStrings(strCount int, strLen int) []string { func randomJson(t testing.TB, maxInt int, strLen int, count int) json.RawMessage { t.Helper() - preResult := make(map[string]any) + + preResult := make(map[string]any, count) for i := 0; i < count; i++ { if rand.Int()%2 == 0 { preResult["a"+acctest.RandString(strLen-1)] = rand.Intn(maxInt) @@ -238,10 +239,11 @@ func randomJson(t testing.TB, maxInt int, strLen int, count int) json.RawMessage preResult["a"+acctest.RandString(strLen-1)] = acctest.RandString(strLen) } } - marshalResult, err := json.Marshal(&preResult) + + result, err := json.Marshal(&preResult) require.NoError(t, err) - return marshalResult + return result } type lineNumberer struct { diff --git a/docs/data-sources/freeform_config_template.md b/docs/data-sources/freeform_config_template.md index 15a21057..81f2118e 100644 --- a/docs/data-sources/freeform_config_template.md +++ b/docs/data-sources/freeform_config_template.md @@ -32,10 +32,10 @@ data "apstra_freeform_config_template" "interfaces" { ### Optional -- `id` (String) Populate this field to look up the Config Template by `id`. Required when `name` is omitted. -- `name` (String) Populate this field to look up an imported Config Template by `name`. Required when `id` is omitted. -- `tags` (Set of String) Set of Tag labels +- `id` (String) Populate this field to look up the Config Template by ID. Required when `name` is omitted. +- `name` (String) Populate this field to look up an imported Config Template by Name. Required when `id` is omitted. ### Read-Only +- `tags` (Set of String) Set of Tag labels - `text` (String) Configuration Jinja2 template text diff --git a/docs/data-sources/freeform_property_set.md b/docs/data-sources/freeform_property_set.md index 5c0c1684..a6df6e35 100644 --- a/docs/data-sources/freeform_property_set.md +++ b/docs/data-sources/freeform_property_set.md @@ -2,13 +2,13 @@ page_title: "apstra_freeform_property_set Data Source - terraform-provider-apstra" subcategory: "Reference Design: Freeform" description: |- - This data source provides details of a specific Freeform PropertySet. + This data source provides details of a specific Freeform Property Set. At least one optional attribute is required. --- # apstra_freeform_property_set (Data Source) -This data source provides details of a specific Freeform PropertySet. +This data source provides details of a specific Freeform Property Set. At least one optional attribute is required. @@ -57,8 +57,8 @@ output "foo" {value = data.apstra_freeform_property_set.foo} ### Optional -- `id` (String) Populate this field to look up a Freeform Property Set by `id`. Required when `name` is omitted. -- `name` (String) Populate this field to look up an imported Property Set by `name`. Required when `id` is omitted. +- `id` (String) Populate this field to look up a Freeform Property Set by ID. Required when `name` is omitted. +- `name` (String) Populate this field to look up an imported Property Set by Name. Required when `id` is omitted. ### Read-Only diff --git a/docs/data-sources/freeform_system.md b/docs/data-sources/freeform_system.md new file mode 100644 index 00000000..ab710fac --- /dev/null +++ b/docs/data-sources/freeform_system.md @@ -0,0 +1,41 @@ +--- +page_title: "apstra_freeform_system Data Source - terraform-provider-apstra" +subcategory: "Reference Design: Freeform" +description: |- + This data source provides details of a specific Freeform System. + At least one optional attribute is required. +--- + +# apstra_freeform_system (Data Source) + +This data source provides details of a specific Freeform System. + +At least one optional attribute is required. + + +## Example Usage + +```terraform +# +``` + + +## Schema + +### Required + +- `blueprint_id` (String) Apstra Blueprint ID. Used to identify the Blueprint where the System lives. + +### Optional + +- `id` (String) Populate this field to look up the Freeform System by ID. Required when `name` is omitted. +- `name` (String) Populate this field to look up System by Name. Required when `id` is omitted. + +### Read-Only + +- `deploy_mode` (String) deploy mode of the System +- `device_profile_id` (String) device profile ID of the System +- `hostname` (String) Hostname of the System +- `system_id` (String) Device System ID assigned to the System +- `tags` (Set of String) Set of Tag labels +- `type` (String) type of the System, either Internal or External diff --git a/docs/resources/freeform_system.md b/docs/resources/freeform_system.md new file mode 100644 index 00000000..308c48b7 --- /dev/null +++ b/docs/resources/freeform_system.md @@ -0,0 +1,41 @@ +--- +page_title: "apstra_freeform_system Resource - terraform-provider-apstra" +subcategory: "Reference Design: Freeform" +description: |- + This resource creates a System in a Freeform Blueprint. +--- + +# apstra_freeform_system (Resource) + +This resource creates a System in a Freeform Blueprint. + + +## Example Usage + +```terraform +# +``` + + +## Schema + +### Required + +- `blueprint_id` (String) Apstra Blueprint ID. +- `name` (String) Freeform System name as shown in the Web UI. +- `type` (String) Type of the System. Must be one of `internal` or `external` + +### Optional + +- `deploy_mode` (String) Deploy mode of the System +- `device_profile_id` (String) Device profile ID of the System +- `hostname` (String) Hostname of the Freeform System. +- `system_id` (String) ID (usually serial number) of the Managed Device to associate with this System +- `tags` (Set of String) Set of Tag labels + +### Read-Only + +- `id` (String) ID of the Freeform System. + + + diff --git a/examples/data-sources/apstra_freeform_system/example.tf b/examples/data-sources/apstra_freeform_system/example.tf new file mode 100644 index 00000000..792d6005 --- /dev/null +++ b/examples/data-sources/apstra_freeform_system/example.tf @@ -0,0 +1 @@ +# diff --git a/examples/resources/apstra_freeform_system/example.tf b/examples/resources/apstra_freeform_system/example.tf new file mode 100644 index 00000000..792d6005 --- /dev/null +++ b/examples/resources/apstra_freeform_system/example.tf @@ -0,0 +1 @@ +# From 2e453a5d20c9880cb2e12588e3231c467cc38a05 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Tue, 2 Jul 2024 21:34:38 -0400 Subject: [PATCH 08/18] gofumpt --- apstra/blueprint/freeform_config_template.go | 3 ++- apstra/blueprint/freeform_property_set.go | 1 + apstra/blueprint/freeform_system.go | 1 + apstra/configure_resource.go | 1 + apstra/data_source_freeform_config_template.go | 7 +++++-- apstra/data_source_freeform_property_set.go | 8 ++++++-- apstra/data_source_freeform_system.go | 8 ++++++-- apstra/resource_freeform_config_template.go | 9 ++++++--- ...resource_freeform_config_template_integration_test.go | 7 ++++--- apstra/resource_freeform_property_set.go | 9 ++++++--- .../resource_freeform_property_set_integration_test.go | 3 ++- apstra/resource_freeform_system.go | 9 ++++++--- 12 files changed, 46 insertions(+), 20 deletions(-) diff --git a/apstra/blueprint/freeform_config_template.go b/apstra/blueprint/freeform_config_template.go index 14b4020a..14c28de8 100644 --- a/apstra/blueprint/freeform_config_template.go +++ b/apstra/blueprint/freeform_config_template.go @@ -2,6 +2,8 @@ package blueprint import ( "context" + "regexp" + "github.com/Juniper/apstra-go-sdk/apstra" "github.com/Juniper/terraform-provider-apstra/apstra/utils" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" @@ -14,7 +16,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "regexp" ) type FreeformConfigTemplate struct { diff --git a/apstra/blueprint/freeform_property_set.go b/apstra/blueprint/freeform_property_set.go index e0038d1f..88085be1 100644 --- a/apstra/blueprint/freeform_property_set.go +++ b/apstra/blueprint/freeform_property_set.go @@ -3,6 +3,7 @@ package blueprint import ( "context" "encoding/json" + "github.com/Juniper/apstra-go-sdk/apstra" "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" diff --git a/apstra/blueprint/freeform_system.go b/apstra/blueprint/freeform_system.go index f2c7ff7c..a0a6e1a8 100644 --- a/apstra/blueprint/freeform_system.go +++ b/apstra/blueprint/freeform_system.go @@ -3,6 +3,7 @@ package blueprint import ( "context" "fmt" + "github.com/Juniper/apstra-go-sdk/apstra" "github.com/Juniper/terraform-provider-apstra/apstra/utils" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" diff --git a/apstra/configure_resource.go b/apstra/configure_resource.go index 4057df90..77b1d4f7 100644 --- a/apstra/configure_resource.go +++ b/apstra/configure_resource.go @@ -3,6 +3,7 @@ package tfapstra import ( "context" "fmt" + "github.com/Juniper/apstra-go-sdk/apstra" "github.com/hashicorp/terraform-plugin-framework/resource" ) diff --git a/apstra/data_source_freeform_config_template.go b/apstra/data_source_freeform_config_template.go index 654fa231..8be3437c 100644 --- a/apstra/data_source_freeform_config_template.go +++ b/apstra/data_source_freeform_config_template.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" @@ -12,8 +13,10 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -var _ datasource.DataSourceWithConfigure = &dataSourceFreeformConfigTemplate{} -var _ datasourceWithSetFfBpClientFunc = &dataSourceFreeformConfigTemplate{} +var ( + _ datasource.DataSourceWithConfigure = &dataSourceFreeformConfigTemplate{} + _ datasourceWithSetFfBpClientFunc = &dataSourceFreeformConfigTemplate{} +) type dataSourceFreeformConfigTemplate struct { getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) diff --git a/apstra/data_source_freeform_property_set.go b/apstra/data_source_freeform_property_set.go index 19c47fbb..36a2f55b 100644 --- a/apstra/data_source_freeform_property_set.go +++ b/apstra/data_source_freeform_property_set.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" @@ -12,8 +13,10 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -var _ datasource.DataSourceWithConfigure = &dataSourceFreeformPropertySet{} -var _ datasourceWithSetFfBpClientFunc = &dataSourceFreeformPropertySet{} +var ( + _ datasource.DataSourceWithConfigure = &dataSourceFreeformPropertySet{} + _ datasourceWithSetFfBpClientFunc = &dataSourceFreeformPropertySet{} +) type dataSourceFreeformPropertySet struct { getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) @@ -34,6 +37,7 @@ func (o *dataSourceFreeformPropertySet) Schema(_ context.Context, _ datasource.S Attributes: blueprint.FreeformPropertySet{}.DataSourceAttributes(), } } + func (o *dataSourceFreeformPropertySet) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var config blueprint.FreeformPropertySet resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) diff --git a/apstra/data_source_freeform_system.go b/apstra/data_source_freeform_system.go index aeae02c0..3d7dd4fe 100644 --- a/apstra/data_source_freeform_system.go +++ b/apstra/data_source_freeform_system.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" @@ -12,8 +13,10 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -var _ datasource.DataSourceWithConfigure = &dataSourceFreeformSystem{} -var _ datasourceWithSetFfBpClientFunc = &dataSourceFreeformSystem{} +var ( + _ datasource.DataSourceWithConfigure = &dataSourceFreeformSystem{} + _ datasourceWithSetFfBpClientFunc = &dataSourceFreeformSystem{} +) type dataSourceFreeformSystem struct { getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) @@ -34,6 +37,7 @@ func (o *dataSourceFreeformSystem) Schema(_ context.Context, _ datasource.Schema Attributes: blueprint.FreeformSystem{}.DataSourceAttributes(), } } + func (o *dataSourceFreeformSystem) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var config blueprint.FreeformSystem resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) diff --git a/apstra/resource_freeform_config_template.go b/apstra/resource_freeform_config_template.go index 9878f686..6d04e02b 100644 --- a/apstra/resource_freeform_config_template.go +++ b/apstra/resource_freeform_config_template.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" @@ -11,9 +12,11 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -var _ resource.ResourceWithConfigure = &resourceFreeformConfigTemplate{} -var _ resourceWithSetFfBpClientFunc = &resourceFreeformConfigTemplate{} -var _ resourceWithSetBpLockFunc = &resourceFreeformConfigTemplate{} +var ( + _ resource.ResourceWithConfigure = &resourceFreeformConfigTemplate{} + _ resourceWithSetFfBpClientFunc = &resourceFreeformConfigTemplate{} + _ resourceWithSetBpLockFunc = &resourceFreeformConfigTemplate{} +) type resourceFreeformConfigTemplate struct { getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) diff --git a/apstra/resource_freeform_config_template_integration_test.go b/apstra/resource_freeform_config_template_integration_test.go index 9e941d50..220baeae 100644 --- a/apstra/resource_freeform_config_template_integration_test.go +++ b/apstra/resource_freeform_config_template_integration_test.go @@ -5,14 +5,15 @@ package tfapstra_test import ( "context" "fmt" + "math/rand" + "strconv" + "testing" + 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" - "math/rand" - "strconv" - "testing" ) const ( diff --git a/apstra/resource_freeform_property_set.go b/apstra/resource_freeform_property_set.go index 1ad39f56..7f6d4001 100644 --- a/apstra/resource_freeform_property_set.go +++ b/apstra/resource_freeform_property_set.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" @@ -11,9 +12,11 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -var _ resource.ResourceWithConfigure = &resourceFreeformPropertySet{} -var _ resourceWithSetFfBpClientFunc = &resourceFreeformPropertySet{} -var _ resourceWithSetBpLockFunc = &resourceFreeformPropertySet{} +var ( + _ resource.ResourceWithConfigure = &resourceFreeformPropertySet{} + _ resourceWithSetFfBpClientFunc = &resourceFreeformPropertySet{} + _ resourceWithSetBpLockFunc = &resourceFreeformPropertySet{} +) type resourceFreeformPropertySet struct { getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) diff --git a/apstra/resource_freeform_property_set_integration_test.go b/apstra/resource_freeform_property_set_integration_test.go index 9bff337d..4cd2898f 100644 --- a/apstra/resource_freeform_property_set_integration_test.go +++ b/apstra/resource_freeform_property_set_integration_test.go @@ -6,12 +6,13 @@ import ( "context" "encoding/json" "fmt" + "testing" + 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" - "testing" ) const ( diff --git a/apstra/resource_freeform_system.go b/apstra/resource_freeform_system.go index 0e36e620..3eef0c42 100644 --- a/apstra/resource_freeform_system.go +++ b/apstra/resource_freeform_system.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" @@ -11,9 +12,11 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -var _ resource.ResourceWithConfigure = &resourceFreeformSystem{} -var _ resourceWithSetFfBpClientFunc = &resourceFreeformSystem{} -var _ resourceWithSetBpLockFunc = &resourceFreeformSystem{} +var ( + _ resource.ResourceWithConfigure = &resourceFreeformSystem{} + _ resourceWithSetFfBpClientFunc = &resourceFreeformSystem{} + _ resourceWithSetBpLockFunc = &resourceFreeformSystem{} +) type resourceFreeformSystem struct { getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) From 0955cea05ac5a883dc97580dcf0c52def40d2c77 Mon Sep 17 00:00:00 2001 From: bwJuniper <98770420+bwJuniper@users.noreply.github.com> Date: Wed, 3 Jul 2024 22:37:37 +0200 Subject: [PATCH 09/18] fix tests and examples --- apstra/blueprint/freeform_system.go | 22 +- apstra/export_test.go | 1 + apstra/resource_freeform_system.go | 1 - ...source_freeform_system_integration_test.go | 204 ++++++++++++++++++ docs/data-sources/freeform_system.md | 41 +++- docs/resources/freeform_system.md | 43 +++- .../apstra_freeform_system/example.tf | 42 +++- .../apstra_freeform_system/example.tf | 41 +++- 8 files changed, 381 insertions(+), 14 deletions(-) create mode 100644 apstra/resource_freeform_system_integration_test.go diff --git a/apstra/blueprint/freeform_system.go b/apstra/blueprint/freeform_system.go index a0a6e1a8..7fa19a91 100644 --- a/apstra/blueprint/freeform_system.go +++ b/apstra/blueprint/freeform_system.go @@ -3,6 +3,7 @@ package blueprint import ( "context" "fmt" + "regexp" "github.com/Juniper/apstra-go-sdk/apstra" "github.com/Juniper/terraform-provider-apstra/apstra/utils" @@ -85,6 +86,7 @@ func (o FreeformSystem) DataSourceAttributes() map[string]dataSourceSchema.Attri } func (o FreeformSystem) ResourceAttributes() map[string]resourceSchema.Attribute { + hostnameRegexp := "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$" return map[string]resourceSchema.Attribute{ "blueprint_id": resourceSchema.StringAttribute{ MarkdownDescription: "Apstra Blueprint ID.", @@ -100,29 +102,33 @@ func (o FreeformSystem) ResourceAttributes() map[string]resourceSchema.Attribute "name": resourceSchema.StringAttribute{ MarkdownDescription: "Freeform System name as shown in the Web UI.", Required: true, - Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile("^[a-zA-Z0-9.-_]+$"), "name may consist only of the following characters : a-zA-Z0-9.-_")}, }, "hostname": resourceSchema.StringAttribute{ MarkdownDescription: "Hostname of the Freeform System.", - Optional: true, - Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(hostnameRegexp), "must match regex "+hostnameRegexp), + }, }, - "deploy_mode": dataSourceSchema.StringAttribute{ + "deploy_mode": resourceSchema.StringAttribute{ MarkdownDescription: "Deploy mode of the System", Optional: true, Validators: []validator.String{stringvalidator.OneOf(utils.AllNodeDeployModes()...)}, }, - "type": dataSourceSchema.StringAttribute{ + "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())}, }, - "device_profile_id": dataSourceSchema.StringAttribute{ + "device_profile_id": resourceSchema.StringAttribute{ MarkdownDescription: "Device profile ID of the System", Optional: true, Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, - "system_id": dataSourceSchema.StringAttribute{ + "system_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)}, @@ -166,7 +172,7 @@ func (o *FreeformSystem) Request(ctx context.Context, diags *diag.Diagnostics) * func (o *FreeformSystem) LoadApiData(ctx context.Context, in *apstra.FreeformSystemData, diags *diag.Diagnostics) { o.Name = types.StringValue(in.Label) o.Hostname = types.StringValue(in.Hostname) - o.Type = types.StringValue(string(rune(in.Type))) + o.Type = types.StringValue(in.Type.String()) o.DeviceProfileId = types.StringValue(string(in.DeviceProfileId)) o.SystemId = types.StringPointerValue((*string)(in.SystemId)) o.Tags = utils.SetValueOrNull(ctx, types.StringType, in.Tags, diags) // safe to ignore diagnostic here diff --git a/apstra/export_test.go b/apstra/export_test.go index 5e1e15e7..9d0f68d0 100644 --- a/apstra/export_test.go +++ b/apstra/export_test.go @@ -11,6 +11,7 @@ var ( ResourceDatacenterGenericSystem = resourceDatacenterGenericSystem{} ResourceDatacenterRoutingZone = resourceDatacenterRoutingZone{} ResourceFreeformConfigTemplate = resourceFreeformConfigTemplate{} + ResourceFreeformSystem = resourceFreeformSystem{} ResourceFreeformPropertySet = resourceFreeformPropertySet{} ResourceIpv4Pool = resourceIpv4Pool{} ResourceTemplatePodBased = resourceTemplatePodBased{} diff --git a/apstra/resource_freeform_system.go b/apstra/resource_freeform_system.go index 3eef0c42..7b255471 100644 --- a/apstra/resource_freeform_system.go +++ b/apstra/resource_freeform_system.go @@ -3,7 +3,6 @@ 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_system_integration_test.go b/apstra/resource_freeform_system_integration_test.go new file mode 100644 index 00000000..cd5e03ac --- /dev/null +++ b/apstra/resource_freeform_system_integration_test.go @@ -0,0 +1,204 @@ +//go:build integration + +package tfapstra_test + +import ( + "context" + "fmt" + "github.com/Juniper/apstra-go-sdk/apstra" + "math/rand" + "strconv" + "testing" + + 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 ( + resourceFreeformSystemHcl = ` +resource %q %q { + blueprint_id = %q + name = %q + device_profile_id = %q + hostname = %q + type = %q + deploy_mode = %s + tags = %s +} +` +) + +type resourceFreeformSystem struct { + blueprintId string + name string + deviceProfileId string + hostname string + systemType string + deployMode string + tags []string +} + +func (o resourceFreeformSystem) render(rType, rName string) string { + return fmt.Sprintf(resourceFreeformSystemHcl, + rType, rName, + o.blueprintId, + o.name, + o.deviceProfileId, + o.hostname, + o.systemType, + stringOrNull(o.deployMode), + stringSetOrNull(o.tags), + ) +} + +func (o resourceFreeformSystem) 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", o.systemType) + result.append(t, "TestCheckResourceAttr", "device_profile_id", o.deviceProfileId) + result.append(t, "TestCheckResourceAttr", "hostname", o.hostname) + if o.deployMode != "" { + result.append(t, "TestCheckResourceAttr", "deploy_mode", o.deployMode) + } + 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) + } + } + + return result +} + +func TestResourceFreeformSystem(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) + // get a device profile + dpId, _ := bp.ImportDeviceProfile(ctx, "Juniper_vEX") + + type testStep struct { + config resourceFreeformSystem + } + type testCase struct { + apiVersionConstraints version.Constraints + steps []testStep + } + + testCases := map[string]testCase{ + "start_with_no_tags": { + steps: []testStep{ + { + config: resourceFreeformSystem{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + hostname: acctest.RandString(6), + deviceProfileId: string(dpId), + systemType: apstra.SystemTypeInternal.String(), + }, + }, + { + config: resourceFreeformSystem{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + hostname: acctest.RandString(6), + deviceProfileId: string(dpId), + deployMode: apstra.DeployModeDeploy.String(), + systemType: apstra.SystemTypeInternal.String(), + tags: randomStrings(rand.Intn(10)+2, 6), + }, + }, + { + config: resourceFreeformSystem{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + hostname: acctest.RandString(6), + deployMode: apstra.DeployModeUndeploy.String(), + systemType: apstra.SystemTypeInternal.String(), + deviceProfileId: string(dpId), + }, + }, + }, + }, + "start_with_tags": { + steps: []testStep{ + { + config: resourceFreeformSystem{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + hostname: acctest.RandString(6), + deviceProfileId: string(dpId), + deployMode: apstra.DeployModeDeploy.String(), + systemType: apstra.SystemTypeInternal.String(), + tags: randomStrings(rand.Intn(10)+2, 6), + }, + }, + { + config: resourceFreeformSystem{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + hostname: acctest.RandString(6), + deployMode: apstra.DeployModeUndeploy.String(), + systemType: apstra.SystemTypeExternal.String(), + deviceProfileId: string(dpId), + }, + }, + { + config: resourceFreeformSystem{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + hostname: acctest.RandString(6), + deviceProfileId: string(dpId), + deployMode: apstra.DeployModeDeploy.String(), + systemType: apstra.SystemTypeExternal.String(), + tags: randomStrings(rand.Intn(10)+2, 6), + }, + }, + }, + }, + } + + resourceType := tfapstra.ResourceName(ctx, &tfapstra.ResourceFreeformSystem) + + 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_system.md b/docs/data-sources/freeform_system.md index ab710fac..a53d3aba 100644 --- a/docs/data-sources/freeform_system.md +++ b/docs/data-sources/freeform_system.md @@ -16,7 +16,46 @@ At least one optional attribute is required. ## Example Usage ```terraform -# +# This example defines a freeform system in a blueprint + + +resource "apstra_freeform_system" "test" { + blueprint_id = "freeform_blueprint-5ba09d07" + name = "test_system" + tags = ["a", "b", "c"] + type = "internal" + hostname = "testsystem" + deploy_mode = "deploy" + device_profile_id = "PtrWb4-VSwKiYRbCodk" +} + +# here we retrieve the freeform system + +data "apstra_freeform_system" "test" { + blueprint_id = "freeform_blueprint-5ba09d07" + id = apstra_freeform_system.test.id +} + +# here we build an output bock to display it + +output "test_System_out" {value = data.apstra_freeform_system.test} + +#Output looks like this +#test_System_out = { +# "blueprint_id" = "freeform_blueprint-5ba09d07" +# "deploy_mode" = tostring(null) +# "device_profile_id" = "PtrWb4-VSwKiYRbCodk" +# "hostname" = "systemfoo" +# "id" = "-63CYLAiWuAq0ljzX0Q" +# "name" = "test_system_foo" +# "system_id" = tostring(null) +# "tags" = toset([ +# "a", +# "b", +# "c", +# ]) +# "type" = "internal" +#} ``` diff --git a/docs/resources/freeform_system.md b/docs/resources/freeform_system.md index 308c48b7..ec0f07e9 100644 --- a/docs/resources/freeform_system.md +++ b/docs/resources/freeform_system.md @@ -13,7 +13,46 @@ This resource creates a System in a Freeform Blueprint. ## Example Usage ```terraform -# +# This example defines a freeform system in a blueprint + + +resource "apstra_freeform_system" "test" { + blueprint_id = "freeform_blueprint-5ba09d07" + name = "test_system" + tags = ["a", "b", "c"] + type = "internal" + hostname = "testsystem" + deploy_mode = "deploy" + device_profile_id = "PtrWb4-VSwKiYRbCodk" +} + +# here we retrieve the freeform system + +data "apstra_freeform_system" "test" { + blueprint_id = "freeform_blueprint-5ba09d07" + id = apstra_freeform_system.test.id +} + +# here we build an output bock to display it + +output "test_System_out" {value = data.apstra_freeform_system.test} + +#Output looks like this +#test_System_out = { +# "blueprint_id" = "freeform_blueprint-5ba09d07" +# "deploy_mode" = tostring(null) +# "device_profile_id" = "PtrWb4-VSwKiYRbCodk" +# "hostname" = "systemfoo" +# "id" = "-63CYLAiWuAq0ljzX0Q" +# "name" = "test_system_foo" +# "system_id" = tostring(null) +# "tags" = toset([ +# "a", +# "b", +# "c", +# ]) +# "type" = "internal" +#} ``` @@ -22,6 +61,7 @@ This resource creates a System in a Freeform Blueprint. ### Required - `blueprint_id` (String) Apstra Blueprint ID. +- `hostname` (String) Hostname of the Freeform System. - `name` (String) Freeform System name as shown in the Web UI. - `type` (String) Type of the System. Must be one of `internal` or `external` @@ -29,7 +69,6 @@ This resource creates a System in a Freeform Blueprint. - `deploy_mode` (String) Deploy mode of the System - `device_profile_id` (String) Device profile ID of the System -- `hostname` (String) Hostname of the Freeform System. - `system_id` (String) ID (usually serial number) of the Managed Device to associate with this System - `tags` (Set of String) Set of Tag labels diff --git a/examples/data-sources/apstra_freeform_system/example.tf b/examples/data-sources/apstra_freeform_system/example.tf index 792d6005..b04f62e1 100644 --- a/examples/data-sources/apstra_freeform_system/example.tf +++ b/examples/data-sources/apstra_freeform_system/example.tf @@ -1 +1,41 @@ -# +# This example defines a freeform system in a blueprint + + +resource "apstra_freeform_system" "test" { + blueprint_id = "freeform_blueprint-5ba09d07" + name = "test_system" + tags = ["a", "b", "c"] + type = "internal" + hostname = "testsystem" + deploy_mode = "deploy" + device_profile_id = "PtrWb4-VSwKiYRbCodk" +} + +# here we retrieve the freeform system + +data "apstra_freeform_system" "test" { + blueprint_id = "freeform_blueprint-5ba09d07" + id = apstra_freeform_system.test.id +} + +# here we build an output bock to display it + +output "test_System_out" {value = data.apstra_freeform_system.test} + +#Output looks like this +#test_System_out = { +# "blueprint_id" = "freeform_blueprint-5ba09d07" +# "deploy_mode" = tostring(null) +# "device_profile_id" = "PtrWb4-VSwKiYRbCodk" +# "hostname" = "systemfoo" +# "id" = "-63CYLAiWuAq0ljzX0Q" +# "name" = "test_system_foo" +# "system_id" = tostring(null) +# "tags" = toset([ +# "a", +# "b", +# "c", +# ]) +# "type" = "internal" +#} + diff --git a/examples/resources/apstra_freeform_system/example.tf b/examples/resources/apstra_freeform_system/example.tf index 792d6005..0e627e05 100644 --- a/examples/resources/apstra_freeform_system/example.tf +++ b/examples/resources/apstra_freeform_system/example.tf @@ -1 +1,40 @@ -# +# This example defines a freeform system in a blueprint + + +resource "apstra_freeform_system" "test" { + blueprint_id = "freeform_blueprint-5ba09d07" + name = "test_system" + tags = ["a", "b", "c"] + type = "internal" + hostname = "testsystem" + deploy_mode = "deploy" + device_profile_id = "PtrWb4-VSwKiYRbCodk" +} + +# here we retrieve the freeform system + +data "apstra_freeform_system" "test" { + blueprint_id = "freeform_blueprint-5ba09d07" + id = apstra_freeform_system.test.id +} + +# here we build an output bock to display it + +output "test_System_out" {value = data.apstra_freeform_system.test} + +#Output looks like this +#test_System_out = { +# "blueprint_id" = "freeform_blueprint-5ba09d07" +# "deploy_mode" = tostring(null) +# "device_profile_id" = "PtrWb4-VSwKiYRbCodk" +# "hostname" = "systemfoo" +# "id" = "-63CYLAiWuAq0ljzX0Q" +# "name" = "test_system_foo" +# "system_id" = tostring(null) +# "tags" = toset([ +# "a", +# "b", +# "c", +# ]) +# "type" = "internal" +#} From dd94e5120cb901cd01d46d3d9db4d9a9a7645f7b Mon Sep 17 00:00:00 2001 From: bwJuniper <98770420+bwJuniper@users.noreply.github.com> Date: Mon, 8 Jul 2024 12:51:05 +0200 Subject: [PATCH 10/18] fix examples to remove blueprint-names and spacing --- .../apstra_freeform_property_set/example.tf | 10 +++++----- .../apstra_freeform_system/example.tf | 6 +++--- .../apstra_freeform_property_set/example.tf | 4 ++-- .../apstra_freeform_system/example.tf | 20 +++++++++---------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/examples/data-sources/apstra_freeform_property_set/example.tf b/examples/data-sources/apstra_freeform_property_set/example.tf index 723ac049..371afac4 100644 --- a/examples/data-sources/apstra_freeform_property_set/example.tf +++ b/examples/data-sources/apstra_freeform_property_set/example.tf @@ -2,9 +2,9 @@ # first we create a property set so we can use a data source to retrieve it. resource "apstra_freeform_property_set" "prop_set_foo" { - blueprint_id = "freeform_blueprint-5ba09d07" + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" name = "prop_set_foo" - values = jsonencode({ + values = jsonencode({ foo = "bar" clown = 2 }) @@ -13,16 +13,16 @@ resource "apstra_freeform_property_set" "prop_set_foo" { # here we retrieve the property_set. data "apstra_freeform_property_set" "foo" { - blueprint_id = "freeform_blueprint-5ba09d07" + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" name = apstra_freeform_property_set.prop_set_foo.name } #here we build an output block to display it. -output "foo" {value = data.apstra_freeform_property_set.foo} +output "foo" { value = data.apstra_freeform_property_set.foo } #Output looks like this #foo = { -# "blueprint_id" = "freeform_blueprint-5ba09d07" +# "blueprint_id" = "043c5787-66e8-41c7-8925-c7e52fbe6e32" # "id" = tostring(null) # "name" = "prop_set_foo" # "system_id" = tostring(null) diff --git a/examples/data-sources/apstra_freeform_system/example.tf b/examples/data-sources/apstra_freeform_system/example.tf index b04f62e1..1fdfd02f 100644 --- a/examples/data-sources/apstra_freeform_system/example.tf +++ b/examples/data-sources/apstra_freeform_system/example.tf @@ -2,7 +2,7 @@ resource "apstra_freeform_system" "test" { - blueprint_id = "freeform_blueprint-5ba09d07" + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" name = "test_system" tags = ["a", "b", "c"] type = "internal" @@ -14,7 +14,7 @@ resource "apstra_freeform_system" "test" { # here we retrieve the freeform system data "apstra_freeform_system" "test" { - blueprint_id = "freeform_blueprint-5ba09d07" + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" id = apstra_freeform_system.test.id } @@ -24,7 +24,7 @@ output "test_System_out" {value = data.apstra_freeform_system.test} #Output looks like this #test_System_out = { -# "blueprint_id" = "freeform_blueprint-5ba09d07" +# "blueprint_id" = "043c5787-66e8-41c7-8925-c7e52fbe6e32" # "deploy_mode" = tostring(null) # "device_profile_id" = "PtrWb4-VSwKiYRbCodk" # "hostname" = "systemfoo" diff --git a/examples/resources/apstra_freeform_property_set/example.tf b/examples/resources/apstra_freeform_property_set/example.tf index 6fe62bc0..f5579ea1 100644 --- a/examples/resources/apstra_freeform_property_set/example.tf +++ b/examples/resources/apstra_freeform_property_set/example.tf @@ -1,7 +1,7 @@ # Create a freeform property set resource. resource "apstra_freeform_property_set" "prop_set_foo" { - blueprint_id = "freeform_blueprint-5ba09d07" + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" name = "prop_set_foo" values = jsonencode({ foo = "bar" @@ -12,7 +12,7 @@ resource "apstra_freeform_property_set" "prop_set_foo" { # Read the property set back with a data source. data "apstra_freeform_property_set" "foods" { - blueprint_id = "freeform_blueprint-5ba09d07" + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" name = apstra_freeform_property_set.prop_set_foo.name } diff --git a/examples/resources/apstra_freeform_system/example.tf b/examples/resources/apstra_freeform_system/example.tf index 0e627e05..f455a473 100644 --- a/examples/resources/apstra_freeform_system/example.tf +++ b/examples/resources/apstra_freeform_system/example.tf @@ -2,29 +2,29 @@ resource "apstra_freeform_system" "test" { - blueprint_id = "freeform_blueprint-5ba09d07" - name = "test_system" - tags = ["a", "b", "c"] - type = "internal" - hostname = "testsystem" - deploy_mode = "deploy" + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + name = "test_system" + tags = ["a", "b", "c"] + type = "internal" + hostname = "testsystem" + deploy_mode = "deploy" device_profile_id = "PtrWb4-VSwKiYRbCodk" } # here we retrieve the freeform system data "apstra_freeform_system" "test" { - blueprint_id = "freeform_blueprint-5ba09d07" - id = apstra_freeform_system.test.id + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + id = apstra_freeform_system.test.id } # here we build an output bock to display it -output "test_System_out" {value = data.apstra_freeform_system.test} +output "test_System_out" { value = data.apstra_freeform_system.test } #Output looks like this #test_System_out = { -# "blueprint_id" = "freeform_blueprint-5ba09d07" +# "blueprint_id" = "043c5787-66e8-41c7-8925-c7e52fbe6e32" # "deploy_mode" = tostring(null) # "device_profile_id" = "PtrWb4-VSwKiYRbCodk" # "hostname" = "systemfoo" From f05146392afe2e037e488828f9310ff2dafb5387 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Mon, 8 Jul 2024 09:14:55 -0400 Subject: [PATCH 11/18] doc whitespace cleanup --- docs/data-sources/freeform_property_set.md | 28 +++++----- docs/data-sources/freeform_system.md | 37 +++++++------ docs/resources/freeform_property_set.md | 18 +++---- docs/resources/freeform_system.md | 52 +++++++++---------- .../apstra_freeform_property_set/example.tf | 18 +++---- .../apstra_freeform_system/example.tf | 33 ++++++------ .../apstra_freeform_property_set/example.tf | 14 ++--- .../apstra_freeform_system/example.tf | 32 ++++++------ 8 files changed, 115 insertions(+), 117 deletions(-) diff --git a/docs/data-sources/freeform_property_set.md b/docs/data-sources/freeform_property_set.md index a6df6e35..68d885b1 100644 --- a/docs/data-sources/freeform_property_set.md +++ b/docs/data-sources/freeform_property_set.md @@ -20,9 +20,9 @@ At least one optional attribute is required. # first we create a property set so we can use a data source to retrieve it. resource "apstra_freeform_property_set" "prop_set_foo" { - blueprint_id = "freeform_blueprint-5ba09d07" + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" name = "prop_set_foo" - values = jsonencode({ + values = jsonencode({ foo = "bar" clown = 2 }) @@ -31,21 +31,21 @@ resource "apstra_freeform_property_set" "prop_set_foo" { # here we retrieve the property_set. data "apstra_freeform_property_set" "foo" { - blueprint_id = "freeform_blueprint-5ba09d07" + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" name = apstra_freeform_property_set.prop_set_foo.name } -#here we build an output block to display it. -output "foo" {value = data.apstra_freeform_property_set.foo} - -#Output looks like this -#foo = { -# "blueprint_id" = "freeform_blueprint-5ba09d07" -# "id" = tostring(null) -# "name" = "prop_set_foo" -# "system_id" = tostring(null) -# "values" = "{\"clown\": 2, \"foo\": \"bar\"}" -#} +# here we build an output block to display it. +output "foo" { value = data.apstra_freeform_property_set.foo } + +# Output looks like this +# foo = { +# "blueprint_id" = "043c5787-66e8-41c7-8925-c7e52fbe6e32" +# "id" = tostring(null) +# "name" = "prop_set_foo" +# "system_id" = tostring(null) +# "values" = "{\"clown\": 2, \"foo\": \"bar\"}" +# } ``` diff --git a/docs/data-sources/freeform_system.md b/docs/data-sources/freeform_system.md index a53d3aba..bf6a546a 100644 --- a/docs/data-sources/freeform_system.md +++ b/docs/data-sources/freeform_system.md @@ -18,9 +18,8 @@ At least one optional attribute is required. ```terraform # This example defines a freeform system in a blueprint - resource "apstra_freeform_system" "test" { - blueprint_id = "freeform_blueprint-5ba09d07" + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" name = "test_system" tags = ["a", "b", "c"] type = "internal" @@ -32,7 +31,7 @@ resource "apstra_freeform_system" "test" { # here we retrieve the freeform system data "apstra_freeform_system" "test" { - blueprint_id = "freeform_blueprint-5ba09d07" + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" id = apstra_freeform_system.test.id } @@ -40,22 +39,22 @@ data "apstra_freeform_system" "test" { output "test_System_out" {value = data.apstra_freeform_system.test} -#Output looks like this -#test_System_out = { -# "blueprint_id" = "freeform_blueprint-5ba09d07" -# "deploy_mode" = tostring(null) -# "device_profile_id" = "PtrWb4-VSwKiYRbCodk" -# "hostname" = "systemfoo" -# "id" = "-63CYLAiWuAq0ljzX0Q" -# "name" = "test_system_foo" -# "system_id" = tostring(null) -# "tags" = toset([ -# "a", -# "b", -# "c", -# ]) -# "type" = "internal" -#} +# Output looks like this +# test_System_out = { +# "blueprint_id" = "043c5787-66e8-41c7-8925-c7e52fbe6e32" +# "deploy_mode" = tostring(null) +# "device_profile_id" = "PtrWb4-VSwKiYRbCodk" +# "hostname" = "systemfoo" +# "id" = "-63CYLAiWuAq0ljzX0Q" +# "name" = "test_system_foo" +# "system_id" = tostring(null) +# "tags" = toset([ +# "a", +# "b", +# "c", +# ]) +# "type" = "internal" +# } ``` diff --git a/docs/resources/freeform_property_set.md b/docs/resources/freeform_property_set.md index 2a70cc13..9fdc9d4a 100644 --- a/docs/resources/freeform_property_set.md +++ b/docs/resources/freeform_property_set.md @@ -16,7 +16,7 @@ This resource creates a Property Set in a Freeform Blueprint. # Create a freeform property set resource. resource "apstra_freeform_property_set" "prop_set_foo" { - blueprint_id = "freeform_blueprint-5ba09d07" + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" name = "prop_set_foo" values = jsonencode({ foo = "bar" @@ -27,7 +27,7 @@ resource "apstra_freeform_property_set" "prop_set_foo" { # Read the property set back with a data source. data "apstra_freeform_property_set" "foods" { - blueprint_id = "freeform_blueprint-5ba09d07" + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" name = apstra_freeform_property_set.prop_set_foo.name } @@ -36,13 +36,13 @@ data "apstra_freeform_property_set" "foods" { output "foo" {value = data.apstra_freeform_property_set.foods} # Output should look like: -#foo = { -# "blueprint_id" = "freeform_blueprint-5ba09d07" -# "id" = tostring(null) -# "name" = "prop_set_foo" -# "system_id" = tostring(null) -# "values" = "{\"clown\": 2, \"foo\": \"bar\"}" -#} +# foo = { +# "blueprint_id" = "freeform_blueprint-5ba09d07" +# "id" = tostring(null) +# "name" = "prop_set_foo" +# "system_id" = tostring(null) +# "values" = "{\"clown\": 2, \"foo\": \"bar\"}" +# } ``` diff --git a/docs/resources/freeform_system.md b/docs/resources/freeform_system.md index ec0f07e9..72e2e773 100644 --- a/docs/resources/freeform_system.md +++ b/docs/resources/freeform_system.md @@ -17,42 +17,42 @@ This resource creates a System in a Freeform Blueprint. resource "apstra_freeform_system" "test" { - blueprint_id = "freeform_blueprint-5ba09d07" - name = "test_system" - tags = ["a", "b", "c"] - type = "internal" - hostname = "testsystem" - deploy_mode = "deploy" + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + name = "test_system" + tags = ["a", "b", "c"] + type = "internal" + hostname = "testsystem" + deploy_mode = "deploy" device_profile_id = "PtrWb4-VSwKiYRbCodk" } # here we retrieve the freeform system data "apstra_freeform_system" "test" { - blueprint_id = "freeform_blueprint-5ba09d07" - id = apstra_freeform_system.test.id + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + id = apstra_freeform_system.test.id } # here we build an output bock to display it -output "test_System_out" {value = data.apstra_freeform_system.test} - -#Output looks like this -#test_System_out = { -# "blueprint_id" = "freeform_blueprint-5ba09d07" -# "deploy_mode" = tostring(null) -# "device_profile_id" = "PtrWb4-VSwKiYRbCodk" -# "hostname" = "systemfoo" -# "id" = "-63CYLAiWuAq0ljzX0Q" -# "name" = "test_system_foo" -# "system_id" = tostring(null) -# "tags" = toset([ -# "a", -# "b", -# "c", -# ]) -# "type" = "internal" -#} +output "test_System_out" { value = data.apstra_freeform_system.test } + +# Output looks like this +# test_System_out = { +# "blueprint_id" = "043c5787-66e8-41c7-8925-c7e52fbe6e32" +# "deploy_mode" = tostring(null) +# "device_profile_id" = "PtrWb4-VSwKiYRbCodk" +# "hostname" = "systemfoo" +# "id" = "-63CYLAiWuAq0ljzX0Q" +# "name" = "test_system_foo" +# "system_id" = tostring(null) +# "tags" = toset([ +# "a", +# "b", +# "c", +# ]) +# "type" = "internal" +# } ``` diff --git a/examples/data-sources/apstra_freeform_property_set/example.tf b/examples/data-sources/apstra_freeform_property_set/example.tf index 371afac4..741bdad0 100644 --- a/examples/data-sources/apstra_freeform_property_set/example.tf +++ b/examples/data-sources/apstra_freeform_property_set/example.tf @@ -17,15 +17,15 @@ data "apstra_freeform_property_set" "foo" { name = apstra_freeform_property_set.prop_set_foo.name } -#here we build an output block to display it. +# here we build an output block to display it. output "foo" { value = data.apstra_freeform_property_set.foo } -#Output looks like this -#foo = { -# "blueprint_id" = "043c5787-66e8-41c7-8925-c7e52fbe6e32" -# "id" = tostring(null) -# "name" = "prop_set_foo" -# "system_id" = tostring(null) -# "values" = "{\"clown\": 2, \"foo\": \"bar\"}" -#} +# Output looks like this +# foo = { +# "blueprint_id" = "043c5787-66e8-41c7-8925-c7e52fbe6e32" +# "id" = tostring(null) +# "name" = "prop_set_foo" +# "system_id" = tostring(null) +# "values" = "{\"clown\": 2, \"foo\": \"bar\"}" +# } diff --git a/examples/data-sources/apstra_freeform_system/example.tf b/examples/data-sources/apstra_freeform_system/example.tf index 1fdfd02f..a0643c8f 100644 --- a/examples/data-sources/apstra_freeform_system/example.tf +++ b/examples/data-sources/apstra_freeform_system/example.tf @@ -1,6 +1,5 @@ # This example defines a freeform system in a blueprint - resource "apstra_freeform_system" "test" { blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" name = "test_system" @@ -22,20 +21,20 @@ data "apstra_freeform_system" "test" { output "test_System_out" {value = data.apstra_freeform_system.test} -#Output looks like this -#test_System_out = { -# "blueprint_id" = "043c5787-66e8-41c7-8925-c7e52fbe6e32" -# "deploy_mode" = tostring(null) -# "device_profile_id" = "PtrWb4-VSwKiYRbCodk" -# "hostname" = "systemfoo" -# "id" = "-63CYLAiWuAq0ljzX0Q" -# "name" = "test_system_foo" -# "system_id" = tostring(null) -# "tags" = toset([ -# "a", -# "b", -# "c", -# ]) -# "type" = "internal" -#} +# Output looks like this +# test_System_out = { +# "blueprint_id" = "043c5787-66e8-41c7-8925-c7e52fbe6e32" +# "deploy_mode" = tostring(null) +# "device_profile_id" = "PtrWb4-VSwKiYRbCodk" +# "hostname" = "systemfoo" +# "id" = "-63CYLAiWuAq0ljzX0Q" +# "name" = "test_system_foo" +# "system_id" = tostring(null) +# "tags" = toset([ +# "a", +# "b", +# "c", +# ]) +# "type" = "internal" +# } diff --git a/examples/resources/apstra_freeform_property_set/example.tf b/examples/resources/apstra_freeform_property_set/example.tf index f5579ea1..28f3249b 100644 --- a/examples/resources/apstra_freeform_property_set/example.tf +++ b/examples/resources/apstra_freeform_property_set/example.tf @@ -21,10 +21,10 @@ data "apstra_freeform_property_set" "foods" { output "foo" {value = data.apstra_freeform_property_set.foods} # Output should look like: -#foo = { -# "blueprint_id" = "freeform_blueprint-5ba09d07" -# "id" = tostring(null) -# "name" = "prop_set_foo" -# "system_id" = tostring(null) -# "values" = "{\"clown\": 2, \"foo\": \"bar\"}" -#} +# foo = { +# "blueprint_id" = "freeform_blueprint-5ba09d07" +# "id" = tostring(null) +# "name" = "prop_set_foo" +# "system_id" = tostring(null) +# "values" = "{\"clown\": 2, \"foo\": \"bar\"}" +# } diff --git a/examples/resources/apstra_freeform_system/example.tf b/examples/resources/apstra_freeform_system/example.tf index f455a473..22cbafa4 100644 --- a/examples/resources/apstra_freeform_system/example.tf +++ b/examples/resources/apstra_freeform_system/example.tf @@ -22,19 +22,19 @@ data "apstra_freeform_system" "test" { output "test_System_out" { value = data.apstra_freeform_system.test } -#Output looks like this -#test_System_out = { -# "blueprint_id" = "043c5787-66e8-41c7-8925-c7e52fbe6e32" -# "deploy_mode" = tostring(null) -# "device_profile_id" = "PtrWb4-VSwKiYRbCodk" -# "hostname" = "systemfoo" -# "id" = "-63CYLAiWuAq0ljzX0Q" -# "name" = "test_system_foo" -# "system_id" = tostring(null) -# "tags" = toset([ -# "a", -# "b", -# "c", -# ]) -# "type" = "internal" -#} +# Output looks like this +# test_System_out = { +# "blueprint_id" = "043c5787-66e8-41c7-8925-c7e52fbe6e32" +# "deploy_mode" = tostring(null) +# "device_profile_id" = "PtrWb4-VSwKiYRbCodk" +# "hostname" = "systemfoo" +# "id" = "-63CYLAiWuAq0ljzX0Q" +# "name" = "test_system_foo" +# "system_id" = tostring(null) +# "tags" = toset([ +# "a", +# "b", +# "c", +# ]) +# "type" = "internal" +# } From 9474cf029d2e1a0749e3f128daa45853e31c7c00 Mon Sep 17 00:00:00 2001 From: bwJuniper <98770420+bwJuniper@users.noreply.github.com> Date: Fri, 12 Jul 2024 21:52:01 +0200 Subject: [PATCH 12/18] add first commit of freeform_link & methods for link --- apstra/blueprint/freeform_endpoint.go | 111 +++++++++++ apstra/blueprint/freeform_link.go | 253 ++++++++++++++++++++++++++ apstra/data_source_freeform_link.go | 101 ++++++++++ apstra/resource_freeform_link.go | 211 +++++++++++++++++++++ go.mod | 2 +- go.sum | 4 +- 6 files changed, 679 insertions(+), 3 deletions(-) create mode 100644 apstra/blueprint/freeform_endpoint.go create mode 100644 apstra/blueprint/freeform_link.go create mode 100644 apstra/data_source_freeform_link.go create mode 100644 apstra/resource_freeform_link.go diff --git a/apstra/blueprint/freeform_endpoint.go b/apstra/blueprint/freeform_endpoint.go new file mode 100644 index 00000000..b33cbd86 --- /dev/null +++ b/apstra/blueprint/freeform_endpoint.go @@ -0,0 +1,111 @@ +package blueprint + +import ( + "context" + "github.com/Juniper/apstra-go-sdk/apstra" + "github.com/Juniper/terraform-provider-apstra/apstra/utils" + "github.com/hashicorp/terraform-plugin-framework-nettypes/cidrtypes" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + resourceSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "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" + "net" +) + +type freeformEndpoint struct { + SystemId types.String `tfsdk:"system_id"` + InterfaceName types.String `tfsdk:"interface_name"` + TransformationId types.Int64 `tfsdk:"transformation_id"` + Ipv4Address cidrtypes.IPv4Prefix `tfsdk:"ipv4_address"` + Ipv6Address cidrtypes.IPv6Prefix `tfsdk:"ipv6_address"` +} + +func (o freeformEndpoint) attrTypes() map[string]attr.Type { + return map[string]attr.Type{ + "system_id": types.StringType, + "interface_name": types.StringType, + "transformation_id": types.Int64Type, + "ipv4_address": cidrtypes.IPv4PrefixType{}, + "ipv6_address": cidrtypes.IPv6PrefixType{}, + } +} + +func (o freeformEndpoint) ResourceAttributes() map[string]resourceSchema.Attribute { + return map[string]resourceSchema.Attribute{ + "system_id": resourceSchema.StringAttribute{ + Required: true, + MarkdownDescription: "System ID ", + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "interface_name": resourceSchema.StringAttribute{ + Required: true, + MarkdownDescription: "fill this out", + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "transformation_id": resourceSchema.Int64Attribute{ + Required: true, + MarkdownDescription: "fill this out", + PlanModifiers: []planmodifier.Int64{int64planmodifier.RequiresReplace()}, + Validators: []validator.Int64{int64validator.AtLeast(1)}, + }, + "ipv4_address": resourceSchema.StringAttribute{ + Optional: true, + MarkdownDescription: "Ipv4 address of the interface", + CustomType: cidrtypes.IPv4PrefixType{}, + }, + "ipv6_address": resourceSchema.StringAttribute{ + Optional: true, + MarkdownDescription: "Ipv6 address of the interface", + CustomType: cidrtypes.IPv6PrefixType{}, + }, + } +} + +func (o *freeformEndpoint) request() *apstra.FreeformEndpoint { + var ipNet4, ipNet6 *net.IPNet + if !o.Ipv4Address.IsNull() { + var ip4 net.IP + ip4, ipNet4, _ = net.ParseCIDR(o.Ipv4Address.ValueString()) + ipNet4.IP = ip4 + } + if !o.Ipv6Address.IsNull() { + var ip6 net.IP + ip6, ipNet6, _ = net.ParseCIDR(o.Ipv6Address.ValueString()) + ipNet6.IP = ip6 + } + + return &apstra.FreeformEndpoint{ + SystemId: apstra.ObjectId(o.SystemId.ValueString()), + Interface: apstra.FreeformInterfaceData{ + IfName: o.InterfaceName.ValueString(), + TransformationId: int(o.TransformationId.ValueInt64()), + Ipv4Address: ipNet4, + Ipv6Address: ipNet6, + }, + } +} + +func (o *freeformEndpoint) loadApiData(_ context.Context, in apstra.FreeformEndpoint, _ *diag.Diagnostics) { + o.InterfaceName = types.StringValue(in.Interface.IfName) + o.SystemId = types.StringValue(in.SystemId.String()) +} + +func newFreeformEndpointSet(ctx context.Context, in [2]apstra.FreeformEndpoint, diags *diag.Diagnostics) types.Set { + endpoints := make([]freeformEndpoint, len(in)) + for i, endpoint := range in { + endpoints[i].loadApiData(ctx, endpoint, diags) + } + if diags.HasError() { + return types.SetNull(types.ObjectType{AttrTypes: freeformEndpoint{}.attrTypes()}) + } + + return utils.SetValueOrNull(ctx, types.ObjectType{AttrTypes: freeformEndpoint{}.attrTypes()}, endpoints, diags) +} diff --git a/apstra/blueprint/freeform_link.go b/apstra/blueprint/freeform_link.go new file mode 100644 index 00000000..76c695e6 --- /dev/null +++ b/apstra/blueprint/freeform_link.go @@ -0,0 +1,253 @@ +package blueprint + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" + "regexp" + + "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" + "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 FreeformLink struct { + BlueprintId types.String `tfsdk:"blueprint_id"` + Id types.String `tfsdk:"id"` + Speed types.String `tfsdk:"speed"` + Type types.String `tfsdk:"type"` + Name types.String `tfsdk:"name"` + AggregateLinkId types.String `tfsdk:"aggregate_link_id"` + Endpoints types.Set `tfsdk:"endpoints"` + InterfaceIds types.Set `tfsdk:"interface_ids"` + Tags types.Set `tfsdk:"tags"` +} + +func (o FreeformLink) 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 Link lives.", + Required: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Populate this field to look up the Freeform Link 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 Link by Name. Required when `id` is omitted.", + Optional: true, + Computed: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "speed": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Speed of the Link", + Computed: true, + }, + "type": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Link type", + Computed: true, + }, + "aggregate_link_id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "aggregate link ID", + Computed: true, + }, + "endpoints": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Endpoints assigned to the Link", + Computed: true, + }, + "interface_ids": dataSourceSchema.SetAttribute{ + MarkdownDescription: "Interface IDs associated with the link", + Computed: true, + }, + "tags": dataSourceSchema.SetAttribute{ + MarkdownDescription: "Set of Tag labels", + ElementType: types.StringType, + Computed: true, + }, + } +} + +func (o FreeformLink) 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 Link.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "name": resourceSchema.StringAttribute{ + MarkdownDescription: "Freeform Link 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.-_")}, + }, + "speed": resourceSchema.StringAttribute{ + MarkdownDescription: "Speed of the Freeform Link.", + Required: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "type": resourceSchema.StringAttribute{ + MarkdownDescription: "Deploy mode of the Link", + Optional: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "aggregate_link_id": resourceSchema.StringAttribute{ + MarkdownDescription: "Aggregate ID of the Link", + Optional: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "endpoints": resourceSchema.SetNestedAttribute{ + NestedObject: resourceSchema.NestedAttributeObject{ + Attributes: freeformEndpoint{}.ResourceAttributes(), + }, + PlanModifiers: []planmodifier.Set{setplanmodifier.RequiresReplace()}, + MarkdownDescription: "Endpoints of the Link", + Required: true, + Validators: []validator.Set{setvalidator.SizeBetween(2, 2)}, + }, + "interface_ids": resourceSchema.SetAttribute{ + MarkdownDescription: "Interface IDs associated with the link", + Computed: true, + ElementType: types.StringType, + Validators: []validator.Set{setvalidator.SizeBetween(2, 2)}, + }, + "tags": resourceSchema.SetAttribute{ + MarkdownDescription: "Set of Tag labels", + ElementType: types.StringType, + Optional: true, + Validators: []validator.Set{setvalidator.SizeAtLeast(1)}, + }, + } +} + +func (o *FreeformLink) Request(ctx context.Context, diags *diag.Diagnostics) *apstra.FreeformLinkRequest { + var tags []string + diags.Append(o.Tags.ElementsAs(ctx, &tags, false)...) + if diags.HasError() { + return nil + } + + var endpoints []freeformEndpoint + diags.Append(o.Endpoints.ElementsAs(ctx, &endpoints, false)...) + if diags.HasError() { + return nil + } + + var epArray [2]apstra.FreeformEndpoint + for i, endpoint := range endpoints { + epArray[i] = *endpoint.request() + } + + return &apstra.FreeformLinkRequest{ + Label: o.Name.ValueString(), + Tags: tags, + Endpoints: epArray, + } +} + +func (o *FreeformLink) GetInterfaceIds(ctx context.Context, bp *apstra.FreeformClient, diags *diag.Diagnostics) { + var endpoints []freeformEndpoint + diags.Append(o.Endpoints.ElementsAs(ctx, &endpoints, false)...) + if diags.HasError() { + return + } + + query := new(apstra.PathQuery). + SetClient(bp.Client()). + SetBlueprintId(bp.Id()). + //node('system', id='v7A_2gUtP9vHq2DYhug') + Node([]apstra.QEEAttribute{ + apstra.NodeTypeSystem.QEEAttribute(), + {Key: "id", Value: apstra.QEStringVal(endpoints[0].SystemId.ValueString())}, + }). + //.out('hosted_interfaces') + Out([]apstra.QEEAttribute{apstra.RelationshipTypeHostedInterfaces.QEEAttribute()}). + //.node('interface', name='n_interface_0') + Node([]apstra.QEEAttribute{ + apstra.NodeTypeInterface.QEEAttribute(), + {Key: "name", Value: apstra.QEStringVal("n_interface_0")}, + }). + //.out('link') + Out([]apstra.QEEAttribute{apstra.RelationshipTypeLink.QEEAttribute()}). + //.node('link', id='P6oXpH9ho-_m41LLcSY') + Node([]apstra.QEEAttribute{ + apstra.NodeTypeLink.QEEAttribute(), + {Key: "id", Value: apstra.QEStringVal(o.Id.ValueString())}, + }). + //.in_('link') + In([]apstra.QEEAttribute{apstra.RelationshipTypeLink.QEEAttribute()}). + //.node('interface', name='n_interface_1') + Node([]apstra.QEEAttribute{ + apstra.NodeTypeInterface.QEEAttribute(), + {Key: "name", Value: apstra.QEStringVal("n_interface_1")}, + //.in_('hosted_interfaces') + }). + In([]apstra.QEEAttribute{apstra.RelationshipTypeHostedInterfaces.QEEAttribute()}). + //.node('system', id='uPeP0h4d-q8OAIomCpY') + Node([]apstra.QEEAttribute{ + apstra.NodeTypeSystem.QEEAttribute(), + {Key: "id", Value: apstra.QEStringVal(endpoints[1].SystemId.ValueString())}, + }) + + var response struct { + Items []struct { + Interface0 struct { + Id string `json:"id"` + } `json:"n_interface_0"` + Interface1 struct { + Id string `json:"id"` + } `json:"n_interface_1"` + } `json:"items"` + } + + err := query.Do(ctx, &response) + if err != nil { + diags.AddError("unable to perform query.do()", "query.do()") + return + } + if len(response.Items) != 1 { + diags.AddError("the Query response is incorrect", "the query is not 1 response ") + return + } + + interfaceIds := make([]attr.Value, 2) + interfaceIds[0] = types.StringValue(response.Items[0].Interface0.Id) + interfaceIds[1] = types.StringValue(response.Items[0].Interface1.Id) + + var d diag.Diagnostics + o.InterfaceIds, d = types.SetValue(types.StringType, interfaceIds) + diags.Append(d...) +} + +func (o *FreeformLink) LoadApiData(ctx context.Context, in *apstra.FreeformLinkData, diags *diag.Diagnostics) { + o.Speed = types.StringValue(string(in.Speed)) + o.Type = types.StringValue(in.Type.String()) + o.Name = types.StringValue(in.Label) + o.Endpoints = newFreeformEndpointSet(ctx, in.Endpoints, diags) // safe to ignore diagnostic here + o.AggregateLinkId = types.StringValue(in.AggregateLinkId.String()) + o.Tags = utils.SetValueOrNull(ctx, types.StringType, in.Tags, diags) // safe to ignore diagnostic here +} diff --git a/apstra/data_source_freeform_link.go b/apstra/data_source_freeform_link.go new file mode 100644 index 00000000..7a232b4c --- /dev/null +++ b/apstra/data_source_freeform_link.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 = &dataSourceFreeformLink{} + _ datasourceWithSetFfBpClientFunc = &dataSourceFreeformLink{} +) + +type dataSourceFreeformLink struct { + getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) +} + +func (o *dataSourceFreeformLink) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_freeform_link" +} + +func (o *dataSourceFreeformLink) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + configureDataSource(ctx, o, req, resp) +} + +func (o *dataSourceFreeformLink) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: docCategoryFreeform + "This data source provides details of a specific Freeform Link.\n\n" + + "At least one optional attribute is required.", + Attributes: blueprint.FreeformLink{}.DataSourceAttributes(), + } +} + +func (o *dataSourceFreeformLink) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config blueprint.FreeformLink + 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.FreeformLink + switch { + case !config.Id.IsNull(): + api, err = bp.GetLink(ctx, apstra.ObjectId(config.Id.ValueString())) + if utils.IsApstra404(err) { + resp.Diagnostics.AddAttributeError( + path.Root("id"), + "Freeform Link not found", + fmt.Sprintf("Freeform Link with ID %s not found", config.Id)) + return + } + case !config.Name.IsNull(): + api, err = bp.GetLinkByName(ctx, config.Name.ValueString()) + if utils.IsApstra404(err) { + resp.Diagnostics.AddAttributeError( + path.Root("name"), + "Freeform Link not found", + fmt.Sprintf("Freeform Link with Name %s not found", config.Name)) + return + } + } + if err != nil { + resp.Diagnostics.AddError("failed reading Freeform Link", err.Error()) + return + } + if api.Data == nil { + resp.Diagnostics.AddError("failed reading Freeform Link", "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 *dataSourceFreeformLink) setBpClientFunc(f func(context.Context, string) (*apstra.FreeformClient, error)) { + o.getBpClientFunc = f +} diff --git a/apstra/resource_freeform_link.go b/apstra/resource_freeform_link.go new file mode 100644 index 00000000..c8402654 --- /dev/null +++ b/apstra/resource_freeform_link.go @@ -0,0 +1,211 @@ +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 = &resourceFreeformLink{} + _ resourceWithSetFfBpClientFunc = &resourceFreeformLink{} + _ resourceWithSetBpLockFunc = &resourceFreeformLink{} +) + +type resourceFreeformLink struct { + getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error) + lockFunc func(context.Context, string) error +} + +func (o *resourceFreeformLink) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_freeform_link" +} + +func (o *resourceFreeformLink) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + configureResource(ctx, o, req, resp) +} + +func (o *resourceFreeformLink) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: docCategoryFreeform + "This resource creates a Link in a Freeform Blueprint.", + Attributes: blueprint.FreeformLink{}.ResourceAttributes(), + } +} + +func (o *resourceFreeformLink) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan blueprint.FreeformLink + 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.CreateLink(ctx, request) + if err != nil { + resp.Diagnostics.AddError("error creating new Link", err.Error()) + return + } + + plan.Id = types.StringValue(id.String()) + plan.GetInterfaceIds(ctx, bp, &resp.Diagnostics) + + // set state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (o *resourceFreeformLink) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state blueprint.FreeformLink + 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.GetLink(ctx, apstra.ObjectId(state.Id.ValueString())) + if err != nil { + if utils.IsApstra404(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("Error retrieving Freeform Link", 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 *resourceFreeformLink) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Get plan values + var plan blueprint.FreeformLink + 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 Link + err = bp.UpdateLink(ctx, apstra.ObjectId(plan.Id.ValueString()), request) + if err != nil { + resp.Diagnostics.AddError("error updating Freeform Link", err.Error()) + return + } + + // set state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (o *resourceFreeformLink) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state blueprint.FreeformLink + 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 Link by calling API + err = bp.DeleteLink(ctx, apstra.ObjectId(state.Id.ValueString())) + if err != nil { + if utils.IsApstra404(err) { + return // 404 is okay + } + resp.Diagnostics.AddError("error deleting Freeform Link", err.Error()) + return + } +} + +func (o *resourceFreeformLink) setBpClientFunc(f func(context.Context, string) (*apstra.FreeformClient, error)) { + o.getBpClientFunc = f +} + +func (o *resourceFreeformLink) setBpLockFunc(f func(context.Context, string) error) { + o.lockFunc = f +} diff --git a/go.mod b/go.mod index 887c0b06..bf2451f8 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ toolchain go1.21.1 require ( github.com/IBM/netaddr v1.5.0 - github.com/Juniper/apstra-go-sdk v0.0.0-20240702201035-23330826fe39 + github.com/Juniper/apstra-go-sdk v0.0.0-20240712195009-8b78f5ce34db 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 cf63b353..be5be22b 100644 --- a/go.sum +++ b/go.sum @@ -108,8 +108,8 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= 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-20240702201035-23330826fe39 h1:BnX79x6kyLpJg6AT+xI8T8OiWUlmc1mUX+jU9AV+f+w= -github.com/Juniper/apstra-go-sdk v0.0.0-20240702201035-23330826fe39/go.mod h1:Xwj3X8v/jRZWv28o6vQAqD4lz2JmzaSYLZ2ch1SS89w= +github.com/Juniper/apstra-go-sdk v0.0.0-20240712195009-8b78f5ce34db h1:1efAHckxbNyrnSaG7fwHHlDlMUk/0CLILGBm/DkDQ/8= +github.com/Juniper/apstra-go-sdk v0.0.0-20240712195009-8b78f5ce34db/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 97c1ec59f8b3f1013cbf756265ab705d78f9801e Mon Sep 17 00:00:00 2001 From: bwJuniper <98770420+bwJuniper@users.noreply.github.com> Date: Wed, 17 Jul 2024 23:10:35 +0200 Subject: [PATCH 13/18] ongoing link work. --- apstra/blueprint/freeform_endpoint.go | 104 +++++++++--- apstra/blueprint/freeform_link.go | 153 ++++++------------ apstra/provider.go | 2 + apstra/resource_freeform_link.go | 9 +- .../apstra_freeform_link/example.tf | 59 +++++++ .../resources/apstra_freeform_link/example.tf | 56 +++++++ go.mod | 2 +- go.sum | 4 +- 8 files changed, 252 insertions(+), 137 deletions(-) create mode 100644 examples/data-sources/apstra_freeform_link/example.tf create mode 100644 examples/resources/apstra_freeform_link/example.tf diff --git a/apstra/blueprint/freeform_endpoint.go b/apstra/blueprint/freeform_endpoint.go index b33cbd86..af98388a 100644 --- a/apstra/blueprint/freeform_endpoint.go +++ b/apstra/blueprint/freeform_endpoint.go @@ -2,12 +2,14 @@ package blueprint import ( "context" + "fmt" "github.com/Juniper/apstra-go-sdk/apstra" "github.com/Juniper/terraform-provider-apstra/apstra/utils" "github.com/hashicorp/terraform-plugin-framework-nettypes/cidrtypes" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "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" resourceSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" @@ -16,11 +18,13 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "net" + "strings" ) type freeformEndpoint struct { - SystemId types.String `tfsdk:"system_id"` + // SystemId types.String `tfsdk:"system_id"` InterfaceName types.String `tfsdk:"interface_name"` + InterfaceId types.String `tfsdk:"interface_id"` TransformationId types.Int64 `tfsdk:"transformation_id"` Ipv4Address cidrtypes.IPv4Prefix `tfsdk:"ipv4_address"` Ipv6Address cidrtypes.IPv6Prefix `tfsdk:"ipv6_address"` @@ -28,31 +32,57 @@ type freeformEndpoint struct { func (o freeformEndpoint) attrTypes() map[string]attr.Type { return map[string]attr.Type{ - "system_id": types.StringType, + // "system_id": types.StringType, "interface_name": types.StringType, + "interface_id": types.StringType, "transformation_id": types.Int64Type, "ipv4_address": cidrtypes.IPv4PrefixType{}, "ipv6_address": cidrtypes.IPv6PrefixType{}, } } +func (o freeformEndpoint) DatasourceAttributes() map[string]dataSourceSchema.Attribute { + return map[string]dataSourceSchema.Attribute{ + "interface_name": dataSourceSchema.StringAttribute{ + Computed: true, + MarkdownDescription: "the interface name", + }, + "interface_id": dataSourceSchema.StringAttribute{ + Computed: true, + MarkdownDescription: "Graph node ID of the attached interface for this side of the link endpoint ", + }, + "transformation_id": dataSourceSchema.Int64Attribute{ + Computed: true, + MarkdownDescription: "ID of the transformation in the device profile", + }, + "ipv4_address": dataSourceSchema.StringAttribute{ + Computed: true, + MarkdownDescription: "Ipv4 address of the interface", + CustomType: cidrtypes.IPv4PrefixType{}, + }, + "ipv6_address": dataSourceSchema.StringAttribute{ + Computed: true, + MarkdownDescription: "Ipv6 address of the interface", + CustomType: cidrtypes.IPv6PrefixType{}, + }, + } +} + func (o freeformEndpoint) ResourceAttributes() map[string]resourceSchema.Attribute { return map[string]resourceSchema.Attribute{ - "system_id": resourceSchema.StringAttribute{ - Required: true, - MarkdownDescription: "System ID ", - PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, - Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, - }, "interface_name": resourceSchema.StringAttribute{ Required: true, - MarkdownDescription: "fill this out", + MarkdownDescription: "the interface name", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, + "interface_id": resourceSchema.StringAttribute{ + Computed: true, + MarkdownDescription: "Graph node ID of the attached interface for this side of the link endpoint.", + }, "transformation_id": resourceSchema.Int64Attribute{ Required: true, - MarkdownDescription: "fill this out", + MarkdownDescription: "ID of the transformation in the device profile", PlanModifiers: []planmodifier.Int64{int64planmodifier.RequiresReplace()}, Validators: []validator.Int64{int64validator.AtLeast(1)}, }, @@ -69,7 +99,7 @@ func (o freeformEndpoint) ResourceAttributes() map[string]resourceSchema.Attribu } } -func (o *freeformEndpoint) request() *apstra.FreeformEndpoint { +func (o *freeformEndpoint) request(systemId string) *apstra.FreeformEndpoint { var ipNet4, ipNet6 *net.IPNet if !o.Ipv4Address.IsNull() { var ip4 net.IP @@ -83,29 +113,51 @@ func (o *freeformEndpoint) request() *apstra.FreeformEndpoint { } return &apstra.FreeformEndpoint{ - SystemId: apstra.ObjectId(o.SystemId.ValueString()), - Interface: apstra.FreeformInterfaceData{ - IfName: o.InterfaceName.ValueString(), - TransformationId: int(o.TransformationId.ValueInt64()), - Ipv4Address: ipNet4, - Ipv6Address: ipNet6, + SystemId: apstra.ObjectId(systemId), + Interface: apstra.FreeformInterface{ + Data: &apstra.FreeformInterfaceData{ + IfName: o.InterfaceName.ValueString(), + TransformationId: int(o.TransformationId.ValueInt64()), + Ipv4Address: ipNet4, + Ipv6Address: ipNet6, + }, }, } } -func (o *freeformEndpoint) loadApiData(_ context.Context, in apstra.FreeformEndpoint, _ *diag.Diagnostics) { - o.InterfaceName = types.StringValue(in.Interface.IfName) - o.SystemId = types.StringValue(in.SystemId.String()) +func (o *freeformEndpoint) loadApiData(_ context.Context, in apstra.FreeformEndpoint, diags *diag.Diagnostics) { + if in.Interface.Id == nil { + diags.AddError( + fmt.Sprintf("api returned nil interface Id for system %s", in.SystemId), + "interface IDs should always be populated", + ) + return + } + + o.InterfaceName = types.StringValue(in.Interface.Data.IfName) + o.InterfaceId = types.StringValue(in.Interface.Id.String()) + //o.SystemId = types.StringValue(in.SystemId.String()) + o.TransformationId = types.Int64Value(int64(in.Interface.Data.TransformationId)) + o.Ipv4Address = cidrtypes.NewIPv4PrefixValue(in.Interface.Data.Ipv4Address.String()) + if strings.Contains(o.Ipv4Address.ValueString(), "nil") { + o.Ipv4Address = cidrtypes.NewIPv4PrefixNull() + } + o.Ipv6Address = cidrtypes.NewIPv6PrefixValue(in.Interface.Data.Ipv6Address.String()) + if strings.Contains(o.Ipv6Address.ValueString(), "nil") { + o.Ipv6Address = cidrtypes.NewIPv6PrefixNull() + } } -func newFreeformEndpointSet(ctx context.Context, in [2]apstra.FreeformEndpoint, diags *diag.Diagnostics) types.Set { - endpoints := make([]freeformEndpoint, len(in)) - for i, endpoint := range in { - endpoints[i].loadApiData(ctx, endpoint, diags) +func newFreeformEndpointMap(ctx context.Context, in [2]apstra.FreeformEndpoint, diags *diag.Diagnostics) types.Map { + endpoints := make(map[string]freeformEndpoint, len(in)) + for i := range in { + var endpoint freeformEndpoint + endpoint.loadApiData(ctx, in[i], diags) + endpoints[in[i].SystemId.String()] = endpoint } if diags.HasError() { - return types.SetNull(types.ObjectType{AttrTypes: freeformEndpoint{}.attrTypes()}) + return types.MapNull(types.ObjectType{AttrTypes: freeformEndpoint{}.attrTypes()}) } - return utils.SetValueOrNull(ctx, types.ObjectType{AttrTypes: freeformEndpoint{}.attrTypes()}, endpoints, diags) + return utils.MapValueOrNull(ctx, types.ObjectType{AttrTypes: freeformEndpoint{}.attrTypes()}, endpoints, diags) } diff --git a/apstra/blueprint/freeform_link.go b/apstra/blueprint/freeform_link.go index 76c695e6..ab83b1b0 100644 --- a/apstra/blueprint/freeform_link.go +++ b/apstra/blueprint/freeform_link.go @@ -2,8 +2,9 @@ package blueprint import ( "context" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" + "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" "regexp" "github.com/Juniper/apstra-go-sdk/apstra" @@ -27,8 +28,7 @@ type FreeformLink struct { Type types.String `tfsdk:"type"` Name types.String `tfsdk:"name"` AggregateLinkId types.String `tfsdk:"aggregate_link_id"` - Endpoints types.Set `tfsdk:"endpoints"` - InterfaceIds types.Set `tfsdk:"interface_ids"` + Endpoints types.Map `tfsdk:"endpoints"` Tags types.Set `tfsdk:"tags"` } @@ -59,27 +59,34 @@ func (o FreeformLink) DataSourceAttributes() map[string]dataSourceSchema.Attribu Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, "speed": dataSourceSchema.StringAttribute{ - MarkdownDescription: "Speed of the Link", - Computed: true, + MarkdownDescription: "Speed of the Link " + + "200G | 5G | 1G | 100G | 150g | 40g | 2500M | 25G | 25g | 10G | 50G | 800G " + + "| 10M | 100m | 2500m | 50g | 400g | 400G | 200g | 5g | 800g | 100M | 10g " + + "| 150G | 10m | 100g | 1g | 40G", + Computed: true, }, "type": dataSourceSchema.StringAttribute{ - MarkdownDescription: "Link type", - Computed: true, + MarkdownDescription: "aggregate_link | ethernet\n" + + "Link Type. An 'ethernet' link is a normal front-panel interface. " + + "An 'aggregate_link' is a bonded interface which is typically used for LACP or Static LAGs. " + + "Note that the lag_mode parameter is a property of the interface and not the link, " + + "since interfaces may have different lag modes on opposite sides of the link - " + + "eg lacp_passive <-> lacp_active", + Computed: true, }, "aggregate_link_id": dataSourceSchema.StringAttribute{ - MarkdownDescription: "aggregate link ID", + MarkdownDescription: "ID of aggregate link node that the current link belongs to", Computed: true, }, - "endpoints": dataSourceSchema.StringAttribute{ + "endpoints": dataSourceSchema.MapNestedAttribute{ MarkdownDescription: "Endpoints assigned to the Link", Computed: true, - }, - "interface_ids": dataSourceSchema.SetAttribute{ - MarkdownDescription: "Interface IDs associated with the link", - Computed: true, + NestedObject: dataSourceSchema.NestedAttributeObject{ + Attributes: freeformEndpoint{}.DatasourceAttributes(), + }, }, "tags": dataSourceSchema.SetAttribute{ - MarkdownDescription: "Set of Tag labels", + MarkdownDescription: "Set of unique case-insensitive tag labels", ElementType: types.StringType, Computed: true, }, @@ -107,33 +114,25 @@ func (o FreeformLink) ResourceAttributes() map[string]resourceSchema.Attribute { }, "speed": resourceSchema.StringAttribute{ MarkdownDescription: "Speed of the Freeform Link.", - Required: true, - Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + Computed: true, }, "type": resourceSchema.StringAttribute{ MarkdownDescription: "Deploy mode of the Link", - Optional: true, - Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + Computed: true, }, "aggregate_link_id": resourceSchema.StringAttribute{ - MarkdownDescription: "Aggregate ID of the Link", + MarkdownDescription: "ID of aggregate link node that the current link belongs to", Optional: true, Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, - "endpoints": resourceSchema.SetNestedAttribute{ + "endpoints": resourceSchema.MapNestedAttribute{ NestedObject: resourceSchema.NestedAttributeObject{ Attributes: freeformEndpoint{}.ResourceAttributes(), }, - PlanModifiers: []planmodifier.Set{setplanmodifier.RequiresReplace()}, + PlanModifiers: []planmodifier.Map{mapplanmodifier.RequiresReplace()}, MarkdownDescription: "Endpoints of the Link", Required: true, - Validators: []validator.Set{setvalidator.SizeBetween(2, 2)}, - }, - "interface_ids": resourceSchema.SetAttribute{ - MarkdownDescription: "Interface IDs associated with the link", - Computed: true, - ElementType: types.StringType, - Validators: []validator.Set{setvalidator.SizeBetween(2, 2)}, + Validators: []validator.Map{mapvalidator.SizeBetween(2, 2)}, }, "tags": resourceSchema.SetAttribute{ MarkdownDescription: "Set of Tag labels", @@ -151,15 +150,17 @@ func (o *FreeformLink) Request(ctx context.Context, diags *diag.Diagnostics) *ap return nil } - var endpoints []freeformEndpoint + var endpoints map[string]freeformEndpoint diags.Append(o.Endpoints.ElementsAs(ctx, &endpoints, false)...) if diags.HasError() { return nil } var epArray [2]apstra.FreeformEndpoint - for i, endpoint := range endpoints { - epArray[i] = *endpoint.request() + var i int + for systemId, endpoint := range endpoints { + epArray[i] = *endpoint.request(systemId) + i++ } return &apstra.FreeformLinkRequest{ @@ -169,85 +170,23 @@ func (o *FreeformLink) Request(ctx context.Context, diags *diag.Diagnostics) *ap } } -func (o *FreeformLink) GetInterfaceIds(ctx context.Context, bp *apstra.FreeformClient, diags *diag.Diagnostics) { - var endpoints []freeformEndpoint - diags.Append(o.Endpoints.ElementsAs(ctx, &endpoints, false)...) - if diags.HasError() { - return - } - - query := new(apstra.PathQuery). - SetClient(bp.Client()). - SetBlueprintId(bp.Id()). - //node('system', id='v7A_2gUtP9vHq2DYhug') - Node([]apstra.QEEAttribute{ - apstra.NodeTypeSystem.QEEAttribute(), - {Key: "id", Value: apstra.QEStringVal(endpoints[0].SystemId.ValueString())}, - }). - //.out('hosted_interfaces') - Out([]apstra.QEEAttribute{apstra.RelationshipTypeHostedInterfaces.QEEAttribute()}). - //.node('interface', name='n_interface_0') - Node([]apstra.QEEAttribute{ - apstra.NodeTypeInterface.QEEAttribute(), - {Key: "name", Value: apstra.QEStringVal("n_interface_0")}, - }). - //.out('link') - Out([]apstra.QEEAttribute{apstra.RelationshipTypeLink.QEEAttribute()}). - //.node('link', id='P6oXpH9ho-_m41LLcSY') - Node([]apstra.QEEAttribute{ - apstra.NodeTypeLink.QEEAttribute(), - {Key: "id", Value: apstra.QEStringVal(o.Id.ValueString())}, - }). - //.in_('link') - In([]apstra.QEEAttribute{apstra.RelationshipTypeLink.QEEAttribute()}). - //.node('interface', name='n_interface_1') - Node([]apstra.QEEAttribute{ - apstra.NodeTypeInterface.QEEAttribute(), - {Key: "name", Value: apstra.QEStringVal("n_interface_1")}, - //.in_('hosted_interfaces') - }). - In([]apstra.QEEAttribute{apstra.RelationshipTypeHostedInterfaces.QEEAttribute()}). - //.node('system', id='uPeP0h4d-q8OAIomCpY') - Node([]apstra.QEEAttribute{ - apstra.NodeTypeSystem.QEEAttribute(), - {Key: "id", Value: apstra.QEStringVal(endpoints[1].SystemId.ValueString())}, - }) - - var response struct { - Items []struct { - Interface0 struct { - Id string `json:"id"` - } `json:"n_interface_0"` - Interface1 struct { - Id string `json:"id"` - } `json:"n_interface_1"` - } `json:"items"` - } - - err := query.Do(ctx, &response) - if err != nil { - diags.AddError("unable to perform query.do()", "query.do()") - return - } - if len(response.Items) != 1 { - diags.AddError("the Query response is incorrect", "the query is not 1 response ") - return +func (o *FreeformLink) LoadApiData(ctx context.Context, in *apstra.FreeformLinkData, diags *diag.Diagnostics) { + interfaceIds := make([]string, len(in.Endpoints)) + for i, endpoint := range in.Endpoints { + if endpoint.Interface.Id == nil { + diags.AddError( + fmt.Sprintf("api returned null interface id for system %s", endpoint.SystemId), + "link endpoints should always have an interface id.", + ) + return + } + interfaceIds[i] = endpoint.Interface.Id.String() } - interfaceIds := make([]attr.Value, 2) - interfaceIds[0] = types.StringValue(response.Items[0].Interface0.Id) - interfaceIds[1] = types.StringValue(response.Items[0].Interface1.Id) - - var d diag.Diagnostics - o.InterfaceIds, d = types.SetValue(types.StringType, interfaceIds) - diags.Append(d...) -} - -func (o *FreeformLink) LoadApiData(ctx context.Context, in *apstra.FreeformLinkData, diags *diag.Diagnostics) { o.Speed = types.StringValue(string(in.Speed)) o.Type = types.StringValue(in.Type.String()) o.Name = types.StringValue(in.Label) - o.Endpoints = newFreeformEndpointSet(ctx, in.Endpoints, diags) // safe to ignore diagnostic here - o.AggregateLinkId = types.StringValue(in.AggregateLinkId.String()) + o.Endpoints = newFreeformEndpointMap(ctx, in.Endpoints, diags) // safe to ignore diagnostic here + o.AggregateLinkId = types.StringPointerValue((*string)(in.AggregateLinkId)) o.Tags = utils.SetValueOrNull(ctx, types.StringType, in.Tags, diags) // safe to ignore diagnostic here } diff --git a/apstra/provider.go b/apstra/provider.go index 17405599..c136ec30 100644 --- a/apstra/provider.go +++ b/apstra/provider.go @@ -530,6 +530,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource func() datasource.DataSource { return &dataSourceDatacenterVirtualNetworks{} }, func() datasource.DataSource { return &dataSourceDeviceConfig{} }, func() datasource.DataSource { return &dataSourceFreeformConfigTemplate{} }, + func() datasource.DataSource { return &dataSourceFreeformLink{} }, func() datasource.DataSource { return &dataSourceFreeformPropertySet{} }, func() datasource.DataSource { return &dataSourceFreeformSystem{} }, func() datasource.DataSource { return &dataSourceIntegerPool{} }, @@ -584,6 +585,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { func() resource.Resource { return &resourceDatacenterVirtualNetwork{} }, func() resource.Resource { return &resourceDeviceAllocation{} }, func() resource.Resource { return &resourceFreeformConfigTemplate{} }, + func() resource.Resource { return &resourceFreeformLink{} }, func() resource.Resource { return &resourceFreeformPropertySet{} }, func() resource.Resource { return &resourceFreeformSystem{} }, func() resource.Resource { return &resourceIntegerPool{} }, diff --git a/apstra/resource_freeform_link.go b/apstra/resource_freeform_link.go index c8402654..17c1d49d 100644 --- a/apstra/resource_freeform_link.go +++ b/apstra/resource_freeform_link.go @@ -77,8 +77,15 @@ func (o *resourceFreeformLink) Create(ctx context.Context, req resource.CreateRe return } + // read the link to learn the speed, type & interface Ids + api, err := bp.GetLink(ctx, id) + if err != nil { + resp.Diagnostics.AddError("error reading just created Link", err.Error()) + return + } + plan.Id = types.StringValue(id.String()) - plan.GetInterfaceIds(ctx, bp, &resp.Diagnostics) + plan.LoadApiData(ctx, api.Data, &resp.Diagnostics) // set state resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) diff --git a/examples/data-sources/apstra_freeform_link/example.tf b/examples/data-sources/apstra_freeform_link/example.tf new file mode 100644 index 00000000..eb4f616f --- /dev/null +++ b/examples/data-sources/apstra_freeform_link/example.tf @@ -0,0 +1,59 @@ +# here we build a link block + +resource "apstra_freeform_link" "test" { + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + name = "link_a_b" + speed = "1g" + tags = ["a", "b"] + endpoints = [ + { + system_id = "-CEYpa9xZ5chndvu0OY" + interface_name = "ge-0/0/3" + transformation_id = 1 + }, + { + system_id = "ySBRdHvl2KZmWKLhkIk" + interface_name = "ge-0/0/3" + transformation_id = 1 + } + ] +} + +data "apstra_freeform_link" "test" { + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + id = apstra_freeform_link.test.id +} + +output "test_Link_out" { value = data.apstra_freeform_link.test } + + +//output +#test_Link_out = { +# "aggregate_link_id" = tostring(null) +# "blueprint_id" = "043c5787-66e8-41c7-8925-c7e52fbe6e32" +# "endpoints" = tomap({ +# "-CEYpa9xZ5chndvu0OY" = { +# "interface_id" = "c459DMed3P42wapAtUY" +# "interface_name" = "ge-0/0/3" +# "ipv4_address" = tostring(null) +# "ipv6_address" = tostring(null) +# "transformation_id" = 1 +# } +# "ySBRdHvl2KZmWKLhkIk" = { +# "interface_id" = "1wWgi25jmyZ5NBy45dA" +# "interface_name" = "ge-0/0/3" +# "ipv4_address" = tostring(null) +# "ipv6_address" = tostring(null) +# "transformation_id" = 1 +# } +# }) +# "id" = "SkY0hved7LajZY7WNzU" +# "name" = "link_a_b" +# "speed" = "10G" +# "tags" = toset([ +# "a", +# "b", +# ]) +# "type" = "ethernet" +#} + diff --git a/examples/resources/apstra_freeform_link/example.tf b/examples/resources/apstra_freeform_link/example.tf new file mode 100644 index 00000000..a2d678e4 --- /dev/null +++ b/examples/resources/apstra_freeform_link/example.tf @@ -0,0 +1,56 @@ +resource "apstra_freeform_link" "test" { + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + name = "link_a_b" + speed = "1g" + tags = ["a", "b"] + endpoints = [ + { + system_id = "-CEYpa9xZ5chndvu0OY" + interface_name = "ge-0/0/3" + transformation_id = 1 + }, + { + system_id = "ySBRdHvl2KZmWKLhkIk" + interface_name = "ge-0/0/3" + transformation_id = 1 + } + ] +} + +data "apstra_freeform_link" "test" { + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + id = apstra_freeform_link.test.id +} + +output "test_Link_out" { value = data.apstra_freeform_link.test } + + +//output +#test_Link_out = { +# "aggregate_link_id" = tostring(null) +# "blueprint_id" = "043c5787-66e8-41c7-8925-c7e52fbe6e32" +# "endpoints" = tomap({ +# "-CEYpa9xZ5chndvu0OY" = { +# "interface_id" = "c459DMed3P42wapAtUY" +# "interface_name" = "ge-0/0/3" +# "ipv4_address" = tostring(null) +# "ipv6_address" = tostring(null) +# "transformation_id" = 1 +# } +# "ySBRdHvl2KZmWKLhkIk" = { +# "interface_id" = "1wWgi25jmyZ5NBy45dA" +# "interface_name" = "ge-0/0/3" +# "ipv4_address" = tostring(null) +# "ipv6_address" = tostring(null) +# "transformation_id" = 1 +# } +# }) +# "id" = "SkY0hved7LajZY7WNzU" +# "name" = "link_a_b" +# "speed" = "10G" +# "tags" = toset([ +# "a", +# "b", +# ]) +# "type" = "ethernet" +#} \ No newline at end of file diff --git a/go.mod b/go.mod index bf2451f8..fb9228bc 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ toolchain go1.21.1 require ( github.com/IBM/netaddr v1.5.0 - github.com/Juniper/apstra-go-sdk v0.0.0-20240712195009-8b78f5ce34db + github.com/Juniper/apstra-go-sdk v0.0.0-20240716232834-1bab0a8d9e68 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 be5be22b..6c050012 100644 --- a/go.sum +++ b/go.sum @@ -108,8 +108,8 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= 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-20240712195009-8b78f5ce34db h1:1efAHckxbNyrnSaG7fwHHlDlMUk/0CLILGBm/DkDQ/8= -github.com/Juniper/apstra-go-sdk v0.0.0-20240712195009-8b78f5ce34db/go.mod h1:Xwj3X8v/jRZWv28o6vQAqD4lz2JmzaSYLZ2ch1SS89w= +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/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 cc5d16eb9a42c57c76b9e3bbe9823fe67e7b9b70 Mon Sep 17 00:00:00 2001 From: bwJuniper <98770420+bwJuniper@users.noreply.github.com> Date: Thu, 18 Jul 2024 23:54:31 +0200 Subject: [PATCH 14/18] finish link integration test --- apstra/blueprint/freeform_endpoint.go | 2 - apstra/export_test.go | 1 + ...resource_freeform_link_integration_test.go | 297 ++++++++++++++++++ apstra/test_helpers_test.go | 6 + apstra/test_utils/blueprint.go | 28 ++ docs/data-sources/freeform_link.md | 109 +++++++ docs/resources/freeform_link.md | 112 +++++++ 7 files changed, 553 insertions(+), 2 deletions(-) create mode 100644 apstra/resource_freeform_link_integration_test.go create mode 100644 docs/data-sources/freeform_link.md create mode 100644 docs/resources/freeform_link.md diff --git a/apstra/blueprint/freeform_endpoint.go b/apstra/blueprint/freeform_endpoint.go index af98388a..bd266a23 100644 --- a/apstra/blueprint/freeform_endpoint.go +++ b/apstra/blueprint/freeform_endpoint.go @@ -22,7 +22,6 @@ import ( ) type freeformEndpoint struct { - // SystemId types.String `tfsdk:"system_id"` InterfaceName types.String `tfsdk:"interface_name"` InterfaceId types.String `tfsdk:"interface_id"` TransformationId types.Int64 `tfsdk:"transformation_id"` @@ -32,7 +31,6 @@ type freeformEndpoint struct { func (o freeformEndpoint) attrTypes() map[string]attr.Type { return map[string]attr.Type{ - // "system_id": types.StringType, "interface_name": types.StringType, "interface_id": types.StringType, "transformation_id": types.Int64Type, diff --git a/apstra/export_test.go b/apstra/export_test.go index 9d0f68d0..d1f770eb 100644 --- a/apstra/export_test.go +++ b/apstra/export_test.go @@ -11,6 +11,7 @@ var ( ResourceDatacenterGenericSystem = resourceDatacenterGenericSystem{} ResourceDatacenterRoutingZone = resourceDatacenterRoutingZone{} ResourceFreeformConfigTemplate = resourceFreeformConfigTemplate{} + ResourceFreeformLink = resourceFreeformLink{} ResourceFreeformSystem = resourceFreeformSystem{} ResourceFreeformPropertySet = resourceFreeformPropertySet{} ResourceIpv4Pool = resourceIpv4Pool{} diff --git a/apstra/resource_freeform_link_integration_test.go b/apstra/resource_freeform_link_integration_test.go new file mode 100644 index 00000000..166f770c --- /dev/null +++ b/apstra/resource_freeform_link_integration_test.go @@ -0,0 +1,297 @@ +//go:build integration + +package tfapstra_test + +import ( + "context" + "fmt" + "github.com/Juniper/apstra-go-sdk/apstra" + "math/rand" + "net" + "strconv" + "testing" + + 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 ( + resourceFreeformLinkHcl = ` +resource %q %q { + blueprint_id = %q + name = %q + tags = %s + endpoints = { + %q = { + interface_name = %q + transformation_id = %d + ipv4_address = %s + ipv6_address = %s + }, + %q = { + interface_name = %q + transformation_id = %d + ipv4_address = %s + ipv6_address = %s + } + } +} +` +) + +type resourceFreeformLink struct { + blueprintId string + name string + endpoints []apstra.FreeformEndpoint + tags []string +} + +func (o resourceFreeformLink) render(rType, rName string) string { + return fmt.Sprintf(resourceFreeformLinkHcl, + rType, rName, + o.blueprintId, + o.name, + stringSetOrNull(o.tags), + o.endpoints[0].SystemId, + o.endpoints[0].Interface.Data.IfName, + o.endpoints[0].Interface.Data.TransformationId, + cidrOrNull(o.endpoints[0].Interface.Data.Ipv4Address), + cidrOrNull(o.endpoints[0].Interface.Data.Ipv6Address), + o.endpoints[1].SystemId, + o.endpoints[1].Interface.Data.IfName, + o.endpoints[1].Interface.Data.TransformationId, + cidrOrNull(o.endpoints[1].Interface.Data.Ipv4Address), + cidrOrNull(o.endpoints[1].Interface.Data.Ipv6Address), + ) +} + +func (o resourceFreeformLink) 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, "TestCheckResourceAttrSet", "speed") + result.append(t, "TestCheckResourceAttr", "type", apstra.FFLinkTypeEthernet.String()) + result.append(t, "TestCheckNoResourceAttr", "aggregate_link_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) + } + } + + result.append(t, "TestCheckResourceAttr", "endpoints.%", "2") + for _, endpoint := range o.endpoints { + result.append(t, "TestCheckResourceAttr", "endpoints."+endpoint.SystemId.String()+".interface_name", endpoint.Interface.Data.IfName) + result.append(t, "TestCheckResourceAttr", "endpoints."+endpoint.SystemId.String()+".transformation_id", strconv.Itoa(endpoint.Interface.Data.TransformationId)) + result.append(t, "TestCheckResourceAttrSet", "endpoints."+endpoint.SystemId.String()+".interface_id") + if endpoint.Interface.Data.Ipv4Address != nil { + result.append(t, "TestCheckResourceAttr", "endpoints."+endpoint.SystemId.String()+".ipv4_address", endpoint.Interface.Data.Ipv4Address.String()) + } else { + result.append(t, "TestCheckNoResourceAttr", "endpoints."+endpoint.SystemId.String()+".ipv4_address") + } + if endpoint.Interface.Data.Ipv6Address != nil { + result.append(t, "TestCheckResourceAttr", "endpoints."+endpoint.SystemId.String()+".ipv6_address", endpoint.Interface.Data.Ipv6Address.String()) + } else { + result.append(t, "TestCheckNoResourceAttr", "endpoints."+endpoint.SystemId.String()+".ipv6_address") + } + } + + return result +} + +func TestResourceFreeformLink(t *testing.T) { + ctx := context.Background() + client := testutils.GetTestClient(t, ctx) + apiVersion := version.Must(version.NewVersion(client.ApiVersion())) + // create a blueprint + bp, sysIds := testutils.FfBlueprintB(t, ctx, 2) + + type testStep struct { + config resourceFreeformLink + } + type testCase struct { + apiVersionConstraints version.Constraints + steps []testStep + } + + testCases := map[string]testCase{ + "start_minimal": { + steps: []testStep{ + { + config: resourceFreeformLink{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + endpoints: []apstra.FreeformEndpoint{ + { + SystemId: sysIds[0], + Interface: apstra.FreeformInterface{ + Data: &apstra.FreeformInterfaceData{ + IfName: "ge-0/0/0", + TransformationId: 1, + Ipv4Address: nil, + Ipv6Address: nil, + Tags: nil, + }, + }, + }, + { + SystemId: sysIds[1], + Interface: apstra.FreeformInterface{ + Data: &apstra.FreeformInterfaceData{ + IfName: "ge-0/0/0", + TransformationId: 1, + Ipv4Address: nil, + Ipv6Address: nil, + Tags: nil, + }, + }, + }, + }, + }, + }, + { + config: resourceFreeformLink{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + endpoints: []apstra.FreeformEndpoint{ + { + SystemId: sysIds[0], + Interface: apstra.FreeformInterface{ + Data: &apstra.FreeformInterfaceData{ + IfName: "ge-0/0/0", + TransformationId: 1, + Ipv4Address: &net.IPNet{IP: net.ParseIP("192.168.10.1"), Mask: net.CIDRMask(30, 32)}, + Ipv6Address: &net.IPNet{IP: net.ParseIP("2001:db8::3"), Mask: net.CIDRMask(64, 128)}, + Tags: randomStrings(rand.Intn(10)+2, 6), + }, + }, + }, + { + SystemId: sysIds[1], + Interface: apstra.FreeformInterface{ + Data: &apstra.FreeformInterfaceData{ + IfName: "ge-0/0/0", + TransformationId: 1, + Ipv4Address: &net.IPNet{IP: net.ParseIP("192.168.10.2"), Mask: net.CIDRMask(30, 32)}, + Ipv6Address: &net.IPNet{IP: net.ParseIP("2001:db8::3"), Mask: net.CIDRMask(64, 128)}, + Tags: randomStrings(rand.Intn(10)+2, 6), + }, + }, + }, + }, + }, + }, + }, + }, + "start_with_tags": { + steps: []testStep{ + { + config: resourceFreeformLink{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + tags: randomStrings(rand.Intn(10)+2, 6), + endpoints: []apstra.FreeformEndpoint{ + { + SystemId: sysIds[0], + Interface: apstra.FreeformInterface{ + Data: &apstra.FreeformInterfaceData{ + IfName: "ge-0/0/5", + TransformationId: 1, + Ipv4Address: &net.IPNet{IP: net.ParseIP("10.1.1.1"), Mask: net.CIDRMask(30, 32)}, + Ipv6Address: &net.IPNet{IP: net.ParseIP("2001:db8::1"), Mask: net.CIDRMask(64, 128)}, + Tags: randomStrings(rand.Intn(10)+2, 6), + }, + }, + }, + { + SystemId: sysIds[1], + Interface: apstra.FreeformInterface{ + Data: &apstra.FreeformInterfaceData{ + IfName: "ge-0/0/5", + TransformationId: 1, + Ipv4Address: &net.IPNet{IP: net.ParseIP("10.1.1.2"), Mask: net.CIDRMask(30, 32)}, + Ipv6Address: &net.IPNet{IP: net.ParseIP("2001:db8::2"), Mask: net.CIDRMask(64, 128)}, + Tags: randomStrings(rand.Intn(10)+2, 6), + }, + }, + }, + }, + }, + }, + { + config: resourceFreeformLink{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + endpoints: []apstra.FreeformEndpoint{ + { + SystemId: sysIds[0], + Interface: apstra.FreeformInterface{ + Data: &apstra.FreeformInterfaceData{ + IfName: "ge-0/0/5", + TransformationId: 1, + Ipv4Address: &net.IPNet{IP: net.ParseIP("10.1.1.1"), Mask: net.CIDRMask(30, 32)}, + Ipv6Address: &net.IPNet{IP: net.ParseIP("2001:db8::1"), Mask: net.CIDRMask(64, 128)}, + Tags: randomStrings(rand.Intn(10)+2, 6), + }, + }, + }, + { + SystemId: sysIds[1], + Interface: apstra.FreeformInterface{ + Data: &apstra.FreeformInterfaceData{ + IfName: "ge-0/0/5", + TransformationId: 1, + Ipv4Address: &net.IPNet{IP: net.ParseIP("10.1.1.2"), Mask: net.CIDRMask(30, 32)}, + Ipv6Address: &net.IPNet{IP: net.ParseIP("2001:db8::2"), Mask: net.CIDRMask(64, 128)}, + Tags: randomStrings(rand.Intn(10)+2, 6), + }, + }, + }, + }, + }, + }, + }, + }, + } + + resourceType := tfapstra.ResourceName(ctx, &tfapstra.ResourceFreeformLink) + + 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/apstra/test_helpers_test.go b/apstra/test_helpers_test.go index 8027f587..b0dad806 100644 --- a/apstra/test_helpers_test.go +++ b/apstra/test_helpers_test.go @@ -419,6 +419,12 @@ func (o *testChecks) append(t testing.TB, testCheckFuncName string, testCheckFun } o.checks = append(o.checks, resource.TestCheckResourceAttrSet(o.path, testCheckFuncArgs[0])) o.logLines.appendf("TestCheckResourceAttrSet(%s, %q)", o.path, testCheckFuncArgs[0]) + case "TestCheckNoResourceAttr": + if len(testCheckFuncArgs) != 1 { + t.Fatalf("%s requires 1 args, got %d", testCheckFuncName, len(testCheckFuncArgs)) + } + o.checks = append(o.checks, resource.TestCheckNoResourceAttr(o.path, testCheckFuncArgs[0])) + o.logLines.appendf("TestCheckNoResourceAttr(%s, %q)", o.path, testCheckFuncArgs[0]) case "TestCheckResourceAttr": if len(testCheckFuncArgs) != 2 { t.Fatalf("%s requires 2 args, got %d", testCheckFuncName, len(testCheckFuncArgs)) diff --git a/apstra/test_utils/blueprint.go b/apstra/test_utils/blueprint.go index 99f03d35..18e0d0e7 100644 --- a/apstra/test_utils/blueprint.go +++ b/apstra/test_utils/blueprint.go @@ -243,3 +243,31 @@ func FfBlueprintA(t testing.TB, ctx context.Context) *apstra.FreeformClient { return bpClient } + +func FfBlueprintB(t testing.TB, ctx context.Context, systemCount int) (*apstra.FreeformClient, []apstra.ObjectId) { + t.Helper() + + client := GetTestClient(t, ctx) + + id, err := client.CreateFreeformBlueprint(ctx, acctest.RandString(6)) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, client.DeleteBlueprint(ctx, id)) }) + + c, err := client.NewFreeformClient(ctx, id) + require.NoError(t, err) + + dpId, err := c.ImportDeviceProfile(ctx, "Juniper_vEX") + require.NoError(t, err) + + systemIds := make([]apstra.ObjectId, systemCount) + for i := range systemIds { + systemIds[i], err = c.CreateSystem(ctx, &apstra.FreeformSystemData{ + Type: apstra.SystemTypeInternal, + Label: acctest.RandString(6), + DeviceProfileId: dpId, + }) + require.NoError(t, err) + } + + return c, systemIds +} diff --git a/docs/data-sources/freeform_link.md b/docs/data-sources/freeform_link.md new file mode 100644 index 00000000..02fa941c --- /dev/null +++ b/docs/data-sources/freeform_link.md @@ -0,0 +1,109 @@ +--- +page_title: "apstra_freeform_link Data Source - terraform-provider-apstra" +subcategory: "Reference Design: Freeform" +description: |- + This data source provides details of a specific Freeform Link. + At least one optional attribute is required. +--- + +# apstra_freeform_link (Data Source) + +This data source provides details of a specific Freeform Link. + +At least one optional attribute is required. + + +## Example Usage + +```terraform +# here we build a link block + +resource "apstra_freeform_link" "test" { + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + name = "link_a_b" + speed = "1g" + tags = ["a", "b"] + endpoints = [ + { + system_id = "-CEYpa9xZ5chndvu0OY" + interface_name = "ge-0/0/3" + transformation_id = 1 + }, + { + system_id = "ySBRdHvl2KZmWKLhkIk" + interface_name = "ge-0/0/3" + transformation_id = 1 + } + ] +} + +data "apstra_freeform_link" "test" { + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + id = apstra_freeform_link.test.id +} + +output "test_Link_out" { value = data.apstra_freeform_link.test } + + +//output +#test_Link_out = { +# "aggregate_link_id" = tostring(null) +# "blueprint_id" = "043c5787-66e8-41c7-8925-c7e52fbe6e32" +# "endpoints" = tomap({ +# "-CEYpa9xZ5chndvu0OY" = { +# "interface_id" = "c459DMed3P42wapAtUY" +# "interface_name" = "ge-0/0/3" +# "ipv4_address" = tostring(null) +# "ipv6_address" = tostring(null) +# "transformation_id" = 1 +# } +# "ySBRdHvl2KZmWKLhkIk" = { +# "interface_id" = "1wWgi25jmyZ5NBy45dA" +# "interface_name" = "ge-0/0/3" +# "ipv4_address" = tostring(null) +# "ipv6_address" = tostring(null) +# "transformation_id" = 1 +# } +# }) +# "id" = "SkY0hved7LajZY7WNzU" +# "name" = "link_a_b" +# "speed" = "10G" +# "tags" = toset([ +# "a", +# "b", +# ]) +# "type" = "ethernet" +#} +``` + + +## Schema + +### Required + +- `blueprint_id` (String) Apstra Blueprint ID. Used to identify the Blueprint where the Link lives. + +### Optional + +- `id` (String) Populate this field to look up the Freeform Link by ID. Required when `name` is omitted. +- `name` (String) Populate this field to look up the Link by Name. Required when `id` is omitted. + +### Read-Only + +- `aggregate_link_id` (String) ID of aggregate link node that the current link belongs to +- `endpoints` (Attributes Map) Endpoints assigned to the Link (see [below for nested schema](#nestedatt--endpoints)) +- `speed` (String) Speed of the Link 200G | 5G | 1G | 100G | 150g | 40g | 2500M | 25G | 25g | 10G | 50G | 800G | 10M | 100m | 2500m | 50g | 400g | 400G | 200g | 5g | 800g | 100M | 10g | 150G | 10m | 100g | 1g | 40G +- `tags` (Set of String) Set of unique case-insensitive tag labels +- `type` (String) aggregate_link | ethernet +Link Type. An 'ethernet' link is a normal front-panel interface. An 'aggregate_link' is a bonded interface which is typically used for LACP or Static LAGs. Note that the lag_mode parameter is a property of the interface and not the link, since interfaces may have different lag modes on opposite sides of the link - eg lacp_passive <-> lacp_active + + +### Nested Schema for `endpoints` + +Read-Only: + +- `interface_id` (String) Graph node ID of the attached interface for this side of the link endpoint +- `interface_name` (String) the interface name +- `ipv4_address` (String) Ipv4 address of the interface +- `ipv6_address` (String) Ipv6 address of the interface +- `transformation_id` (Number) ID of the transformation in the device profile diff --git a/docs/resources/freeform_link.md b/docs/resources/freeform_link.md new file mode 100644 index 00000000..c422089c --- /dev/null +++ b/docs/resources/freeform_link.md @@ -0,0 +1,112 @@ +--- +page_title: "apstra_freeform_link Resource - terraform-provider-apstra" +subcategory: "Reference Design: Freeform" +description: |- + This resource creates a Link in a Freeform Blueprint. +--- + +# apstra_freeform_link (Resource) + +This resource creates a Link in a Freeform Blueprint. + + +## Example Usage + +```terraform +resource "apstra_freeform_link" "test" { + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + name = "link_a_b" + speed = "1g" + tags = ["a", "b"] + endpoints = [ + { + system_id = "-CEYpa9xZ5chndvu0OY" + interface_name = "ge-0/0/3" + transformation_id = 1 + }, + { + system_id = "ySBRdHvl2KZmWKLhkIk" + interface_name = "ge-0/0/3" + transformation_id = 1 + } + ] +} + +data "apstra_freeform_link" "test" { + blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" + id = apstra_freeform_link.test.id +} + +output "test_Link_out" { value = data.apstra_freeform_link.test } + + +//output +#test_Link_out = { +# "aggregate_link_id" = tostring(null) +# "blueprint_id" = "043c5787-66e8-41c7-8925-c7e52fbe6e32" +# "endpoints" = tomap({ +# "-CEYpa9xZ5chndvu0OY" = { +# "interface_id" = "c459DMed3P42wapAtUY" +# "interface_name" = "ge-0/0/3" +# "ipv4_address" = tostring(null) +# "ipv6_address" = tostring(null) +# "transformation_id" = 1 +# } +# "ySBRdHvl2KZmWKLhkIk" = { +# "interface_id" = "1wWgi25jmyZ5NBy45dA" +# "interface_name" = "ge-0/0/3" +# "ipv4_address" = tostring(null) +# "ipv6_address" = tostring(null) +# "transformation_id" = 1 +# } +# }) +# "id" = "SkY0hved7LajZY7WNzU" +# "name" = "link_a_b" +# "speed" = "10G" +# "tags" = toset([ +# "a", +# "b", +# ]) +# "type" = "ethernet" +#} +``` + + +## Schema + +### Required + +- `blueprint_id` (String) Apstra Blueprint ID. +- `endpoints` (Attributes Map) Endpoints of the Link (see [below for nested schema](#nestedatt--endpoints)) +- `name` (String) Freeform Link name as shown in the Web UI. + +### Optional + +- `aggregate_link_id` (String) ID of aggregate link node that the current link belongs to +- `tags` (Set of String) Set of Tag labels + +### Read-Only + +- `id` (String) ID of the Freeform Link. +- `speed` (String) Speed of the Freeform Link. +- `type` (String) Deploy mode of the Link + + +### Nested Schema for `endpoints` + +Required: + +- `interface_name` (String) the interface name +- `transformation_id` (Number) ID of the transformation in the device profile + +Optional: + +- `ipv4_address` (String) Ipv4 address of the interface +- `ipv6_address` (String) Ipv6 address of the interface + +Read-Only: + +- `interface_id` (String) Graph node ID of the attached interface for this side of the link endpoint. + + + From 0a29e9f1938348141ddd28cb58d282ae3217ef89 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Thu, 18 Jul 2024 18:12:31 -0400 Subject: [PATCH 15/18] fix merge conflict havoc --- apstra/export_test.go | 4 ++-- .../resource_datacenter_connectivity_template_assignment.go | 2 +- .../resource_datacenter_connectivity_template_assignments.go | 2 +- .../resource_datacenter_connectivity_templates_assignment.go | 2 +- apstra/resource_datacenter_ip_link_addressing.go | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apstra/export_test.go b/apstra/export_test.go index 0ca3fa80..658d31c6 100644 --- a/apstra/export_test.go +++ b/apstra/export_test.go @@ -8,7 +8,7 @@ import ( var ( ResourceAgentProfile = resourceAgentProfile{} - ResourceDatacenterGenericSystem. = resourceDatacenterGenericSystem{} + ResourceDatacenterGenericSystem = resourceDatacenterGenericSystem{} ResourceDatacenterRoutingZone = resourceDatacenterRoutingZone{} ResourceFreeformConfigTemplate = resourceFreeformConfigTemplate{} ResourceFreeformLink = resourceFreeformLink{} @@ -17,7 +17,7 @@ var ( ResourceIpv4Pool = resourceIpv4Pool{} ResourceTemplatePodBased = resourceTemplatePodBased{} ResourceTemplateCollapsed = resourceTemplateCollapsed{} - ResourceDatacenterIpLinkAddressing = resourceDatacenterIpLinkAddressing{} + ResourceDatacenterIpLinkAddressing = resourceDatacenterIpLinkAddressing{} ) func ResourceName(ctx context.Context, r resource.Resource) string { diff --git a/apstra/resource_datacenter_connectivity_template_assignment.go b/apstra/resource_datacenter_connectivity_template_assignment.go index 14baa54e..606d9ab0 100644 --- a/apstra/resource_datacenter_connectivity_template_assignment.go +++ b/apstra/resource_datacenter_connectivity_template_assignment.go @@ -14,7 +14,7 @@ import ( var ( _ resource.ResourceWithConfigure = &resourceDatacenterConnectivityTemplateAssignment{} - _ resourceWithSetBpClientFunc = &resourceDatacenterConnectivityTemplateAssignment{} + _ resourceWithSetDcBpClientFunc = &resourceDatacenterConnectivityTemplateAssignment{} _ resourceWithSetBpLockFunc = &resourceDatacenterConnectivityTemplateAssignment{} ) diff --git a/apstra/resource_datacenter_connectivity_template_assignments.go b/apstra/resource_datacenter_connectivity_template_assignments.go index af4e2968..95a2c8b0 100644 --- a/apstra/resource_datacenter_connectivity_template_assignments.go +++ b/apstra/resource_datacenter_connectivity_template_assignments.go @@ -16,7 +16,7 @@ import ( var ( _ resource.ResourceWithConfigure = &resourceDatacenterConnectivityTemplateAssignments{} - _ resourceWithSetBpClientFunc = &resourceDatacenterConnectivityTemplateAssignments{} + _ resourceWithSetDcBpClientFunc = &resourceDatacenterConnectivityTemplateAssignments{} _ resourceWithSetBpLockFunc = &resourceDatacenterConnectivityTemplateAssignments{} ) diff --git a/apstra/resource_datacenter_connectivity_templates_assignment.go b/apstra/resource_datacenter_connectivity_templates_assignment.go index 1d422e85..18d0b60c 100644 --- a/apstra/resource_datacenter_connectivity_templates_assignment.go +++ b/apstra/resource_datacenter_connectivity_templates_assignment.go @@ -14,7 +14,7 @@ import ( var ( _ resource.ResourceWithConfigure = &resourceDatacenterConnectivityTemplatesAssignment{} - _ resourceWithSetBpClientFunc = &resourceDatacenterConnectivityTemplatesAssignment{} + _ resourceWithSetDcBpClientFunc = &resourceDatacenterConnectivityTemplatesAssignment{} _ resourceWithSetBpLockFunc = &resourceDatacenterConnectivityTemplatesAssignment{} ) diff --git a/apstra/resource_datacenter_ip_link_addressing.go b/apstra/resource_datacenter_ip_link_addressing.go index 1365137c..9cf95426 100644 --- a/apstra/resource_datacenter_ip_link_addressing.go +++ b/apstra/resource_datacenter_ip_link_addressing.go @@ -21,7 +21,7 @@ import ( var ( _ resource.ResourceWithValidateConfig = &resourceDatacenterIpLinkAddressing{} - _ resourceWithSetBpClientFunc = &resourceDatacenterIpLinkAddressing{} + _ resourceWithSetDcBpClientFunc = &resourceDatacenterIpLinkAddressing{} _ resourceWithSetBpLockFunc = &resourceDatacenterIpLinkAddressing{} ) From 7c3ebdd7ae1273caf38a13395949fcecb7724b1e Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Thu, 18 Jul 2024 18:29:03 -0400 Subject: [PATCH 16/18] add more test steps --- ...resource_freeform_link_integration_test.go | 79 +++++++++++++++++-- 1 file changed, 72 insertions(+), 7 deletions(-) diff --git a/apstra/resource_freeform_link_integration_test.go b/apstra/resource_freeform_link_integration_test.go index 166f770c..858b2311 100644 --- a/apstra/resource_freeform_link_integration_test.go +++ b/apstra/resource_freeform_link_integration_test.go @@ -160,6 +160,7 @@ func TestResourceFreeformLink(t *testing.T) { config: resourceFreeformLink{ blueprintId: bp.Id().String(), name: acctest.RandString(6), + tags: randomStrings(rand.Intn(10)+2, 6), endpoints: []apstra.FreeformEndpoint{ { SystemId: sysIds[0], @@ -180,7 +181,7 @@ func TestResourceFreeformLink(t *testing.T) { IfName: "ge-0/0/0", TransformationId: 1, Ipv4Address: &net.IPNet{IP: net.ParseIP("192.168.10.2"), Mask: net.CIDRMask(30, 32)}, - Ipv6Address: &net.IPNet{IP: net.ParseIP("2001:db8::3"), Mask: net.CIDRMask(64, 128)}, + Ipv6Address: &net.IPNet{IP: net.ParseIP("2001:db8::4"), Mask: net.CIDRMask(64, 128)}, Tags: randomStrings(rand.Intn(10)+2, 6), }, }, @@ -188,9 +189,40 @@ func TestResourceFreeformLink(t *testing.T) { }, }, }, - }, + { + config: resourceFreeformLink{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + endpoints: []apstra.FreeformEndpoint{ + { + SystemId: sysIds[0], + Interface: apstra.FreeformInterface{ + Data: &apstra.FreeformInterfaceData{ + IfName: "ge-0/0/0", + TransformationId: 1, + Ipv4Address: nil, + Ipv6Address: nil, + Tags: nil, + }, + }, + }, + { + SystemId: sysIds[1], + Interface: apstra.FreeformInterface{ + Data: &apstra.FreeformInterfaceData{ + IfName: "ge-0/0/0", + TransformationId: 1, + Ipv4Address: nil, + Ipv6Address: nil, + Tags: nil, + }, + }, + }, + }, + }, + }}, }, - "start_with_tags": { + "start_maximal": { steps: []testStep{ { config: resourceFreeformLink{ @@ -236,8 +268,41 @@ func TestResourceFreeformLink(t *testing.T) { Data: &apstra.FreeformInterfaceData{ IfName: "ge-0/0/5", TransformationId: 1, - Ipv4Address: &net.IPNet{IP: net.ParseIP("10.1.1.1"), Mask: net.CIDRMask(30, 32)}, - Ipv6Address: &net.IPNet{IP: net.ParseIP("2001:db8::1"), Mask: net.CIDRMask(64, 128)}, + Ipv4Address: nil, + Ipv6Address: nil, + Tags: nil, + }, + }, + }, + { + SystemId: sysIds[1], + Interface: apstra.FreeformInterface{ + Data: &apstra.FreeformInterfaceData{ + IfName: "ge-0/0/5", + TransformationId: 1, + Ipv4Address: nil, + Ipv6Address: nil, + Tags: nil, + }, + }, + }, + }, + }, + }, + { + config: resourceFreeformLink{ + blueprintId: bp.Id().String(), + name: acctest.RandString(6), + tags: randomStrings(rand.Intn(10)+2, 6), + endpoints: []apstra.FreeformEndpoint{ + { + SystemId: sysIds[0], + Interface: apstra.FreeformInterface{ + Data: &apstra.FreeformInterfaceData{ + IfName: "ge-0/0/5", + TransformationId: 1, + Ipv4Address: &net.IPNet{IP: net.ParseIP("10.2.1.1"), Mask: net.CIDRMask(30, 32)}, + Ipv6Address: &net.IPNet{IP: net.ParseIP("2001:db8::3"), Mask: net.CIDRMask(64, 128)}, Tags: randomStrings(rand.Intn(10)+2, 6), }, }, @@ -248,8 +313,8 @@ func TestResourceFreeformLink(t *testing.T) { Data: &apstra.FreeformInterfaceData{ IfName: "ge-0/0/5", TransformationId: 1, - Ipv4Address: &net.IPNet{IP: net.ParseIP("10.1.1.2"), Mask: net.CIDRMask(30, 32)}, - Ipv6Address: &net.IPNet{IP: net.ParseIP("2001:db8::2"), Mask: net.CIDRMask(64, 128)}, + Ipv4Address: &net.IPNet{IP: net.ParseIP("10.2.1.2"), Mask: net.CIDRMask(30, 32)}, + Ipv6Address: &net.IPNet{IP: net.ParseIP("2001:db8::4"), Mask: net.CIDRMask(64, 128)}, Tags: randomStrings(rand.Intn(10)+2, 6), }, }, From ebad8fa5337d174f63be696c86f017f9994de5a1 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Thu, 18 Jul 2024 18:54:21 -0400 Subject: [PATCH 17/18] add interface tags, remove requiresreplace from if_name and transform_id --- apstra/blueprint/freeform_endpoint.go | 54 ++++++++++++------- apstra/blueprint/freeform_link.go | 9 ++-- ...resource_freeform_link_integration_test.go | 47 ++++++++-------- 3 files changed, 65 insertions(+), 45 deletions(-) diff --git a/apstra/blueprint/freeform_endpoint.go b/apstra/blueprint/freeform_endpoint.go index bd266a23..15d39198 100644 --- a/apstra/blueprint/freeform_endpoint.go +++ b/apstra/blueprint/freeform_endpoint.go @@ -3,22 +3,21 @@ package blueprint import ( "context" "fmt" + "net" + "strings" + "github.com/Juniper/apstra-go-sdk/apstra" "github.com/Juniper/terraform-provider-apstra/apstra/utils" "github.com/hashicorp/terraform-plugin-framework-nettypes/cidrtypes" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "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" resourceSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" - "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" - "net" - "strings" ) type freeformEndpoint struct { @@ -27,6 +26,7 @@ type freeformEndpoint struct { TransformationId types.Int64 `tfsdk:"transformation_id"` Ipv4Address cidrtypes.IPv4Prefix `tfsdk:"ipv4_address"` Ipv6Address cidrtypes.IPv6Prefix `tfsdk:"ipv6_address"` + Tags types.Set `tfsdk:"tags"` } func (o freeformEndpoint) attrTypes() map[string]attr.Type { @@ -36,6 +36,7 @@ func (o freeformEndpoint) attrTypes() map[string]attr.Type { "transformation_id": types.Int64Type, "ipv4_address": cidrtypes.IPv4PrefixType{}, "ipv6_address": cidrtypes.IPv6PrefixType{}, + "tags": types.SetType{ElemType: types.StringType}, } } @@ -43,26 +44,31 @@ func (o freeformEndpoint) DatasourceAttributes() map[string]dataSourceSchema.Att return map[string]dataSourceSchema.Attribute{ "interface_name": dataSourceSchema.StringAttribute{ Computed: true, - MarkdownDescription: "the interface name", + MarkdownDescription: "The interface name, as found in the associated Device Profile, e.g. `xe-0/0/0`", }, "interface_id": dataSourceSchema.StringAttribute{ Computed: true, - MarkdownDescription: "Graph node ID of the attached interface for this side of the link endpoint ", + MarkdownDescription: "Graph node ID of the associated interface", }, "transformation_id": dataSourceSchema.Int64Attribute{ Computed: true, - MarkdownDescription: "ID of the transformation in the device profile", + MarkdownDescription: "ID # of the transformation in the Device Profile", }, "ipv4_address": dataSourceSchema.StringAttribute{ Computed: true, - MarkdownDescription: "Ipv4 address of the interface", + MarkdownDescription: "Ipv4 address of the interface in CIDR notation", CustomType: cidrtypes.IPv4PrefixType{}, }, "ipv6_address": dataSourceSchema.StringAttribute{ Computed: true, - MarkdownDescription: "Ipv6 address of the interface", + MarkdownDescription: "Ipv6 address of the interface in CIDR notation", CustomType: cidrtypes.IPv6PrefixType{}, }, + "tags": dataSourceSchema.SetAttribute{ + MarkdownDescription: "Set of Tags applied to the interface", + Computed: true, + ElementType: types.StringType, + }, } } @@ -70,34 +76,38 @@ func (o freeformEndpoint) ResourceAttributes() map[string]resourceSchema.Attribu return map[string]resourceSchema.Attribute{ "interface_name": resourceSchema.StringAttribute{ Required: true, - MarkdownDescription: "the interface name", - PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + MarkdownDescription: "The interface name, as found in the associated Device Profile, e.g. `xe-0/0/0`", Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, "interface_id": resourceSchema.StringAttribute{ Computed: true, - MarkdownDescription: "Graph node ID of the attached interface for this side of the link endpoint.", + MarkdownDescription: "Graph node ID of the associated interface", }, "transformation_id": resourceSchema.Int64Attribute{ Required: true, - MarkdownDescription: "ID of the transformation in the device profile", - PlanModifiers: []planmodifier.Int64{int64planmodifier.RequiresReplace()}, + MarkdownDescription: "ID # of the transformation in the Device Profile", Validators: []validator.Int64{int64validator.AtLeast(1)}, }, "ipv4_address": resourceSchema.StringAttribute{ Optional: true, - MarkdownDescription: "Ipv4 address of the interface", + MarkdownDescription: "Ipv4 address of the interface in CIDR notation", CustomType: cidrtypes.IPv4PrefixType{}, }, "ipv6_address": resourceSchema.StringAttribute{ Optional: true, - MarkdownDescription: "Ipv6 address of the interface", + MarkdownDescription: "Ipv6 address of the interface in CIDR notation", CustomType: cidrtypes.IPv6PrefixType{}, }, + "tags": resourceSchema.SetAttribute{ + MarkdownDescription: "Set of Tags applied to the interface", + Optional: true, + ElementType: types.StringType, + Validators: []validator.Set{setvalidator.SizeAtLeast(1)}, + }, } } -func (o *freeformEndpoint) request(systemId string) *apstra.FreeformEndpoint { +func (o *freeformEndpoint) request(ctx context.Context, systemId string, diags *diag.Diagnostics) *apstra.FreeformEndpoint { var ipNet4, ipNet6 *net.IPNet if !o.Ipv4Address.IsNull() { var ip4 net.IP @@ -110,6 +120,9 @@ func (o *freeformEndpoint) request(systemId string) *apstra.FreeformEndpoint { ipNet6.IP = ip6 } + var tags []string + diags.Append(o.Tags.ElementsAs(ctx, &tags, false)...) + return &apstra.FreeformEndpoint{ SystemId: apstra.ObjectId(systemId), Interface: apstra.FreeformInterface{ @@ -118,12 +131,13 @@ func (o *freeformEndpoint) request(systemId string) *apstra.FreeformEndpoint { TransformationId: int(o.TransformationId.ValueInt64()), Ipv4Address: ipNet4, Ipv6Address: ipNet6, + Tags: tags, }, }, } } -func (o *freeformEndpoint) loadApiData(_ context.Context, in apstra.FreeformEndpoint, diags *diag.Diagnostics) { +func (o *freeformEndpoint) loadApiData(ctx context.Context, in apstra.FreeformEndpoint, diags *diag.Diagnostics) { if in.Interface.Id == nil { diags.AddError( fmt.Sprintf("api returned nil interface Id for system %s", in.SystemId), @@ -134,7 +148,6 @@ func (o *freeformEndpoint) loadApiData(_ context.Context, in apstra.FreeformEndp o.InterfaceName = types.StringValue(in.Interface.Data.IfName) o.InterfaceId = types.StringValue(in.Interface.Id.String()) - //o.SystemId = types.StringValue(in.SystemId.String()) o.TransformationId = types.Int64Value(int64(in.Interface.Data.TransformationId)) o.Ipv4Address = cidrtypes.NewIPv4PrefixValue(in.Interface.Data.Ipv4Address.String()) if strings.Contains(o.Ipv4Address.ValueString(), "nil") { @@ -144,6 +157,7 @@ func (o *freeformEndpoint) loadApiData(_ context.Context, in apstra.FreeformEndp if strings.Contains(o.Ipv6Address.ValueString(), "nil") { o.Ipv6Address = cidrtypes.NewIPv6PrefixNull() } + o.Tags = utils.SetValueOrNull(ctx, types.StringType, in.Interface.Data.Tags, diags) } func newFreeformEndpointMap(ctx context.Context, in [2]apstra.FreeformEndpoint, diags *diag.Diagnostics) types.Map { diff --git a/apstra/blueprint/freeform_link.go b/apstra/blueprint/freeform_link.go index ab83b1b0..85e563e1 100644 --- a/apstra/blueprint/freeform_link.go +++ b/apstra/blueprint/freeform_link.go @@ -3,18 +3,18 @@ package blueprint import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" "regexp" "github.com/Juniper/apstra-go-sdk/apstra" "github.com/Juniper/terraform-provider-apstra/apstra/utils" + "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" "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" "github.com/hashicorp/terraform-plugin-framework/path" resourceSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" "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" @@ -110,7 +110,8 @@ func (o FreeformLink) ResourceAttributes() map[string]resourceSchema.Attribute { MarkdownDescription: "Freeform Link name as shown in the Web UI.", Required: true, Validators: []validator.String{ - stringvalidator.RegexMatches(regexp.MustCompile("^[a-zA-Z0-9.-_]+$"), "name may consist only of the following characters : a-zA-Z0-9.-_")}, + stringvalidator.RegexMatches(regexp.MustCompile("^[a-zA-Z0-9.-_]+$"), "name may consist only of the following characters : a-zA-Z0-9.-_"), + }, }, "speed": resourceSchema.StringAttribute{ MarkdownDescription: "Speed of the Freeform Link.", @@ -159,7 +160,7 @@ func (o *FreeformLink) Request(ctx context.Context, diags *diag.Diagnostics) *ap var epArray [2]apstra.FreeformEndpoint var i int for systemId, endpoint := range endpoints { - epArray[i] = *endpoint.request(systemId) + epArray[i] = *endpoint.request(ctx, systemId, diags) i++ } diff --git a/apstra/resource_freeform_link_integration_test.go b/apstra/resource_freeform_link_integration_test.go index 858b2311..4b854531 100644 --- a/apstra/resource_freeform_link_integration_test.go +++ b/apstra/resource_freeform_link_integration_test.go @@ -5,12 +5,12 @@ package tfapstra_test import ( "context" "fmt" - "github.com/Juniper/apstra-go-sdk/apstra" "math/rand" "net" "strconv" "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" @@ -22,20 +22,20 @@ const ( resourceFreeformLinkHcl = ` resource %q %q { blueprint_id = %q - name = %q - tags = %s - endpoints = { + name = %q + tags = %s + endpoints = { %q = { interface_name = %q transformation_id = %d - ipv4_address = %s - ipv6_address = %s + ipv4_address = %s + ipv6_address = %s }, %q = { interface_name = %q transformation_id = %d - ipv4_address = %s - ipv6_address = %s + ipv4_address = %s + ipv6_address = %s } } } @@ -91,11 +91,13 @@ func (o resourceFreeformLink) testChecks(t testing.TB, rType, rName string) test result.append(t, "TestCheckResourceAttr", "endpoints."+endpoint.SystemId.String()+".interface_name", endpoint.Interface.Data.IfName) result.append(t, "TestCheckResourceAttr", "endpoints."+endpoint.SystemId.String()+".transformation_id", strconv.Itoa(endpoint.Interface.Data.TransformationId)) result.append(t, "TestCheckResourceAttrSet", "endpoints."+endpoint.SystemId.String()+".interface_id") + if endpoint.Interface.Data.Ipv4Address != nil { result.append(t, "TestCheckResourceAttr", "endpoints."+endpoint.SystemId.String()+".ipv4_address", endpoint.Interface.Data.Ipv4Address.String()) } else { result.append(t, "TestCheckNoResourceAttr", "endpoints."+endpoint.SystemId.String()+".ipv4_address") } + if endpoint.Interface.Data.Ipv6Address != nil { result.append(t, "TestCheckResourceAttr", "endpoints."+endpoint.SystemId.String()+".ipv6_address", endpoint.Interface.Data.Ipv6Address.String()) } else { @@ -110,12 +112,14 @@ func TestResourceFreeformLink(t *testing.T) { ctx := context.Background() client := testutils.GetTestClient(t, ctx) apiVersion := version.Must(version.NewVersion(client.ApiVersion())) + // create a blueprint bp, sysIds := testutils.FfBlueprintB(t, ctx, 2) type testStep struct { config resourceFreeformLink } + type testCase struct { apiVersionConstraints version.Constraints steps []testStep @@ -166,8 +170,8 @@ func TestResourceFreeformLink(t *testing.T) { SystemId: sysIds[0], Interface: apstra.FreeformInterface{ Data: &apstra.FreeformInterfaceData{ - IfName: "ge-0/0/0", - TransformationId: 1, + IfName: "ge-0/0/1", + TransformationId: 2, Ipv4Address: &net.IPNet{IP: net.ParseIP("192.168.10.1"), Mask: net.CIDRMask(30, 32)}, Ipv6Address: &net.IPNet{IP: net.ParseIP("2001:db8::3"), Mask: net.CIDRMask(64, 128)}, Tags: randomStrings(rand.Intn(10)+2, 6), @@ -178,8 +182,8 @@ func TestResourceFreeformLink(t *testing.T) { SystemId: sysIds[1], Interface: apstra.FreeformInterface{ Data: &apstra.FreeformInterfaceData{ - IfName: "ge-0/0/0", - TransformationId: 1, + IfName: "ge-0/0/1", + TransformationId: 2, Ipv4Address: &net.IPNet{IP: net.ParseIP("192.168.10.2"), Mask: net.CIDRMask(30, 32)}, Ipv6Address: &net.IPNet{IP: net.ParseIP("2001:db8::4"), Mask: net.CIDRMask(64, 128)}, Tags: randomStrings(rand.Intn(10)+2, 6), @@ -198,7 +202,7 @@ func TestResourceFreeformLink(t *testing.T) { SystemId: sysIds[0], Interface: apstra.FreeformInterface{ Data: &apstra.FreeformInterfaceData{ - IfName: "ge-0/0/0", + IfName: "ge-0/0/3", TransformationId: 1, Ipv4Address: nil, Ipv6Address: nil, @@ -210,7 +214,7 @@ func TestResourceFreeformLink(t *testing.T) { SystemId: sysIds[1], Interface: apstra.FreeformInterface{ Data: &apstra.FreeformInterfaceData{ - IfName: "ge-0/0/0", + IfName: "ge-0/0/3", TransformationId: 1, Ipv4Address: nil, Ipv6Address: nil, @@ -220,7 +224,8 @@ func TestResourceFreeformLink(t *testing.T) { }, }, }, - }}, + }, + }, }, "start_maximal": { steps: []testStep{ @@ -234,7 +239,7 @@ func TestResourceFreeformLink(t *testing.T) { SystemId: sysIds[0], Interface: apstra.FreeformInterface{ Data: &apstra.FreeformInterfaceData{ - IfName: "ge-0/0/5", + IfName: "ge-0/0/4", TransformationId: 1, Ipv4Address: &net.IPNet{IP: net.ParseIP("10.1.1.1"), Mask: net.CIDRMask(30, 32)}, Ipv6Address: &net.IPNet{IP: net.ParseIP("2001:db8::1"), Mask: net.CIDRMask(64, 128)}, @@ -246,7 +251,7 @@ func TestResourceFreeformLink(t *testing.T) { SystemId: sysIds[1], Interface: apstra.FreeformInterface{ Data: &apstra.FreeformInterfaceData{ - IfName: "ge-0/0/5", + IfName: "ge-0/0/4", TransformationId: 1, Ipv4Address: &net.IPNet{IP: net.ParseIP("10.1.1.2"), Mask: net.CIDRMask(30, 32)}, Ipv6Address: &net.IPNet{IP: net.ParseIP("2001:db8::2"), Mask: net.CIDRMask(64, 128)}, @@ -267,7 +272,7 @@ func TestResourceFreeformLink(t *testing.T) { Interface: apstra.FreeformInterface{ Data: &apstra.FreeformInterfaceData{ IfName: "ge-0/0/5", - TransformationId: 1, + TransformationId: 2, Ipv4Address: nil, Ipv6Address: nil, Tags: nil, @@ -279,7 +284,7 @@ func TestResourceFreeformLink(t *testing.T) { Interface: apstra.FreeformInterface{ Data: &apstra.FreeformInterfaceData{ IfName: "ge-0/0/5", - TransformationId: 1, + TransformationId: 2, Ipv4Address: nil, Ipv6Address: nil, Tags: nil, @@ -299,7 +304,7 @@ func TestResourceFreeformLink(t *testing.T) { SystemId: sysIds[0], Interface: apstra.FreeformInterface{ Data: &apstra.FreeformInterfaceData{ - IfName: "ge-0/0/5", + IfName: "ge-0/0/6", TransformationId: 1, Ipv4Address: &net.IPNet{IP: net.ParseIP("10.2.1.1"), Mask: net.CIDRMask(30, 32)}, Ipv6Address: &net.IPNet{IP: net.ParseIP("2001:db8::3"), Mask: net.CIDRMask(64, 128)}, @@ -311,7 +316,7 @@ func TestResourceFreeformLink(t *testing.T) { SystemId: sysIds[1], Interface: apstra.FreeformInterface{ Data: &apstra.FreeformInterfaceData{ - IfName: "ge-0/0/5", + IfName: "ge-0/0/6", TransformationId: 1, Ipv4Address: &net.IPNet{IP: net.ParseIP("10.2.1.2"), Mask: net.CIDRMask(30, 32)}, Ipv6Address: &net.IPNet{IP: net.ParseIP("2001:db8::4"), Mask: net.CIDRMask(64, 128)}, From 9abc9a8f70154318560e6882268809cad4d8e6fa Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Thu, 18 Jul 2024 19:01:44 -0400 Subject: [PATCH 18/18] make docs --- docs/data-sources/freeform_link.md | 35 +++-------- docs/resources/freeform_link.md | 63 +++++-------------- .../apstra_freeform_link/example.tf | 24 +------ .../resources/apstra_freeform_link/example.tf | 52 +++------------ 4 files changed, 34 insertions(+), 140 deletions(-) diff --git a/docs/data-sources/freeform_link.md b/docs/data-sources/freeform_link.md index 02fa941c..577350c7 100644 --- a/docs/data-sources/freeform_link.md +++ b/docs/data-sources/freeform_link.md @@ -16,35 +16,15 @@ At least one optional attribute is required. ## Example Usage ```terraform -# here we build a link block - -resource "apstra_freeform_link" "test" { - blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" - name = "link_a_b" - speed = "1g" - tags = ["a", "b"] - endpoints = [ - { - system_id = "-CEYpa9xZ5chndvu0OY" - interface_name = "ge-0/0/3" - transformation_id = 1 - }, - { - system_id = "ySBRdHvl2KZmWKLhkIk" - interface_name = "ge-0/0/3" - transformation_id = 1 - } - ] -} +# This example pulls details from a link in a Freeform blueprint data "apstra_freeform_link" "test" { blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" - id = apstra_freeform_link.test.id + id = "SkY0hved7LajZY7WNzU" } output "test_Link_out" { value = data.apstra_freeform_link.test } - //output #test_Link_out = { # "aggregate_link_id" = tostring(null) @@ -102,8 +82,9 @@ Link Type. An 'ethernet' link is a normal front-panel interface. An 'aggregate_l Read-Only: -- `interface_id` (String) Graph node ID of the attached interface for this side of the link endpoint -- `interface_name` (String) the interface name -- `ipv4_address` (String) Ipv4 address of the interface -- `ipv6_address` (String) Ipv6 address of the interface -- `transformation_id` (Number) ID of the transformation in the device profile +- `interface_id` (String) Graph node ID of the associated interface +- `interface_name` (String) The interface name, as found in the associated Device Profile, e.g. `xe-0/0/0` +- `ipv4_address` (String) Ipv4 address of the interface in CIDR notation +- `ipv6_address` (String) Ipv6 address of the interface in CIDR notation +- `tags` (Set of String) Set of Tags applied to the interface +- `transformation_id` (Number) ID # of the transformation in the Device Profile diff --git a/docs/resources/freeform_link.md b/docs/resources/freeform_link.md index c422089c..a5da4d69 100644 --- a/docs/resources/freeform_link.md +++ b/docs/resources/freeform_link.md @@ -13,62 +13,28 @@ This resource creates a Link in a Freeform Blueprint. ## Example Usage ```terraform +# This example creates a link between systems "-CEYpa9xZ5chndvu0OY" and +# "ySBRdHvl2KZmWKLhkIk" in a Freeform Blueprint + resource "apstra_freeform_link" "test" { blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" name = "link_a_b" - speed = "1g" tags = ["a", "b"] endpoints = [ { - system_id = "-CEYpa9xZ5chndvu0OY" - interface_name = "ge-0/0/3" + system_id = "-CEYpa9xZ5chndvu0OY" + interface_name = "ge-0/0/3" transformation_id = 1 + tags = ["prod", "native_1000BASE-T"] }, { - system_id = "ySBRdHvl2KZmWKLhkIk" - interface_name = "ge-0/0/3" + system_id = "ySBRdHvl2KZmWKLhkIk" + interface_name = "ge-0/0/3" transformation_id = 1 + tags = ["prod", "requires_transceiver"] } ] } - -data "apstra_freeform_link" "test" { - blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" - id = apstra_freeform_link.test.id -} - -output "test_Link_out" { value = data.apstra_freeform_link.test } - - -//output -#test_Link_out = { -# "aggregate_link_id" = tostring(null) -# "blueprint_id" = "043c5787-66e8-41c7-8925-c7e52fbe6e32" -# "endpoints" = tomap({ -# "-CEYpa9xZ5chndvu0OY" = { -# "interface_id" = "c459DMed3P42wapAtUY" -# "interface_name" = "ge-0/0/3" -# "ipv4_address" = tostring(null) -# "ipv6_address" = tostring(null) -# "transformation_id" = 1 -# } -# "ySBRdHvl2KZmWKLhkIk" = { -# "interface_id" = "1wWgi25jmyZ5NBy45dA" -# "interface_name" = "ge-0/0/3" -# "ipv4_address" = tostring(null) -# "ipv6_address" = tostring(null) -# "transformation_id" = 1 -# } -# }) -# "id" = "SkY0hved7LajZY7WNzU" -# "name" = "link_a_b" -# "speed" = "10G" -# "tags" = toset([ -# "a", -# "b", -# ]) -# "type" = "ethernet" -#} ``` @@ -96,17 +62,18 @@ output "test_Link_out" { value = data.apstra_freeform_link.test } Required: -- `interface_name` (String) the interface name -- `transformation_id` (Number) ID of the transformation in the device profile +- `interface_name` (String) The interface name, as found in the associated Device Profile, e.g. `xe-0/0/0` +- `transformation_id` (Number) ID # of the transformation in the Device Profile Optional: -- `ipv4_address` (String) Ipv4 address of the interface -- `ipv6_address` (String) Ipv6 address of the interface +- `ipv4_address` (String) Ipv4 address of the interface in CIDR notation +- `ipv6_address` (String) Ipv6 address of the interface in CIDR notation +- `tags` (Set of String) Set of Tags applied to the interface Read-Only: -- `interface_id` (String) Graph node ID of the attached interface for this side of the link endpoint. +- `interface_id` (String) Graph node ID of the associated interface diff --git a/examples/data-sources/apstra_freeform_link/example.tf b/examples/data-sources/apstra_freeform_link/example.tf index eb4f616f..49e2da15 100644 --- a/examples/data-sources/apstra_freeform_link/example.tf +++ b/examples/data-sources/apstra_freeform_link/example.tf @@ -1,32 +1,12 @@ -# here we build a link block - -resource "apstra_freeform_link" "test" { - blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" - name = "link_a_b" - speed = "1g" - tags = ["a", "b"] - endpoints = [ - { - system_id = "-CEYpa9xZ5chndvu0OY" - interface_name = "ge-0/0/3" - transformation_id = 1 - }, - { - system_id = "ySBRdHvl2KZmWKLhkIk" - interface_name = "ge-0/0/3" - transformation_id = 1 - } - ] -} +# This example pulls details from a link in a Freeform blueprint data "apstra_freeform_link" "test" { blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" - id = apstra_freeform_link.test.id + id = "SkY0hved7LajZY7WNzU" } output "test_Link_out" { value = data.apstra_freeform_link.test } - //output #test_Link_out = { # "aggregate_link_id" = tostring(null) diff --git a/examples/resources/apstra_freeform_link/example.tf b/examples/resources/apstra_freeform_link/example.tf index a2d678e4..9960863d 100644 --- a/examples/resources/apstra_freeform_link/example.tf +++ b/examples/resources/apstra_freeform_link/example.tf @@ -1,56 +1,22 @@ +# This example creates a link between systems "-CEYpa9xZ5chndvu0OY" and +# "ySBRdHvl2KZmWKLhkIk" in a Freeform Blueprint + resource "apstra_freeform_link" "test" { blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" name = "link_a_b" - speed = "1g" tags = ["a", "b"] endpoints = [ { - system_id = "-CEYpa9xZ5chndvu0OY" - interface_name = "ge-0/0/3" + system_id = "-CEYpa9xZ5chndvu0OY" + interface_name = "ge-0/0/3" transformation_id = 1 + tags = ["prod", "native_1000BASE-T"] }, { - system_id = "ySBRdHvl2KZmWKLhkIk" - interface_name = "ge-0/0/3" + system_id = "ySBRdHvl2KZmWKLhkIk" + interface_name = "ge-0/0/3" transformation_id = 1 + tags = ["prod", "requires_transceiver"] } ] } - -data "apstra_freeform_link" "test" { - blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32" - id = apstra_freeform_link.test.id -} - -output "test_Link_out" { value = data.apstra_freeform_link.test } - - -//output -#test_Link_out = { -# "aggregate_link_id" = tostring(null) -# "blueprint_id" = "043c5787-66e8-41c7-8925-c7e52fbe6e32" -# "endpoints" = tomap({ -# "-CEYpa9xZ5chndvu0OY" = { -# "interface_id" = "c459DMed3P42wapAtUY" -# "interface_name" = "ge-0/0/3" -# "ipv4_address" = tostring(null) -# "ipv6_address" = tostring(null) -# "transformation_id" = 1 -# } -# "ySBRdHvl2KZmWKLhkIk" = { -# "interface_id" = "1wWgi25jmyZ5NBy45dA" -# "interface_name" = "ge-0/0/3" -# "ipv4_address" = tostring(null) -# "ipv6_address" = tostring(null) -# "transformation_id" = 1 -# } -# }) -# "id" = "SkY0hved7LajZY7WNzU" -# "name" = "link_a_b" -# "speed" = "10G" -# "tags" = toset([ -# "a", -# "b", -# ]) -# "type" = "ethernet" -#} \ No newline at end of file