diff --git a/apstra/blueprint/freeform_config_template.go b/apstra/blueprint/freeform_config_template.go
new file mode 100644
index 00000000..14c28de8
--- /dev/null
+++ b/apstra/blueprint/freeform_config_template.go
@@ -0,0 +1,120 @@
+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"
+ "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"`
+ 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"),
+ }...),
+ },
+ },
+ "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,
+ },
+ "tags": dataSourceSchema.SetAttribute{
+ MarkdownDescription: "Set of Tag labels",
+ ElementType: types.StringType,
+ Computed: true,
+ },
+ }
+}
+
+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,
+ PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
+ },
+ "name": resourceSchema.StringAttribute{
+ MarkdownDescription: "Config Template name as shown in the Web UI. Must end with `.jinja`.",
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.LengthAtLeast(7),
+ stringvalidator.RegexMatches(regexp.MustCompile(".jinja$"), "must end with '.jinja'"),
+ },
+ },
+ "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,
+ }
+}
+
+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.Tags = utils.SetValueOrNull(ctx, types.StringType, in.Tags, diags) // safe to ignore diagnostic here
+}
diff --git a/apstra/blueprint/freeform_endpoint.go b/apstra/blueprint/freeform_endpoint.go
new file mode 100644
index 00000000..15d39198
--- /dev/null
+++ b/apstra/blueprint/freeform_endpoint.go
@@ -0,0 +1,175 @@
+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/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+type freeformEndpoint struct {
+ 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"`
+ Tags types.Set `tfsdk:"tags"`
+}
+
+func (o freeformEndpoint) attrTypes() map[string]attr.Type {
+ return map[string]attr.Type{
+ "interface_name": types.StringType,
+ "interface_id": types.StringType,
+ "transformation_id": types.Int64Type,
+ "ipv4_address": cidrtypes.IPv4PrefixType{},
+ "ipv6_address": cidrtypes.IPv6PrefixType{},
+ "tags": types.SetType{ElemType: types.StringType},
+ }
+}
+
+func (o freeformEndpoint) DatasourceAttributes() map[string]dataSourceSchema.Attribute {
+ return map[string]dataSourceSchema.Attribute{
+ "interface_name": dataSourceSchema.StringAttribute{
+ Computed: true,
+ 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 associated interface",
+ },
+ "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 in CIDR notation",
+ CustomType: cidrtypes.IPv4PrefixType{},
+ },
+ "ipv6_address": dataSourceSchema.StringAttribute{
+ Computed: true,
+ 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,
+ },
+ }
+}
+
+func (o freeformEndpoint) ResourceAttributes() map[string]resourceSchema.Attribute {
+ return map[string]resourceSchema.Attribute{
+ "interface_name": resourceSchema.StringAttribute{
+ Required: true,
+ 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 associated interface",
+ },
+ "transformation_id": resourceSchema.Int64Attribute{
+ Required: true,
+ 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 in CIDR notation",
+ CustomType: cidrtypes.IPv4PrefixType{},
+ },
+ "ipv6_address": resourceSchema.StringAttribute{
+ Optional: true,
+ 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(ctx context.Context, systemId string, diags *diag.Diagnostics) *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
+ }
+
+ var tags []string
+ diags.Append(o.Tags.ElementsAs(ctx, &tags, false)...)
+
+ return &apstra.FreeformEndpoint{
+ SystemId: apstra.ObjectId(systemId),
+ Interface: apstra.FreeformInterface{
+ Data: &apstra.FreeformInterfaceData{
+ IfName: o.InterfaceName.ValueString(),
+ TransformationId: int(o.TransformationId.ValueInt64()),
+ Ipv4Address: ipNet4,
+ Ipv6Address: ipNet6,
+ Tags: tags,
+ },
+ },
+ }
+}
+
+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),
+ "interface IDs should always be populated",
+ )
+ return
+ }
+
+ o.InterfaceName = types.StringValue(in.Interface.Data.IfName)
+ o.InterfaceId = types.StringValue(in.Interface.Id.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()
+ }
+ 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 {
+ 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.MapNull(types.ObjectType{AttrTypes: freeformEndpoint{}.attrTypes()})
+ }
+
+ 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
new file mode 100644
index 00000000..85e563e1
--- /dev/null
+++ b/apstra/blueprint/freeform_link.go
@@ -0,0 +1,193 @@
+package blueprint
+
+import (
+ "context"
+ "fmt"
+ "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"
+ "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.Map `tfsdk:"endpoints"`
+ 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 " +
+ "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: "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: "ID of aggregate link node that the current link belongs to",
+ Computed: true,
+ },
+ "endpoints": dataSourceSchema.MapNestedAttribute{
+ MarkdownDescription: "Endpoints assigned to the Link",
+ Computed: true,
+ NestedObject: dataSourceSchema.NestedAttributeObject{
+ Attributes: freeformEndpoint{}.DatasourceAttributes(),
+ },
+ },
+ "tags": dataSourceSchema.SetAttribute{
+ MarkdownDescription: "Set of unique case-insensitive 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.",
+ Computed: true,
+ },
+ "type": resourceSchema.StringAttribute{
+ MarkdownDescription: "Deploy mode of the Link",
+ Computed: true,
+ },
+ "aggregate_link_id": resourceSchema.StringAttribute{
+ MarkdownDescription: "ID of aggregate link node that the current link belongs to",
+ Optional: true,
+ Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
+ },
+ "endpoints": resourceSchema.MapNestedAttribute{
+ NestedObject: resourceSchema.NestedAttributeObject{
+ Attributes: freeformEndpoint{}.ResourceAttributes(),
+ },
+ PlanModifiers: []planmodifier.Map{mapplanmodifier.RequiresReplace()},
+ MarkdownDescription: "Endpoints of the Link",
+ Required: true,
+ Validators: []validator.Map{mapvalidator.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 map[string]freeformEndpoint
+ diags.Append(o.Endpoints.ElementsAs(ctx, &endpoints, false)...)
+ if diags.HasError() {
+ return nil
+ }
+
+ var epArray [2]apstra.FreeformEndpoint
+ var i int
+ for systemId, endpoint := range endpoints {
+ epArray[i] = *endpoint.request(ctx, systemId, diags)
+ i++
+ }
+
+ return &apstra.FreeformLinkRequest{
+ Label: o.Name.ValueString(),
+ Tags: tags,
+ Endpoints: epArray,
+ }
+}
+
+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()
+ }
+
+ o.Speed = types.StringValue(string(in.Speed))
+ o.Type = types.StringValue(in.Type.String())
+ o.Name = types.StringValue(in.Label)
+ 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/blueprint/freeform_property_set.go b/apstra/blueprint/freeform_property_set.go
new file mode 100644
index 00000000..88085be1
--- /dev/null
+++ b/apstra/blueprint/freeform_property_set.go
@@ -0,0 +1,113 @@
+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) 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 Property Set lives.",
+ Required: true,
+ 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.",
+ Optional: true,
+ Computed: true,
+ Validators: []validator.String{
+ stringvalidator.LengthAtLeast(1),
+ stringvalidator.ExactlyOneOf(path.Expressions{
+ path.MatchRelative(),
+ path.MatchRoot("name"),
+ }...),
+ },
+ },
+ "system_id": dataSourceSchema.StringAttribute{
+ MarkdownDescription: "The system ID where the Property Set is associated.",
+ Computed: true,
+ 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.",
+ Optional: true,
+ Computed: true,
+ Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
+ },
+ "values": dataSourceSchema.StringAttribute{
+ MarkdownDescription: "A map of values in the Property Set in JSON format.",
+ CustomType: jsontypes.NormalizedType{},
+ Computed: true,
+ },
+ }
+}
+
+func (o FreeformPropertySet) 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 Property Set.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
+ },
+ "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.",
+ 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{},
+ Required: 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/blueprint/freeform_system.go b/apstra/blueprint/freeform_system.go
new file mode 100644
index 00000000..7fa19a91
--- /dev/null
+++ b/apstra/blueprint/freeform_system.go
@@ -0,0 +1,179 @@
+package blueprint
+
+import (
+ "context"
+ "fmt"
+ "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 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,
+ Computed: true,
+ },
+ }
+}
+
+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.",
+ Required: true,
+ Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
+ PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
+ },
+ "id": resourceSchema.StringAttribute{
+ MarkdownDescription: "ID of the Freeform System.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
+ },
+ "name": resourceSchema.StringAttribute{
+ MarkdownDescription: "Freeform System name as shown in the Web UI.",
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.RegexMatches(regexp.MustCompile("^[a-zA-Z0-9.-_]+$"), "name may consist only of the following characters : a-zA-Z0-9.-_")},
+ },
+ "hostname": resourceSchema.StringAttribute{
+ MarkdownDescription: "Hostname of the Freeform System.",
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.RegexMatches(regexp.MustCompile(hostnameRegexp), "must match regex "+hostnameRegexp),
+ },
+ },
+ "deploy_mode": resourceSchema.StringAttribute{
+ MarkdownDescription: "Deploy mode of the System",
+ Optional: true,
+ Validators: []validator.String{stringvalidator.OneOf(utils.AllNodeDeployModes()...)},
+ },
+ "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": resourceSchema.StringAttribute{
+ MarkdownDescription: "Device profile ID of the System",
+ Optional: true,
+ Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
+ },
+ "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)},
+ },
+ "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(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/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/configure_resource.go b/apstra/configure_resource.go
index 8b1de434..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"
)
@@ -12,11 +13,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 +67,14 @@ 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.(resourceWithSetFfBpClientFunc); ok {
+ rs.setBpClientFunc(pd.getFreeformClient)
+ }
+
if rs, ok := rs.(resourceWithSetBpLockFunc); ok {
rs.setBpLockFunc(pd.bpLockFunc)
}
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_config_template.go b/apstra/data_source_freeform_config_template.go
new file mode 100644
index 00000000..8be3437c
--- /dev/null
+++ b/apstra/data_source_freeform_config_template.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 = &dataSourceFreeformConfigTemplate{}
+ _ 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.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 *dataSourceFreeformConfigTemplate) setBpClientFunc(f func(context.Context, string) (*apstra.FreeformClient, error)) {
+ o.getBpClientFunc = f
+}
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/data_source_freeform_property_set.go b/apstra/data_source_freeform_property_set.go
new file mode 100644
index 00000000..36a2f55b
--- /dev/null
+++ b/apstra/data_source_freeform_property_set.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 = &dataSourceFreeformPropertySet{}
+ _ datasourceWithSetFfBpClientFunc = &dataSourceFreeformPropertySet{}
+)
+
+type dataSourceFreeformPropertySet struct {
+ getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error)
+}
+
+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 Property Set.\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 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, 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.FreeformPropertySet
+ switch {
+ case !config.Id.IsNull():
+ api, err = bp.GetPropertySet(ctx, apstra.ObjectId(config.Id.ValueString()))
+ if utils.IsApstra404(err) {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("id"),
+ "Property Set not found",
+ fmt.Sprintf("Property Set with ID %s not found", config.Id))
+ return
+ }
+ 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
+ }
+
+ 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 *dataSourceFreeformPropertySet) setBpClientFunc(f func(context.Context, string) (*apstra.FreeformClient, error)) {
+ o.getBpClientFunc = f
+}
diff --git a/apstra/data_source_freeform_system.go b/apstra/data_source_freeform_system.go
new file mode 100644
index 00000000..3d7dd4fe
--- /dev/null
+++ b/apstra/data_source_freeform_system.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 = &dataSourceFreeformSystem{}
+ _ 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 System.\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/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/export_test.go b/apstra/export_test.go
index f800289e..658d31c6 100644
--- a/apstra/export_test.go
+++ b/apstra/export_test.go
@@ -9,11 +9,15 @@ import (
var (
ResourceAgentProfile = resourceAgentProfile{}
ResourceDatacenterGenericSystem = resourceDatacenterGenericSystem{}
- ResourceDatacenterIpLinkAddressing = resourceDatacenterIpLinkAddressing{}
ResourceDatacenterRoutingZone = resourceDatacenterRoutingZone{}
+ ResourceFreeformConfigTemplate = resourceFreeformConfigTemplate{}
+ ResourceFreeformLink = resourceFreeformLink{}
+ ResourceFreeformSystem = resourceFreeformSystem{}
+ ResourceFreeformPropertySet = resourceFreeformPropertySet{}
ResourceIpv4Pool = resourceIpv4Pool{}
ResourceTemplatePodBased = resourceTemplatePodBased{}
ResourceTemplateCollapsed = resourceTemplateCollapsed{}
+ ResourceDatacenterIpLinkAddressing = resourceDatacenterIpLinkAddressing{}
)
func ResourceName(ctx context.Context, r resource.Resource) string {
diff --git a/apstra/provider.go b/apstra/provider.go
index fef23d31..815157a6 100644
--- a/apstra/provider.go
+++ b/apstra/provider.go
@@ -81,11 +81,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 {
@@ -103,6 +104,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
}
@@ -430,8 +432,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 {
@@ -455,6 +457,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,
@@ -463,6 +492,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
@@ -518,6 +548,10 @@ 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 &dataSourceFreeformLink{} },
+ 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{} },
@@ -552,6 +586,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{} },
@@ -569,9 +606,10 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource {
func() resource.Resource { return &resourceDatacenterIpLinkAddressing{} },
func() resource.Resource { return &resourceDatacenterVirtualNetwork{} },
func() resource.Resource { return &resourceDeviceAllocation{} },
- func() resource.Resource { return &resourceBlueprintIbaDashboard{} },
- func() resource.Resource { return &resourceBlueprintIbaProbe{} },
- func() resource.Resource { return &resourceBlueprintIbaWidget{} },
+ 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{} },
func() resource.Resource { return &resourceInterfaceMap{} },
func() resource.Resource { return &resourceIpv4Pool{} },
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 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_device_allocation.go b/apstra/resource_datacenter_device_allocation.go
index 78774662..99b2dbf4 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_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{}
)
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 5ebfd2bb..cef3d520 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_config_template.go b/apstra/resource_freeform_config_template.go
new file mode 100644
index 00000000..6d04e02b
--- /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{}
+ _ resourceWithSetFfBpClientFunc = &resourceFreeformConfigTemplate{}
+ _ 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)...)
+}
+
+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)...)
+}
+
+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) {
+ return // 404 is okay
+ }
+ resp.Diagnostics.AddError("failed to create blueprint client", err.Error())
+ return
+ }
+
+ // Lock the blueprint mutex.
+ err = o.lockFunc(ctx, state.BlueprintId.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError(
+ fmt.Sprintf("error locking blueprint %q mutex", state.BlueprintId.ValueString()),
+ err.Error())
+ return
+ }
+
+ // Delete Config Template by calling API
+ err = bp.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/apstra/resource_freeform_config_template_integration_test.go b/apstra/resource_freeform_config_template_integration_test.go
new file mode 100644
index 00000000..220baeae
--- /dev/null
+++ b/apstra/resource_freeform_config_template_integration_test.go
@@ -0,0 +1,171 @@
+//go:build integration
+
+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"
+)
+
+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/resource_freeform_link.go b/apstra/resource_freeform_link.go
new file mode 100644
index 00000000..17c1d49d
--- /dev/null
+++ b/apstra/resource_freeform_link.go
@@ -0,0 +1,218 @@
+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
+ }
+
+ // 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.LoadApiData(ctx, api.Data, &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/apstra/resource_freeform_link_integration_test.go b/apstra/resource_freeform_link_integration_test.go
new file mode 100644
index 00000000..4b854531
--- /dev/null
+++ b/apstra/resource_freeform_link_integration_test.go
@@ -0,0 +1,367 @@
+//go:build integration
+
+package tfapstra_test
+
+import (
+ "context"
+ "fmt"
+ "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"
+ "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),
+ tags: randomStrings(rand.Intn(10)+2, 6),
+ endpoints: []apstra.FreeformEndpoint{
+ {
+ SystemId: sysIds[0],
+ Interface: apstra.FreeformInterface{
+ Data: &apstra.FreeformInterfaceData{
+ 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),
+ },
+ },
+ },
+ {
+ SystemId: sysIds[1],
+ Interface: apstra.FreeformInterface{
+ Data: &apstra.FreeformInterfaceData{
+ 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),
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ 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/3",
+ TransformationId: 1,
+ Ipv4Address: nil,
+ Ipv6Address: nil,
+ Tags: nil,
+ },
+ },
+ },
+ {
+ SystemId: sysIds[1],
+ Interface: apstra.FreeformInterface{
+ Data: &apstra.FreeformInterfaceData{
+ IfName: "ge-0/0/3",
+ TransformationId: 1,
+ Ipv4Address: nil,
+ Ipv6Address: nil,
+ Tags: nil,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "start_maximal": {
+ 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/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)},
+ Tags: randomStrings(rand.Intn(10)+2, 6),
+ },
+ },
+ },
+ {
+ SystemId: sysIds[1],
+ Interface: apstra.FreeformInterface{
+ Data: &apstra.FreeformInterfaceData{
+ 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)},
+ 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: 2,
+ Ipv4Address: nil,
+ Ipv6Address: nil,
+ Tags: nil,
+ },
+ },
+ },
+ {
+ SystemId: sysIds[1],
+ Interface: apstra.FreeformInterface{
+ Data: &apstra.FreeformInterfaceData{
+ IfName: "ge-0/0/5",
+ TransformationId: 2,
+ 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/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)},
+ Tags: randomStrings(rand.Intn(10)+2, 6),
+ },
+ },
+ },
+ {
+ SystemId: sysIds[1],
+ Interface: apstra.FreeformInterface{
+ Data: &apstra.FreeformInterfaceData{
+ 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)},
+ 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/resource_freeform_property_set.go b/apstra/resource_freeform_property_set.go
new file mode 100644
index 00000000..7f6d4001
--- /dev/null
+++ b/apstra/resource_freeform_property_set.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 = &resourceFreeformPropertySet{}
+ _ resourceWithSetFfBpClientFunc = &resourceFreeformPropertySet{}
+ _ 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)...)
+}
+
+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)...)
+}
+
+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) {
+ 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 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/apstra/resource_freeform_property_set_integration_test.go b/apstra/resource_freeform_property_set_integration_test.go
new file mode 100644
index 00000000..4cd2898f
--- /dev/null
+++ b/apstra/resource_freeform_property_set_integration_test.go
@@ -0,0 +1,124 @@
+//go:build integration
+
+package tfapstra_test
+
+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"
+)
+
+const (
+ resourceFreeformPropertySetHcl = `
+resource %q %q {
+ blueprint_id = %q
+ system_id = %s
+ name = %q
+ values = %q
+}
+`
+)
+
+type resourceFreeformPropertySet struct {
+ blueprintId string
+ name string
+ systemId string
+ values json.RawMessage
+}
+
+func (o resourceFreeformPropertySet) render(rType, rName string) string {
+ return fmt.Sprintf(resourceFreeformPropertySetHcl,
+ rType, rName,
+ o.blueprintId,
+ stringOrNull(o.systemId),
+ 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.systemId != "" {
+ result.append(t, "TestCheckResourceAttr", "system_id", o.systemId)
+ }
+
+ 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..7b255471
--- /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{}
+ _ resourceWithSetFfBpClientFunc = &resourceFreeformSystem{}
+ _ 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)...)
+}
+
+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)...)
+}
+
+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) {
+ return // 404 is okay
+ }
+ resp.Diagnostics.AddError("failed to create blueprint client", err.Error())
+ return
+ }
+
+ // Lock the blueprint mutex.
+ err = o.lockFunc(ctx, state.BlueprintId.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError(
+ fmt.Sprintf("error locking blueprint %q mutex", state.BlueprintId.ValueString()),
+ err.Error())
+ return
+ }
+
+ // Delete Config Template by calling API
+ err = bp.DeleteSystem(ctx, apstra.ObjectId(state.Id.ValueString()))
+ if err != nil {
+ if utils.IsApstra404(err) {
+ return // 404 is okay
+ }
+ resp.Diagnostics.AddError("error deleting Freeform System", err.Error())
+ return
+ }
+}
+
+func (o *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/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/apstra/test_helpers_test.go b/apstra/test_helpers_test.go
index 9b80cab5..a469f38f 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"
@@ -219,6 +220,32 @@ 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
+}
+
+func randomJson(t testing.TB, maxInt int, strLen int, count int) json.RawMessage {
+ t.Helper()
+
+ 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)
+ } else {
+ preResult["a"+acctest.RandString(strLen-1)] = acctest.RandString(strLen)
+ }
+ }
+
+ result, err := json.Marshal(&preResult)
+ require.NoError(t, err)
+
+ return result
+}
+
func randomSlash31(t testing.TB, cidrBlock string) net.IPNet {
t.Helper()
@@ -410,6 +437,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 dfd54e52..18e0d0e7 100644
--- a/apstra/test_utils/blueprint.go
+++ b/apstra/test_utils/blueprint.go
@@ -228,3 +228,46 @@ 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
+}
+
+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/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
new file mode 100644
index 00000000..81f2118e
--- /dev/null
+++ b/docs/data-sources/freeform_config_template.md
@@ -0,0 +1,41 @@
+---
+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
+# 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"
+}
+```
+
+
+## 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.
+
+### Read-Only
+
+- `tags` (Set of String) Set of Tag labels
+- `text` (String) Configuration Jinja2 template text
diff --git a/docs/data-sources/freeform_link.md b/docs/data-sources/freeform_link.md
new file mode 100644
index 00000000..577350c7
--- /dev/null
+++ b/docs/data-sources/freeform_link.md
@@ -0,0 +1,90 @@
+---
+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
+# This example pulls details from a link in a Freeform blueprint
+
+data "apstra_freeform_link" "test" {
+ blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32"
+ id = "SkY0hved7LajZY7WNzU"
+}
+
+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 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/data-sources/freeform_property_set.md b/docs/data-sources/freeform_property_set.md
new file mode 100644
index 00000000..68d885b1
--- /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 Property Set.
+ At least one optional attribute is required.
+---
+
+# apstra_freeform_property_set (Data Source)
+
+This data source provides details of a specific Freeform Property Set.
+
+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 = "043c5787-66e8-41c7-8925-c7e52fbe6e32"
+ name = "prop_set_foo"
+ values = jsonencode({
+ foo = "bar"
+ clown = 2
+ })
+}
+
+# here we retrieve the property_set.
+
+data "apstra_freeform_property_set" "foo" {
+ 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" = "043c5787-66e8-41c7-8925-c7e52fbe6e32"
+# "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/data-sources/freeform_system.md b/docs/data-sources/freeform_system.md
new file mode 100644
index 00000000..bf6a546a
--- /dev/null
+++ b/docs/data-sources/freeform_system.md
@@ -0,0 +1,79 @@
+---
+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
+# This example defines a freeform system in a blueprint
+
+resource "apstra_freeform_system" "test" {
+ 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 = "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" = "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"
+# }
+```
+
+
+## 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_config_template.md b/docs/resources/freeform_config_template.md
new file mode 100644
index 00000000..46b5c33f
--- /dev/null
+++ b/docs/resources/freeform_config_template.md
@@ -0,0 +1,73 @@
+---
+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
+# This example creates a Config Template from a local jinja file.
+locals {
+ template_filename = "interfaces.jinja"
+}
+
+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}")
+}
+
+/*
+# 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;
+ }
+ }
+ }
+}
+*/
+```
+
+
+## Schema
+
+### Required
+
+- `blueprint_id` (String) Apstra Blueprint ID.
+- `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
+
+### Read-Only
+
+- `id` (String) ID of the Config Template.
+
+
+
diff --git a/docs/resources/freeform_link.md b/docs/resources/freeform_link.md
new file mode 100644
index 00000000..a5da4d69
--- /dev/null
+++ b/docs/resources/freeform_link.md
@@ -0,0 +1,79 @@
+---
+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
+# 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"
+ tags = ["a", "b"]
+ endpoints = [
+ {
+ 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"
+ transformation_id = 1
+ tags = ["prod", "requires_transceiver"]
+ }
+ ]
+}
+```
+
+
+## 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, 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 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 associated interface
+
+
+
diff --git a/docs/resources/freeform_property_set.md b/docs/resources/freeform_property_set.md
new file mode 100644
index 00000000..9fdc9d4a
--- /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 = "043c5787-66e8-41c7-8925-c7e52fbe6e32"
+ 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 = "043c5787-66e8-41c7-8925-c7e52fbe6e32"
+ 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/docs/resources/freeform_system.md b/docs/resources/freeform_system.md
new file mode 100644
index 00000000..72e2e773
--- /dev/null
+++ b/docs/resources/freeform_system.md
@@ -0,0 +1,80 @@
+---
+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
+# This example defines a freeform system in a blueprint
+
+
+resource "apstra_freeform_system" "test" {
+ 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 = "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" = "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"
+# }
+```
+
+
+## Schema
+
+### 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`
+
+### Optional
+
+- `deploy_mode` (String) Deploy mode of the System
+- `device_profile_id` (String) Device profile ID of the 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_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
new file mode 100644
index 00000000..08f1b55f
--- /dev/null
+++ b/examples/data-sources/apstra_freeform_config_template/example.tf
@@ -0,0 +1,5 @@
+# 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"
+}
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..49e2da15
--- /dev/null
+++ b/examples/data-sources/apstra_freeform_link/example.tf
@@ -0,0 +1,39 @@
+# This example pulls details from a link in a Freeform blueprint
+
+data "apstra_freeform_link" "test" {
+ blueprint_id = "043c5787-66e8-41c7-8925-c7e52fbe6e32"
+ id = "SkY0hved7LajZY7WNzU"
+}
+
+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/data-sources/apstra_freeform_property_set/example.tf b/examples/data-sources/apstra_freeform_property_set/example.tf
new file mode 100644
index 00000000..741bdad0
--- /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 = "043c5787-66e8-41c7-8925-c7e52fbe6e32"
+ name = "prop_set_foo"
+ values = jsonencode({
+ foo = "bar"
+ clown = 2
+ })
+}
+
+# here we retrieve the property_set.
+
+data "apstra_freeform_property_set" "foo" {
+ 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" = "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
new file mode 100644
index 00000000..a0643c8f
--- /dev/null
+++ b/examples/data-sources/apstra_freeform_system/example.tf
@@ -0,0 +1,40 @@
+# This example defines a freeform system in a blueprint
+
+resource "apstra_freeform_system" "test" {
+ 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 = "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" = "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_config_template/example.tf b/examples/resources/apstra_freeform_config_template/example.tf
new file mode 100644
index 00000000..7223fe6d
--- /dev/null
+++ b/examples/resources/apstra_freeform_config_template/example.tf
@@ -0,0 +1,37 @@
+# This example creates a Config Template from a local jinja file.
+locals {
+ template_filename = "interfaces.jinja"
+}
+
+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}")
+}
+
+/*
+# 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
diff --git a/examples/resources/apstra_freeform_link/example.tf b/examples/resources/apstra_freeform_link/example.tf
new file mode 100644
index 00000000..9960863d
--- /dev/null
+++ b/examples/resources/apstra_freeform_link/example.tf
@@ -0,0 +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"
+ tags = ["a", "b"]
+ endpoints = [
+ {
+ 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"
+ transformation_id = 1
+ tags = ["prod", "requires_transceiver"]
+ }
+ ]
+}
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..28f3249b
--- /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 = "043c5787-66e8-41c7-8925-c7e52fbe6e32"
+ 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 = "043c5787-66e8-41c7-8925-c7e52fbe6e32"
+ 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/examples/resources/apstra_freeform_system/example.tf b/examples/resources/apstra_freeform_system/example.tf
new file mode 100644
index 00000000..22cbafa4
--- /dev/null
+++ b/examples/resources/apstra_freeform_system/example.tf
@@ -0,0 +1,40 @@
+# This example defines a freeform system in a blueprint
+
+
+resource "apstra_freeform_system" "test" {
+ 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 = "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" = "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/go.mod b/go.mod
index b3444921..b76ba5c5 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ go 1.22.5
require (
github.com/IBM/netaddr v1.5.0
- github.com/Juniper/apstra-go-sdk v0.0.0-20240712140830-91b2f21e59a3
+ 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 8477909a..5d490fa9 100644
--- a/go.sum
+++ b/go.sum
@@ -6,8 +6,8 @@ github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/IBM/netaddr v1.5.0 h1:IJlFZe1+nFs09TeMB/HOP4+xBnX2iM/xgiDOgZgTJq0=
github.com/IBM/netaddr v1.5.0/go.mod h1:DDBPeYgbFzoXHjSz9Jwk7K8wmWV4+a/Kv0LqRnb8we4=
-github.com/Juniper/apstra-go-sdk v0.0.0-20240712140830-91b2f21e59a3 h1:MzPp2KhQsGu87ZVXJOCy6pqwLMZwC8VTXSBUC14rPw4=
-github.com/Juniper/apstra-go-sdk v0.0.0-20240712140830-91b2f21e59a3/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=