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=