From 67e38c9868131f56be5c7b688d23497b1b0979ce Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Wed, 4 Dec 2024 18:28:42 -0500 Subject: [PATCH 1/4] eliminate bad assumption about logical interface node IDs --- apstra/blueprint/ip_link_addressing.go | 128 ++------------ .../resource_datacenter_ip_link_addressing.go | 156 ++++++++++++++++++ apstra/private/state.go | 14 ++ .../resource_datacenter_ip_link_addressing.go | 124 ++++++++------ 4 files changed, 255 insertions(+), 167 deletions(-) create mode 100644 apstra/private/resource_datacenter_ip_link_addressing.go create mode 100644 apstra/private/state.go diff --git a/apstra/blueprint/ip_link_addressing.go b/apstra/blueprint/ip_link_addressing.go index b5d792a9..651a4eb2 100644 --- a/apstra/blueprint/ip_link_addressing.go +++ b/apstra/blueprint/ip_link_addressing.go @@ -2,21 +2,19 @@ package blueprint import ( "context" - "encoding/json" "fmt" "net" "strings" + "github.com/Juniper/terraform-provider-apstra/apstra/private" "github.com/Juniper/apstra-go-sdk/apstra" "github.com/Juniper/apstra-go-sdk/apstra/enum" - "github.com/Juniper/terraform-provider-apstra/apstra/constants" "github.com/Juniper/terraform-provider-apstra/apstra/utils" apstravalidator "github.com/Juniper/terraform-provider-apstra/apstra/validator" "github.com/hashicorp/terraform-plugin-framework-nettypes/cidrtypes" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" 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/stringdefault" @@ -28,8 +26,6 @@ import ( type IpLinkAddressing struct { BlueprintId types.String `tfsdk:"blueprint_id"` LinkId types.String `tfsdk:"link_id"` - SwitchIntfId types.String `tfsdk:"switch_interface_id"` - GenericIntfId types.String `tfsdk:"generic_interface_id"` SwitchIpv4Type types.String `tfsdk:"switch_ipv4_address_type"` SwitchIpv4Addr cidrtypes.IPv4Prefix `tfsdk:"switch_ipv4_address"` SwitchIpv6Type types.String `tfsdk:"switch_ipv6_address_type"` @@ -57,16 +53,6 @@ func (o IpLinkAddressing) ResourceAttributes() map[string]resourceSchema.Attribu PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, - "switch_interface_id": resourceSchema.StringAttribute{ - MarkdownDescription: "Apstra graph node ID of the node to which `switch` IP information will be associated.", - Computed: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, - }, - "generic_interface_id": resourceSchema.StringAttribute{ - MarkdownDescription: "Apstra graph node ID of the node to which `generic` IP information will be associated.", - Computed: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, - }, "switch_ipv4_address_type": resourceSchema.StringAttribute{ MarkdownDescription: fmt.Sprintf("Allowed values: [`%s`]", strings.Join(utils.AllInterfaceNumberingIpv4Types(), "`,`")), Optional: true, @@ -180,20 +166,11 @@ func requestEndpoint(v4type, v6type types.String, v4addr cidrtypes.IPv4Prefix, v return result } -func (o IpLinkAddressing) Request(_ context.Context, diags *diag.Diagnostics) map[apstra.ObjectId]apstra.TwoStageL3ClosSubinterface { - if !utils.HasValue(o.SwitchIntfId) || !utils.HasValue(o.GenericIntfId) { - diags.AddError( - constants.ErrProviderBug, - fmt.Sprintf("attempt to generate ip link addressing with unknown interface ID\n"+ - "switch_interface_id: %s\n generic_interface_id: %s", o.SwitchIntfId, o.GenericIntfId), - ) +func (o IpLinkAddressing) Request(_ context.Context, ids private.ResourceDatacenterIpLinkAddressingInterfaceIds, diags *diag.Diagnostics) map[apstra.ObjectId]apstra.TwoStageL3ClosSubinterface { + return map[apstra.ObjectId]apstra.TwoStageL3ClosSubinterface{ + ids.SwitchInterface: requestEndpoint(o.SwitchIpv4Type, o.SwitchIpv6Type, o.SwitchIpv4Addr, o.SwitchIpv6Addr, "switch", diags), + ids.GenericInterface: requestEndpoint(o.GenericIpv4Type, o.GenericIpv6Type, o.GenericIpv4Addr, o.GenericIpv6Addr, "generic", diags), } - - result := make(map[apstra.ObjectId]apstra.TwoStageL3ClosSubinterface, 2) - result[apstra.ObjectId(o.SwitchIntfId.ValueString())] = requestEndpoint(o.SwitchIpv4Type, o.SwitchIpv6Type, o.SwitchIpv4Addr, o.SwitchIpv6Addr, "switch", diags) - result[apstra.ObjectId(o.GenericIntfId.ValueString())] = requestEndpoint(o.GenericIpv4Type, o.GenericIpv6Type, o.GenericIpv4Addr, o.GenericIpv6Addr, "generic", diags) - - return result } func epBySubinterfaceId(siId apstra.ObjectId, eps []apstra.TwoStageL3ClosSubinterfaceLinkEndpoint, diags *diag.Diagnostics) *apstra.TwoStageL3ClosSubinterfaceLinkEndpoint { @@ -223,46 +200,7 @@ func epBySubinterfaceId(siId apstra.ObjectId, eps []apstra.TwoStageL3ClosSubinte return result } -func epBySystemType(sysType apstra.SystemType, eps []apstra.TwoStageL3ClosSubinterfaceLinkEndpoint, diags *diag.Diagnostics) *apstra.TwoStageL3ClosSubinterfaceLinkEndpoint { - var systemRoles []apstra.SystemRole - - switch sysType { - case apstra.SystemTypeSwitch: - systemRoles = []apstra.SystemRole{apstra.SystemRoleSuperSpine, apstra.SystemRoleSpine, apstra.SystemRoleLeaf, apstra.SystemRoleAccess} - case apstra.SystemTypeServer: - systemRoles = []apstra.SystemRole{apstra.SystemRoleGeneric} - default: - diags.AddError(constants.ErrProviderBug, fmt.Sprintf("unexpected system type %q", sysType)) - return nil - } - - var result *apstra.TwoStageL3ClosSubinterfaceLinkEndpoint - for _, ep := range eps { - ep := ep - if utils.SliceContains(ep.System.Role, systemRoles) { - if result != nil { - diags.AddError( - "Unexpected API response", - fmt.Sprintf("Logical link has multiple endpoints on systems with %q roles", sysType), - ) - return nil - } - - result = &ep - } - } - - if result == nil { - diags.AddError( - "Unexpected API response", - fmt.Sprintf("Logical link has no endpoints on systems with %q roles", sysType), - ) - } - - return result -} - -func (o *IpLinkAddressing) LoadApiData(_ context.Context, in *apstra.TwoStageL3ClosSubinterfaceLink, diags *diag.Diagnostics) { +func (o *IpLinkAddressing) LoadApiData(_ context.Context, in *apstra.TwoStageL3ClosSubinterfaceLink, private private.ResourceDatacenterIpLinkAddressingInterfaceIds, diags *diag.Diagnostics) { // ensure 2 endpoints if len(in.Endpoints) != 2 { diags.AddError("Unexpected API response", fmt.Sprintf("Logical links should have 2 endpoints, got %d", len(in.Endpoints))) @@ -284,21 +222,9 @@ func (o *IpLinkAddressing) LoadApiData(_ context.Context, in *apstra.TwoStageL3C return } - // ensure the subinterface IDs have the expected values - if !utils.ItemInSlice(apstra.ObjectId(o.GenericIntfId.ValueString()), siIds) || - !utils.ItemInSlice(apstra.ObjectId(o.SwitchIntfId.ValueString()), siIds) { - diags.AddError( - "Unexpected API response", - fmt.Sprintf("Logical link %s previously had subinterface IDs %s and %s.\n"+ - "Now it has IDs %q and %q. Endpoint IDs are not expected to change.", - o.LinkId, o.SwitchIntfId, o.GenericIntfId, siIds[0], siIds[1]), - ) - return - } - // extract the endpoints by subinterface ID - switchEp := epBySubinterfaceId(apstra.ObjectId(o.SwitchIntfId.ValueString()), in.Endpoints, diags) - genericEp := epBySubinterfaceId(apstra.ObjectId(o.GenericIntfId.ValueString()), in.Endpoints, diags) + switchEp := epBySubinterfaceId(private.SwitchInterface, in.Endpoints, diags) + genericEp := epBySubinterfaceId(private.GenericInterface, in.Endpoints, diags) if diags.HasError() { return } @@ -327,39 +253,7 @@ func (o *IpLinkAddressing) LoadApiData(_ context.Context, in *apstra.TwoStageL3C } } -// LoadImmutableData sets the switch and generic subinterface ID elements within o and saves the -// initial switch/generic v4/v6 address types (probably those indicated in the connectivity template). -// The user supplies the link ID, so we only need to do this once: at the beginning of Create(). The -// subinterface nodes associated with a given link node should never change. -func (o *IpLinkAddressing) LoadImmutableData(ctx context.Context, in *apstra.TwoStageL3ClosSubinterfaceLink, resp *resource.CreateResponse) { - switchEp := epBySystemType(apstra.SystemTypeSwitch, in.Endpoints, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - - genericEp := epBySystemType(apstra.SystemTypeServer, in.Endpoints, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - - o.SwitchIntfId = types.StringValue(switchEp.SubinterfaceId.String()) - o.GenericIntfId = types.StringValue(genericEp.SubinterfaceId.String()) - - private, err := json.Marshal(struct { - SwitchIpv4AddressType string `json:"switch_ipv4_address_type"` - SwitchIpv6AddressType string `json:"switch_ipv6_address_type"` - GenericIpv4AddressType string `json:"generic_ipv4_address_type"` - GenericIpv6AddressType string `json:"generic_ipv6_address_type"` - }{ - SwitchIpv4AddressType: utils.StringersToFriendlyString(switchEp.Subinterface.Ipv4AddrType), - SwitchIpv6AddressType: utils.StringersToFriendlyString(switchEp.Subinterface.Ipv6AddrType), - GenericIpv4AddressType: utils.StringersToFriendlyString(genericEp.Subinterface.Ipv4AddrType), - GenericIpv6AddressType: utils.StringersToFriendlyString(genericEp.Subinterface.Ipv6AddrType), - }) - if err != nil { - resp.Diagnostics.AddError("failed marshaling private data", err.Error()) - return - } - - resp.Private.SetKey(ctx, "ep_addr_types", private) +type PrivateInterfaceIds struct { + SwitchInterface apstra.ObjectId `json:"switch_interface"` + GenericInterface apstra.ObjectId `json:"generic_interface"` } diff --git a/apstra/private/resource_datacenter_ip_link_addressing.go b/apstra/private/resource_datacenter_ip_link_addressing.go new file mode 100644 index 00000000..cd64b340 --- /dev/null +++ b/apstra/private/resource_datacenter_ip_link_addressing.go @@ -0,0 +1,156 @@ +package private + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/Juniper/apstra-go-sdk/apstra" + "github.com/Juniper/apstra-go-sdk/apstra/enum" + "github.com/Juniper/terraform-provider-apstra/apstra/constants" + "github.com/Juniper/terraform-provider-apstra/apstra/utils" + "github.com/hashicorp/terraform-plugin-framework/diag" +) + +// ResourceDatacenterIpLinkAddressingInterfaceAddressing is stored in private state by +// ResourceDatacenterIpLinkAddressing.Create(). It is the record of the original numbering scheme +// on a logical link, and is restored by ResourceDatacenterIpLinkAddressing.Delete() +type ResourceDatacenterIpLinkAddressingInterfaceAddressing struct { + SwitchIpv4 enum.InterfaceNumberingIpv4Type `json:"switch_ipv4"` + SwitchIpv6 enum.InterfaceNumberingIpv6Type `json:"switch_ipv6"` + GenericIpv4 enum.InterfaceNumberingIpv4Type `json:"generic_ipv4"` + GenericIpv6 enum.InterfaceNumberingIpv6Type `json:"generic_ipv6"` +} + +func (o *ResourceDatacenterIpLinkAddressingInterfaceAddressing) LoadApiData(_ context.Context, link *apstra.TwoStageL3ClosSubinterfaceLink, diags *diag.Diagnostics) { + switchEp := epBySystemType(apstra.SystemTypeSwitch, link.Endpoints, diags) + if diags.HasError() { + return + } + + genericEp := epBySystemType(apstra.SystemTypeServer, link.Endpoints, diags) + if diags.HasError() { + return + } + + o.SwitchIpv4 = switchEp.Subinterface.Ipv4AddrType + o.SwitchIpv6 = switchEp.Subinterface.Ipv6AddrType + o.GenericIpv4 = genericEp.Subinterface.Ipv4AddrType + o.GenericIpv6 = genericEp.Subinterface.Ipv6AddrType +} + +func (o *ResourceDatacenterIpLinkAddressingInterfaceAddressing) LoadPrivateState(ctx context.Context, ps State, diags *diag.Diagnostics) { + b, d := ps.GetKey(ctx, "ResourceDatacenterIpLinkAddressingInterfaceAddressing") + diags.Append(d...) + if diags.HasError() { + return + } + + err := json.Unmarshal(b, &o) + if err != nil { + diags.AddError("failed to unmarshal private state", err.Error()) + return + } +} + +func (o *ResourceDatacenterIpLinkAddressingInterfaceAddressing) SetPrivateState(ctx context.Context, ps State, diags *diag.Diagnostics) { + b, err := json.Marshal(o) + if err != nil { + diags.AddError("failed to marshal private state", err.Error()) + return + } + + diags.Append(ps.SetKey(ctx, "ResourceDatacenterIpLinkAddressingInterfaceAddressing", b)...) +} + +// ResourceDatacenterIpLinkAddressingInterfaceIds contains the logical interfaces associated with +// a logical link. It turns out that these interface IDs are NOT immutable. The interfaces, along +// with the logical link are created as a side-effect of associating a CT containing IP Link +// primitives with a physical switch port. Modifying the CT may cause the logical link and logical +// interface pair to be replaced. The logical link is indistinguishable from immutable because +// its ID is constructed by encoding other information. The interfaces, on the other hand, use +// random IDs, so they may be found to have changed from one run to the next. As a result, this +// "private data" struct is no longer committed to private state. It's now relegated to merely +// unpacking the API response via the LoadApiData() method. +type ResourceDatacenterIpLinkAddressingInterfaceIds struct { + SwitchInterface apstra.ObjectId `json:"switch_interface"` + GenericInterface apstra.ObjectId `json:"generic_interface"` +} + +func (o *ResourceDatacenterIpLinkAddressingInterfaceIds) LoadApiData(_ context.Context, link *apstra.TwoStageL3ClosSubinterfaceLink, diags *diag.Diagnostics) { + switchEp := epBySystemType(apstra.SystemTypeSwitch, link.Endpoints, diags) + if diags.HasError() { + return + } + + genericEp := epBySystemType(apstra.SystemTypeServer, link.Endpoints, diags) + if diags.HasError() { + return + } + + o.SwitchInterface = switchEp.SubinterfaceId + o.GenericInterface = genericEp.SubinterfaceId +} + +func (o *ResourceDatacenterIpLinkAddressingInterfaceIds) LoadPrivateState(ctx context.Context, ps State, diags *diag.Diagnostics) { + b, d := ps.GetKey(ctx, "ResourceDatacenterIpLinkAddressingInterfaceIds") + diags.Append(d...) + if diags.HasError() { + return + } + + err := json.Unmarshal(b, o) + if err != nil { + diags.AddError("failed to unmarshal private state", err.Error()) + return + } +} + +func (o *ResourceDatacenterIpLinkAddressingInterfaceIds) SetPrivateState(ctx context.Context, ps State, diags *diag.Diagnostics) { + b, err := json.Marshal(o) + if err != nil { + diags.AddError("failed to marshal private state", err.Error()) + return + } + + diags.Append(ps.SetKey(ctx, "ResourceDatacenterIpLinkAddressingInterfaceIds", b)...) +} + +func epBySystemType(sysType apstra.SystemType, eps []apstra.TwoStageL3ClosSubinterfaceLinkEndpoint, diags *diag.Diagnostics) *apstra.TwoStageL3ClosSubinterfaceLinkEndpoint { + var systemRoles []apstra.SystemRole + + switch sysType { + case apstra.SystemTypeSwitch: + systemRoles = []apstra.SystemRole{apstra.SystemRoleSuperSpine, apstra.SystemRoleSpine, apstra.SystemRoleLeaf, apstra.SystemRoleAccess} + case apstra.SystemTypeServer: + systemRoles = []apstra.SystemRole{apstra.SystemRoleGeneric} + default: + diags.AddError(constants.ErrProviderBug, fmt.Sprintf("unexpected system type %q", sysType)) + return nil + } + + var result *apstra.TwoStageL3ClosSubinterfaceLinkEndpoint + for _, ep := range eps { + ep := ep + if utils.SliceContains(ep.System.Role, systemRoles) { + if result != nil { + diags.AddError( + "Unexpected API response", + fmt.Sprintf("Logical link has multiple endpoints on systems with %q roles", sysType), + ) + return nil + } + + result = &ep + } + } + + if result == nil { + diags.AddError( + "Unexpected API response", + fmt.Sprintf("Logical link has no endpoints on systems with %q roles", sysType), + ) + } + + return result +} diff --git a/apstra/private/state.go b/apstra/private/state.go new file mode 100644 index 00000000..ee99d4c3 --- /dev/null +++ b/apstra/private/state.go @@ -0,0 +1,14 @@ +package private + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" +) + +// State is intended as a stand-in for ProviderData from the not-import-able +// github.com/hashicorp/terraform-plugin-framework/internal/privatestate package. +type State interface { + GetKey(ctx context.Context, key string) ([]byte, diag.Diagnostics) + SetKey(ctx context.Context, key string, value []byte) diag.Diagnostics +} diff --git a/apstra/resource_datacenter_ip_link_addressing.go b/apstra/resource_datacenter_ip_link_addressing.go index d34dfe78..61ea9234 100644 --- a/apstra/resource_datacenter_ip_link_addressing.go +++ b/apstra/resource_datacenter_ip_link_addressing.go @@ -3,16 +3,15 @@ package tfapstra import ( "context" "encoding/binary" - "encoding/json" "fmt" "math" "net" "net/netip" "github.com/Juniper/apstra-go-sdk/apstra" - "github.com/Juniper/apstra-go-sdk/apstra/enum" "github.com/Juniper/terraform-provider-apstra/apstra/blueprint" "github.com/Juniper/terraform-provider-apstra/apstra/constants" + "github.com/Juniper/terraform-provider-apstra/apstra/private" "github.com/Juniper/terraform-provider-apstra/apstra/utils" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -48,14 +47,15 @@ func (o *resourceDatacenterIpLinkAddressing) Schema(_ context.Context, _ resourc } func (o *resourceDatacenterIpLinkAddressing) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + // Extract the configuration. var config blueprint.IpLinkAddressing resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) if resp.Diagnostics.HasError() { return } + // Parse IPv4 and IPv6 addresses found in the configuration. var switchIpv4, switchIpv6, genericIpv4, genericIpv6 netip.Prefix - if !config.SwitchIpv4Addr.IsNull() { switchIpv4, _ = netip.ParsePrefix(config.SwitchIpv4Addr.ValueString()) } @@ -69,6 +69,7 @@ func (o *resourceDatacenterIpLinkAddressing) ValidateConfig(ctx context.Context, genericIpv6, _ = netip.ParsePrefix(config.GenericIpv6Addr.ValueString()) } + // checkPrefix appends an error if the prefix is valid, but is not suitable for use on a point-to-point link checkPrefix := func(prefix netip.Prefix, path path.Path) { if !prefix.IsValid() { return @@ -122,6 +123,7 @@ func (o *resourceDatacenterIpLinkAddressing) ValidateConfig(ctx context.Context, checkPrefix(genericIpv4, path.Root("generic_ipv4_address")) checkPrefix(genericIpv6, path.Root("generic_ipv6_address")) + // checkPrefixPair appends an error if the switch/generic address pairs aren't suitable for use together on a point-to-point link checkPrefixPair := func(switchPrefix, genericPrefix netip.Prefix) { if !switchPrefix.IsValid() || !genericPrefix.IsValid() { return @@ -184,8 +186,8 @@ func (o *resourceDatacenterIpLinkAddressing) Create(ctx context.Context, req res return } - // fetch the link so that we can "compute" (learn) the switch/generic subinterface IDs - link, err := bp.GetSubinterfaceLink(ctx, apstra.ObjectId(plan.LinkId.ValueString())) + // retrieve the link details by ID - we need the IDs of the interface nodes returned by this call + apiData, err := bp.GetSubinterfaceLink(ctx, apstra.ObjectId(plan.LinkId.ValueString())) if err != nil { if utils.IsApstra404(err) { resp.Diagnostics.AddAttributeError( @@ -199,19 +201,31 @@ func (o *resourceDatacenterIpLinkAddressing) Create(ctx context.Context, req res return } - // LoadImmutableData only needs to be done once, and only in the Create() method. - plan.LoadImmutableData(ctx, link, resp) + // extract the interface IDs from the API response + var privateInterfaceIds private.ResourceDatacenterIpLinkAddressingInterfaceIds + privateInterfaceIds.LoadApiData(ctx, apiData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // save initial interface addressing (addressed/local/none) to private state for use in Delete() + var privateInterfaceAddressing private.ResourceDatacenterIpLinkAddressingInterfaceAddressing + privateInterfaceAddressing.LoadApiData(ctx, apiData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + privateInterfaceAddressing.SetPrivateState(ctx, resp.Private, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } // create a subinterface addressing request - request := plan.Request(ctx, &resp.Diagnostics) + request := plan.Request(ctx, privateInterfaceIds, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } - // update the subinterface + // update the subinterfaces err = bp.UpdateSubinterfaces(ctx, request) if err != nil { resp.Diagnostics.AddError("Failed to add subinterface addressing", err.Error()) @@ -251,8 +265,15 @@ func (o *resourceDatacenterIpLinkAddressing) Read(ctx context.Context, req resou return } + // load the API details to a private state object + var privateInterfaceIds private.ResourceDatacenterIpLinkAddressingInterfaceIds + privateInterfaceIds.LoadApiData(ctx, apiData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + // load the state - state.LoadApiData(ctx, apiData, &resp.Diagnostics) + state.LoadApiData(ctx, apiData, privateInterfaceIds, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -288,8 +309,30 @@ func (o *resourceDatacenterIpLinkAddressing) Update(ctx context.Context, req res return } + // retrieve the link details by ID - we need the IDs of the interface nodes returned by this call + apiData, err := bp.GetSubinterfaceLink(ctx, apstra.ObjectId(plan.LinkId.ValueString())) + if err != nil { + if utils.IsApstra404(err) { + resp.Diagnostics.AddAttributeError( + path.Root("link_id"), + "Link not found", + fmt.Sprintf("Link %s not found in blueprint %s", plan.LinkId, plan.BlueprintId), + ) + return + } + resp.Diagnostics.AddError(fmt.Sprintf("failed to fetch link %s info", plan.LinkId), err.Error()) + return + } + + // extract the interface IDs from the API response + var privateInterfaceIds private.ResourceDatacenterIpLinkAddressingInterfaceIds + privateInterfaceIds.LoadApiData(ctx, apiData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + // create a subinterface addressing request - request := plan.Request(ctx, &resp.Diagnostics) + request := plan.Request(ctx, privateInterfaceIds, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -331,59 +374,40 @@ func (o *resourceDatacenterIpLinkAddressing) Delete(ctx context.Context, req res return } - // collect the switch/server v4/v6 addressing types saved to private state during create - pBytes, d := req.Private.GetKey(ctx, "ep_addr_types") - resp.Diagnostics.Append(d...) + // Extract interface IDs stashed away by Create(). + var privateInterfaceAddressing private.ResourceDatacenterIpLinkAddressingInterfaceAddressing + privateInterfaceAddressing.LoadPrivateState(ctx, req.Private, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } - var private struct { - SwitchIpv4AddressType string `json:"switch_ipv4_address_type"` - SwitchIpv6AddressType string `json:"switch_ipv6_address_type"` - GenericIpv4AddressType string `json:"generic_ipv4_address_type"` - GenericIpv6AddressType string `json:"generic_ipv6_address_type"` - } - err = json.Unmarshal(pBytes, &private) + // retrieve the link details by ID - we need the IDs of the interface nodes returned by this call + apiData, err := bp.GetSubinterfaceLink(ctx, apstra.ObjectId(state.LinkId.ValueString())) if err != nil { - resp.Diagnostics.AddError("failed unmarshaling private data", err.Error()) + if utils.IsApstra404(err) { + return // 404 is okay + } + resp.Diagnostics.AddError(fmt.Sprintf("failed to fetch link %s info", state.LinkId), err.Error()) return } - // unpack the private state into apstra objects - var switchIpv4AddressType, genericIpv4AddressType enum.InterfaceNumberingIpv4Type - var switchIpv6AddressType, genericIpv6AddressType enum.InterfaceNumberingIpv6Type - err = utils.ApiStringerFromFriendlyString(&switchIpv4AddressType, private.SwitchIpv4AddressType) - if err != nil { - resp.Diagnostics.AddError("failed to parse private data switch_ipv4_address_type", err.Error()) - return - } - err = utils.ApiStringerFromFriendlyString(&switchIpv6AddressType, private.SwitchIpv6AddressType) - if err != nil { - resp.Diagnostics.AddError("failed to parse private data switch_ipv6_address_type", err.Error()) - return - } - err = utils.ApiStringerFromFriendlyString(&genericIpv4AddressType, private.GenericIpv4AddressType) - if err != nil { - resp.Diagnostics.AddError("failed to parse private data generic_ipv4_address_type", err.Error()) - return - } - err = utils.ApiStringerFromFriendlyString(&genericIpv6AddressType, private.GenericIpv6AddressType) - if err != nil { - resp.Diagnostics.AddError("failed to parse private data generic_ipv6_address_type", err.Error()) + // extract the interface IDs from the API response + var privateInterfaceIds private.ResourceDatacenterIpLinkAddressingInterfaceIds + privateInterfaceIds.LoadApiData(ctx, apiData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } // create a subinterface addressing request which kills off IPv4 and IPv6 - // addressing for each subinterface associated with the link. + // addressing for each subinterface associated with the logical link. request := map[apstra.ObjectId]apstra.TwoStageL3ClosSubinterface{ - apstra.ObjectId(state.SwitchIntfId.ValueString()): { - Ipv4AddrType: switchIpv4AddressType, - Ipv6AddrType: switchIpv6AddressType, + privateInterfaceIds.SwitchInterface: { + Ipv4AddrType: privateInterfaceAddressing.SwitchIpv4, + Ipv6AddrType: privateInterfaceAddressing.SwitchIpv6, }, - apstra.ObjectId(state.GenericIntfId.ValueString()): { - Ipv4AddrType: genericIpv4AddressType, - Ipv6AddrType: genericIpv6AddressType, + privateInterfaceIds.GenericInterface: { + Ipv4AddrType: privateInterfaceAddressing.GenericIpv4, + Ipv6AddrType: privateInterfaceAddressing.GenericIpv6, }, } From 86431a091b2850b8b91665d86c830853f0ab8ab0 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Wed, 4 Dec 2024 18:31:21 -0500 Subject: [PATCH 2/4] fmt --- apstra/blueprint/ip_link_addressing.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apstra/blueprint/ip_link_addressing.go b/apstra/blueprint/ip_link_addressing.go index 651a4eb2..9fe35750 100644 --- a/apstra/blueprint/ip_link_addressing.go +++ b/apstra/blueprint/ip_link_addressing.go @@ -6,9 +6,9 @@ import ( "net" "strings" - "github.com/Juniper/terraform-provider-apstra/apstra/private" "github.com/Juniper/apstra-go-sdk/apstra" "github.com/Juniper/apstra-go-sdk/apstra/enum" + "github.com/Juniper/terraform-provider-apstra/apstra/private" "github.com/Juniper/terraform-provider-apstra/apstra/utils" apstravalidator "github.com/Juniper/terraform-provider-apstra/apstra/validator" "github.com/hashicorp/terraform-plugin-framework-nettypes/cidrtypes" From 9666881844eb1b30dfa05c642f3a8de2d8de84d2 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Wed, 4 Dec 2024 18:32:14 -0500 Subject: [PATCH 3/4] make docs --- docs/resources/datacenter_ip_link_addressing.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/resources/datacenter_ip_link_addressing.md b/docs/resources/datacenter_ip_link_addressing.md index b49781dd..15f7f51b 100644 --- a/docs/resources/datacenter_ip_link_addressing.md +++ b/docs/resources/datacenter_ip_link_addressing.md @@ -130,10 +130,5 @@ resource "apstra_datacenter_ip_link_addressing" "example" { - `switch_ipv6_address` (String) IPv6 address in CIDR notation. - `switch_ipv6_address_type` (String) Allowed values: [`link_local`,`none`,`numbered`] -### Read-Only - -- `generic_interface_id` (String) Apstra graph node ID of the node to which `generic` IP information will be associated. -- `switch_interface_id` (String) Apstra graph node ID of the node to which `switch` IP information will be associated. - From cd2b4f8533eb5792ee1b48897ecdc782363b3957 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Thu, 5 Dec 2024 13:02:41 -0500 Subject: [PATCH 4/4] remove unused struct --- apstra/blueprint/ip_link_addressing.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apstra/blueprint/ip_link_addressing.go b/apstra/blueprint/ip_link_addressing.go index 9fe35750..37ada648 100644 --- a/apstra/blueprint/ip_link_addressing.go +++ b/apstra/blueprint/ip_link_addressing.go @@ -252,8 +252,3 @@ func (o *IpLinkAddressing) LoadApiData(_ context.Context, in *apstra.TwoStageL3C o.GenericIpv6Addr = cidrtypes.NewIPv6PrefixValue(genericEp.Subinterface.Ipv6Addr.String()) } } - -type PrivateInterfaceIds struct { - SwitchInterface apstra.ObjectId `json:"switch_interface"` - GenericInterface apstra.ObjectId `json:"generic_interface"` -}