diff --git a/Third_Party_Code/NOTICES.md b/Third_Party_Code/NOTICES.md index e8e33aa5..1e974161 100644 --- a/Third_Party_Code/NOTICES.md +++ b/Third_Party_Code/NOTICES.md @@ -5212,8 +5212,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## golang.org/x/crypto * Name: golang.org/x/crypto -* Version: v0.10.0 -* License: [BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.10.0:LICENSE) +* Version: v0.14.0 +* License: [BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.14.0:LICENSE) ``` Copyright (c) 2009 The Go Authors. All rights reserved. @@ -5249,8 +5249,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## golang.org/x/exp/constraints * Name: golang.org/x/exp/constraints -* Version: v0.0.0-20230713183714-613f0c0eb8a1 -* License: [BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/613f0c0e:LICENSE) +* Version: v0.0.0-20231006140011-7918f672742d +* License: [BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/7918f672:LICENSE) ``` Copyright (c) 2009 The Go Authors. All rights reserved. @@ -5286,8 +5286,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## golang.org/x/net * Name: golang.org/x/net -* Version: v0.11.0 -* License: [BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.11.0:LICENSE) +* Version: v0.16.0 +* License: [BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.16.0:LICENSE) ``` Copyright (c) 2009 The Go Authors. All rights reserved. @@ -5323,8 +5323,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## golang.org/x/sys/unix * Name: golang.org/x/sys/unix -* Version: v0.9.0 -* License: [BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.9.0:LICENSE) +* Version: v0.13.0 +* License: [BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.13.0:LICENSE) ``` Copyright (c) 2009 The Go Authors. All rights reserved. @@ -5360,8 +5360,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## golang.org/x/text * Name: golang.org/x/text -* Version: v0.10.0 -* License: [BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.10.0:LICENSE) +* Version: v0.13.0 +* License: [BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.13.0:LICENSE) ``` Copyright (c) 2009 The Go Authors. All rights reserved. diff --git a/apstra/blueprint/datacenter_external_gateway.go b/apstra/blueprint/datacenter_external_gateway.go new file mode 100644 index 00000000..6e3289df --- /dev/null +++ b/apstra/blueprint/datacenter_external_gateway.go @@ -0,0 +1,171 @@ +package blueprint + +import ( + "context" + "fmt" + "github.com/Juniper/apstra-go-sdk/apstra" + customtypes "github.com/Juniper/terraform-provider-apstra/apstra/custom_types" + "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" + "github.com/hashicorp/terraform-plugin-framework/diag" + 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" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "math" + "net" + "strings" +) + +type DatacenterExternalGateway struct { + Id types.String `tfsdk:"id"` + BlueprintId types.String `tfsdk:"blueprint_id"` + Name types.String `tfsdk:"name"` + IpAddress customtypes.IPv46Address `tfsdk:"ip_address"` + Asn types.Int64 `tfsdk:"asn"` + Ttl types.Int64 `tfsdk:"ttl"` + KeepaliveTime types.Int64 `tfsdk:"keepalive_time"` + HoldTime types.Int64 `tfsdk:"hold_time"` + EvpnRouteTypes types.String `tfsdk:"evpn_route_types"` + LocalGatewayNodes types.Set `tfsdk:"local_gateway_nodes"` +} + +func (o DatacenterExternalGateway) ResourceAttributes() map[string]resourceSchema.Attribute { + return map[string]resourceSchema.Attribute{ + "id": resourceSchema.StringAttribute{ + MarkdownDescription: "Apstra Object ID.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "blueprint_id": resourceSchema.StringAttribute{ + MarkdownDescription: "Apstra ID of the Blueprint in which the External Gateway should be created.", + Required: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "name": resourceSchema.StringAttribute{ + MarkdownDescription: "External Gateway name", + Required: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "ip_address": resourceSchema.StringAttribute{ + MarkdownDescription: "External Gateway IP address", + Required: true, + CustomType: customtypes.IPv46AddressType{}, + }, + "asn": resourceSchema.Int64Attribute{ + MarkdownDescription: "External Gateway AS Number", + Required: true, + Validators: []validator.Int64{int64validator.Between(1, int64(math.MaxUint32))}, + }, + "ttl": resourceSchema.Int64Attribute{ + MarkdownDescription: "BGP Time To Live. Omit to use device defaults.", + Optional: true, + Computed: true, + Validators: []validator.Int64{int64validator.Between(2, int64(math.MaxUint8))}, + }, + "keepalive_time": resourceSchema.Int64Attribute{ + MarkdownDescription: "BGP keepalive time (seconds).", + Optional: true, + Computed: true, + Validators: []validator.Int64{int64validator.Between(1, int64(math.MaxUint16))}, + }, + "hold_time": resourceSchema.Int64Attribute{ + MarkdownDescription: "BGP hold time (seconds).", + Optional: true, + Computed: true, + Validators: []validator.Int64{int64validator.Between(3, int64(math.MaxUint16))}, + }, + "evpn_route_types": resourceSchema.StringAttribute{ + MarkdownDescription: fmt.Sprintf(`EVPN route types. Valid values are: ["%s"]. Default: %q`, + strings.Join(apstra.RemoteGatewayRouteTypesEnum.Values(), `", "`), + apstra.RemoteGatewayRouteTypesAll.Value), + Optional: true, + Computed: true, + Default: stringdefault.StaticString(apstra.RemoteGatewayRouteTypesAll.Value), + Validators: []validator.String{stringvalidator.OneOf(apstra.RemoteGatewayRouteTypesEnum.Values()...)}, + }, + "local_gateway_nodes": resourceSchema.SetAttribute{ + MarkdownDescription: "Set of IDs of switch nodes which will be configured to peer with the External Gateway", + Required: true, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + setvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)), + }, + }, + } +} + +func (o *DatacenterExternalGateway) Request(ctx context.Context, diags *diag.Diagnostics) *apstra.RemoteGatewayData { + routeTypes := apstra.RemoteGatewayRouteTypesEnum.Parse(o.EvpnRouteTypes.ValueString()) + // skipping nil check because input validation should make that impossible + + var localGwNodes []apstra.ObjectId + diags.Append(o.LocalGatewayNodes.ElementsAs(ctx, &localGwNodes, false)...) + if diags.HasError() { + return nil + } + + ttl := uint8(o.Ttl.ValueInt64()) + keepaliveTimer := uint16(o.KeepaliveTime.ValueInt64()) + holdtimeTimer := uint16(o.HoldTime.ValueInt64()) + + return &apstra.RemoteGatewayData{ + RouteTypes: *routeTypes, + LocalGwNodes: localGwNodes, + GwAsn: uint32(o.Asn.ValueInt64()), + GwIp: net.ParseIP(o.IpAddress.ValueString()), // skipping nil check because input + GwName: o.Name.ValueString(), // validation should make that impossible + Ttl: &ttl, + KeepaliveTimer: &keepaliveTimer, + HoldtimeTimer: &holdtimeTimer, + } +} + +func (o *DatacenterExternalGateway) Read(ctx context.Context, bp *apstra.TwoStageL3ClosClient, diags *diag.Diagnostics) { + remoteGateway, err := bp.GetRemoteGateway(ctx, apstra.ObjectId(o.Id.ValueString())) + if err != nil { + diags.AddError("failed to fetch remote gateway", err.Error()) + return + } + + o.loadApiData(ctx, remoteGateway.Data, diags) + if diags.HasError() { + return + } +} + +func (o *DatacenterExternalGateway) loadApiData(_ context.Context, in *apstra.RemoteGatewayData, _ *diag.Diagnostics) { + ttl := types.Int64Null() + if in.Ttl != nil { + ttl = types.Int64Value(int64(*in.Ttl)) + } + + keepaliveTime := types.Int64Null() + if in.KeepaliveTimer != nil { + keepaliveTime = types.Int64Value(int64(*in.KeepaliveTimer)) + } + + holdTime := types.Int64Null() + if in.HoldtimeTimer != nil { + holdTime = types.Int64Value(int64(*in.HoldtimeTimer)) + } + + localGatewayNodes := make([]attr.Value, len(in.LocalGwNodes)) + for i, localGatewayNode := range in.LocalGwNodes { + localGatewayNodes[i] = types.StringValue(localGatewayNode.String()) + } + + o.Name = types.StringValue(in.GwName) + o.IpAddress = customtypes.NewIPv46AddressValue(in.GwIp.String()) + o.Asn = types.Int64Value(int64(in.GwAsn)) + o.Ttl = ttl + o.KeepaliveTime = keepaliveTime + o.HoldTime = holdTime + o.EvpnRouteTypes = types.StringValue(in.RouteTypes.Value) + o.LocalGatewayNodes = types.SetValueMust(types.StringType, localGatewayNodes) +} diff --git a/apstra/custom_types/ipv46_address_type.go b/apstra/custom_types/ipv46_address_type.go new file mode 100644 index 00000000..57f5e2c8 --- /dev/null +++ b/apstra/custom_types/ipv46_address_type.go @@ -0,0 +1,137 @@ +package customtypes + +import ( + "context" + "fmt" + "net/netip" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/attr/xattr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var ( + _ basetypes.StringTypable = (*IPv46AddressType)(nil) + _ xattr.TypeWithValidate = (*IPv46AddressType)(nil) +) + +type IPv46AddressType struct { + basetypes.StringType +} + +// String returns a human readable string of the type name. +func (t IPv46AddressType) String() string { + return "customtypes.IPv46AddressType" +} + +// ValueType returns the Value type. +func (t IPv46AddressType) ValueType(_ context.Context) attr.Value { + return IPv46Address{} +} + +// Equal returns true if the given type is equivalent. +func (t IPv46AddressType) Equal(o attr.Type) bool { + other, ok := o.(IPv46AddressType) + + if !ok { + return false + } + + return t.StringType.Equal(other.StringType) +} + +func (t IPv46AddressType) Validate(_ context.Context, in tftypes.Value, path path.Path) diag.Diagnostics { + var diags diag.Diagnostics + + if in.Type() == nil { + return diags + } + + if !in.Type().Is(tftypes.String) { + err := fmt.Errorf("expected String value, received %T with value: %v", in, in) + diags.AddAttributeError( + path, + "IPv46 Address Type Validation Error", + "An unexpected error was encountered trying to validate an attribute value. This is always an error in the provider. "+ + "Please report the following to the provider developer:\n\n"+err.Error(), + ) + return diags + } + + if !in.IsKnown() || in.IsNull() { + return diags + } + + var valueString string + + if err := in.As(&valueString); err != nil { + diags.AddAttributeError( + path, + "IPv46 Address Type Validation Error", + "An unexpected error was encountered trying to validate an attribute value. This is always an error in the provider. "+ + "Please report the following to the provider developer:\n\n"+err.Error(), + ) + + return diags + } + + ipAddr, err := netip.ParseAddr(valueString) + if err != nil { + diags.AddAttributeError( + path, + "Invalid IPv46 Address String Value", + "A string value was provided that is not valid IPv4 or IPv6 string format.\n\n"+ + "Given Value: "+valueString+"\n"+ + "Error: "+err.Error(), + ) + + return diags + } + + if !ipAddr.IsValid() && (!ipAddr.Is4() && !ipAddr.Is6()) { + diags.AddAttributeError( + path, + "Invalid IPv46 Address String Value", + "A string value was provided that is not valid IPv4 or IPv6 string format.\n\n"+ + "Given Value: "+valueString+"\n", + ) + + return diags + } + + return diags +} + +// ValueFromString returns a StringValuable type given a StringValue. +func (t IPv46AddressType) ValueFromString(_ context.Context, in basetypes.StringValue) (basetypes.StringValuable, diag.Diagnostics) { + return IPv46Address{ + StringValue: in, + }, nil +} + +// ValueFromTerraform returns a Value given a tftypes.Value. This is meant to convert the tftypes.Value into a more convenient Go type +// for the provider to consume the data with. +func (t IPv46AddressType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { + attrValue, err := t.StringType.ValueFromTerraform(ctx, in) + + if err != nil { + return nil, err + } + + stringValue, ok := attrValue.(basetypes.StringValue) + + if !ok { + return nil, fmt.Errorf("unexpected value type of %T", attrValue) + } + + stringValuable, diags := t.ValueFromString(ctx, stringValue) + + if diags.HasError() { + return nil, fmt.Errorf("unexpected error converting StringValue to StringValuable: %v", diags) + } + + return stringValuable, nil +} diff --git a/apstra/custom_types/ipv46_address_type_test.go b/apstra/custom_types/ipv46_address_type_test.go new file mode 100644 index 00000000..25866eff --- /dev/null +++ b/apstra/custom_types/ipv46_address_type_test.go @@ -0,0 +1,185 @@ +package customtypes_test + +import ( + "context" + customtypes "github.com/Juniper/terraform-provider-apstra/apstra/custom_types" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestIPv46AddressTypeValidate(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in tftypes.Value + expectedDiags diag.Diagnostics + }{ + "empty-struct": { + in: tftypes.Value{}, + }, + "null": { + in: tftypes.NewValue(tftypes.String, nil), + }, + "unknown": { + in: tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + }, + "valid IPv4 address - broadcast": { + in: tftypes.NewValue(tftypes.String, "255.255.255.255"), + }, + "valid IPv4 address - loopback": { + in: tftypes.NewValue(tftypes.String, "127.0.0.1"), + }, + "valid IPv4 address - multicast": { + in: tftypes.NewValue(tftypes.String, "224.1.2.3"), + }, + "valid IPv4 address - zeros": { + in: tftypes.NewValue(tftypes.String, "0.0.0.0"), + }, + "valid IPv6 address - unspecified": { + in: tftypes.NewValue(tftypes.String, "::"), + }, + "valid IPv6 address - full": { + in: tftypes.NewValue(tftypes.String, "1:2:3:4:5:6:7:8"), + }, + "valid IPv6 address - trailing double colon": { + in: tftypes.NewValue(tftypes.String, "FF01::"), + }, + "valid IPv6 address - leading double colon": { + in: tftypes.NewValue(tftypes.String, "::8:800:200C:417A"), + }, + "valid IPv6 address - middle double colon": { + in: tftypes.NewValue(tftypes.String, "2001:DB8::8:800:200C:417A"), + }, + "valid IPv6 address - lowercase": { + in: tftypes.NewValue(tftypes.String, "2001:db8::8:800:200c:417a"), + }, + "valid IPv6 address - IPv4-Mapped": { + in: tftypes.NewValue(tftypes.String, "::FFFF:192.168.255.255"), + }, + "valid IPv6 address - IPv4-Compatible": { + in: tftypes.NewValue(tftypes.String, "::127.0.0.1"), + }, + "invalid IPv6 address - invalid colon end": { + in: tftypes.NewValue(tftypes.String, "0:0:0:0:0:0:0:"), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Invalid IPv46 Address String Value", + "A string value was provided that is not valid IPv4 or IPv6 string format.\n\n"+ + "Given Value: 0:0:0:0:0:0:0:\n"+ + "Error: ParseAddr(\"0:0:0:0:0:0:0:\"): colon must be followed by more characters (at \":\")", + ), + }, + }, + "invalid IPv6 address - too many colons": { + in: tftypes.NewValue(tftypes.String, "0:0::1::"), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Invalid IPv46 Address String Value", + "A string value was provided that is not valid IPv4 or IPv6 string format.\n\n"+ + "Given Value: 0:0::1::\n"+ + "Error: ParseAddr(\"0:0::1::\"): multiple :: in address (at \":\")", + ), + }, + }, + "invalid IPv6 address - trailing numbers": { + in: tftypes.NewValue(tftypes.String, "0:0:0:0:0:0:0:1:99"), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Invalid IPv46 Address String Value", + "A string value was provided that is not valid IPv4 or IPv6 string format.\n\n"+ + "Given Value: 0:0:0:0:0:0:0:1:99\n"+ + "Error: ParseAddr(\"0:0:0:0:0:0:0:1:99\"): trailing garbage after address (at \"99\")", + ), + }, + }, + "wrong-value-type": { + in: tftypes.NewValue(tftypes.Number, 123), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "IPv46 Address Type Validation Error", + "An unexpected error was encountered trying to validate an attribute value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "expected String value, received tftypes.Value with value: tftypes.Number<\"123\">", + ), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := customtypes.IPv46AddressType{}.Validate(context.Background(), testCase.in, path.Root("test")) + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (-got, +expected): %s", diff) + } + }) + } +} + +func TestIPv46AddressTypeValueFromTerraform(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in tftypes.Value + expectation attr.Value + expectedErr string + }{ + "true": { + in: tftypes.NewValue(tftypes.String, "FF01::101"), + expectation: customtypes.NewIPv46AddressValue("FF01::101"), + }, + "unknown": { + in: tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + expectation: customtypes.NewIPv46AddressUnknown(), + }, + "null": { + in: tftypes.NewValue(tftypes.String, nil), + expectation: customtypes.NewIPv46AddressNull(), + }, + "wrongType": { + in: tftypes.NewValue(tftypes.Number, 123), + expectedErr: "can't unmarshal tftypes.Number into *string, expected string", + }, + } + for name, testCase := range testCases { + name, testCase := name, testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + ctx := context.Background() + + got, err := customtypes.IPv46AddressType{}.ValueFromTerraform(ctx, testCase.in) + if err != nil { + if testCase.expectedErr == "" { + t.Fatalf("Unexpected error: %s", err) + } + if testCase.expectedErr != err.Error() { + t.Fatalf("Expected error to be %q, got %q", testCase.expectedErr, err.Error()) + } + return + } + if err == nil && testCase.expectedErr != "" { + t.Fatalf("Expected error to be %q, didn't get an error", testCase.expectedErr) + } + if !got.Equal(testCase.expectation) { + t.Errorf("Expected %+v, got %+v", testCase.expectation, got) + } + if testCase.expectation.IsNull() != testCase.in.IsNull() { + t.Errorf("Expected null-ness match: expected %t, got %t", testCase.expectation.IsNull(), testCase.in.IsNull()) + } + if testCase.expectation.IsUnknown() != !testCase.in.IsKnown() { + t.Errorf("Expected unknown-ness match: expected %t, got %t", testCase.expectation.IsUnknown(), !testCase.in.IsKnown()) + } + }) + } +} diff --git a/apstra/custom_types/ipv46_address_value.go b/apstra/custom_types/ipv46_address_value.go new file mode 100644 index 00000000..17be2fb8 --- /dev/null +++ b/apstra/custom_types/ipv46_address_value.go @@ -0,0 +1,102 @@ +package customtypes + +import ( + "context" + "fmt" + "net/netip" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +var ( + _ basetypes.StringValuable = (*IPv46Address)(nil) + _ basetypes.StringValuableWithSemanticEquals = (*IPv46Address)(nil) +) + +type IPv46Address struct { + basetypes.StringValue +} + +func (v IPv46Address) Type(_ context.Context) attr.Type { + return IPv46AddressType{} +} + +func (v IPv46Address) Equal(o attr.Value) bool { + other, ok := o.(IPv46Address) + + if !ok { + return false + } + + return v.StringValue.Equal(other.StringValue) +} + +func (v IPv46Address) StringSemanticEquals(_ context.Context, newValuable basetypes.StringValuable) (bool, diag.Diagnostics) { + var diags diag.Diagnostics + + newValue, ok := newValuable.(IPv46Address) + if !ok { + diags.AddError( + "Semantic Equality Check Error", + "An unexpected value type was received while performing semantic equality checks. "+ + "Please report this to the provider developers.\n\n"+ + "Expected Value Type: "+fmt.Sprintf("%T", v)+"\n"+ + "Got Value Type: "+fmt.Sprintf("%T", newValuable), + ) + + return false, diags + } + + newIpAddr, _ := netip.ParseAddr(newValue.ValueString()) + currentIpAddr, _ := netip.ParseAddr(v.ValueString()) + + return currentIpAddr == newIpAddr, diags +} + +func (v IPv46Address) ValueIPv46Address() (netip.Addr, diag.Diagnostics) { + var diags diag.Diagnostics + + if v.IsNull() { + diags.Append(diag.NewErrorDiagnostic("IPv46Address ValueIPv46Address Error", "address string value is null")) + return netip.Addr{}, diags + } + + if v.IsUnknown() { + diags.Append(diag.NewErrorDiagnostic("IPv46Address ValueIPv46Address Error", "address string value is unknown")) + return netip.Addr{}, diags + } + + ipv46Addr, err := netip.ParseAddr(v.ValueString()) + if err != nil { + diags.Append(diag.NewErrorDiagnostic("IPv46Address ValueIPv46Address Error", err.Error())) + return netip.Addr{}, diags + } + + return ipv46Addr, nil +} + +func NewIPv46AddressNull() IPv46Address { + return IPv46Address{ + StringValue: basetypes.NewStringNull(), + } +} + +func NewIPv46AddressUnknown() IPv46Address { + return IPv46Address{ + StringValue: basetypes.NewStringUnknown(), + } +} + +func NewIPv46AddressValue(value string) IPv46Address { + return IPv46Address{ + StringValue: basetypes.NewStringValue(value), + } +} + +func NewIPv46AddressPointerValue(value *string) IPv46Address { + return IPv46Address{ + StringValue: basetypes.NewStringPointerValue(value), + } +} diff --git a/apstra/custom_types/ipv46_address_value_test.go b/apstra/custom_types/ipv46_address_value_test.go new file mode 100644 index 00000000..c7fc1820 --- /dev/null +++ b/apstra/custom_types/ipv46_address_value_test.go @@ -0,0 +1,213 @@ +package customtypes_test + +import ( + "context" + "net/netip" + "testing" + + "github.com/Juniper/terraform-provider-apstra/apstra/custom_types" + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework-nettypes/iptypes" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestIPv46AddressStringSemanticEquals(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + currentIpAddr customtypes.IPv46Address + givenIpAddr basetypes.StringValuable + expectedMatch bool + expectedDiags diag.Diagnostics + }{ + "not equal - IPv6 address mismatch": { + currentIpAddr: customtypes.NewIPv46AddressValue("0:0:0:0:0:0:0:0"), + givenIpAddr: customtypes.NewIPv46AddressValue("0:0:0:0:0:0:0:1"), + expectedMatch: false, + }, + "not equal - IPv6 address compressed mismatch": { + currentIpAddr: customtypes.NewIPv46AddressValue("FF01::"), + givenIpAddr: customtypes.NewIPv46AddressValue("FF01::1"), + expectedMatch: false, + }, + "not equal - IPv4-Mapped IPv6 address mismatch": { + currentIpAddr: customtypes.NewIPv46AddressValue("::FFFF:192.168.255.255"), + givenIpAddr: customtypes.NewIPv46AddressValue("::FFFF:192.168.255.254"), + expectedMatch: false, + }, + "semantically equal - byte-for-byte match": { + currentIpAddr: customtypes.NewIPv46AddressValue("0:0:0:0:0:0:0:0"), + givenIpAddr: customtypes.NewIPv46AddressValue("0:0:0:0:0:0:0:0"), + expectedMatch: true, + }, + "semantically equal - case insensitive": { + currentIpAddr: customtypes.NewIPv46AddressValue("2001:0DB8:0000:0000:0008:0800:0200C:417A"), + givenIpAddr: customtypes.NewIPv46AddressValue("2001:0db8:0000:0000:0008:0800:0200c:417a"), + expectedMatch: true, + }, + "semantically equal - IPv4-Mapped byte-for-byte match": { + currentIpAddr: customtypes.NewIPv46AddressValue("::FFFF:192.168.255.255"), + givenIpAddr: customtypes.NewIPv46AddressValue("::FFFF:192.168.255.255"), + expectedMatch: true, + }, + "semantically equal - compressed all zeroes match": { + currentIpAddr: customtypes.NewIPv46AddressValue("0:0:0:0:0:0:0:0"), + givenIpAddr: customtypes.NewIPv46AddressValue("::"), + expectedMatch: true, + }, + "semantically equal - compressed all leading zeroes match": { + currentIpAddr: customtypes.NewIPv46AddressValue("2001:0DB8:0000:0000:0008:0800:0200C:417A"), + givenIpAddr: customtypes.NewIPv46AddressValue("2001:DB8::8:800:200C:417A"), + expectedMatch: true, + }, + "semantically equal - start compressed match": { + currentIpAddr: customtypes.NewIPv46AddressValue("::101"), + givenIpAddr: customtypes.NewIPv46AddressValue("0:0:0:0:0:0:0:101"), + expectedMatch: true, + }, + "semantically equal - middle compressed match": { + currentIpAddr: customtypes.NewIPv46AddressValue("2001:DB8::8:800:200C:417A"), + givenIpAddr: customtypes.NewIPv46AddressValue("2001:DB8:0:0:8:800:200C:417A"), + expectedMatch: true, + }, + "semantically equal - end compressed match": { + currentIpAddr: customtypes.NewIPv46AddressValue("FF01:0:0:0:0:0:0:0"), + givenIpAddr: customtypes.NewIPv46AddressValue("FF01::"), + expectedMatch: true, + }, + "semantically equal - IPv4-Mapped compressed match": { + currentIpAddr: customtypes.NewIPv46AddressValue("0:0:0:0:0:FFFF:192.168.255.255"), + givenIpAddr: customtypes.NewIPv46AddressValue("::FFFF:192.168.255.255"), + expectedMatch: true, + }, + "error - not given IPv6Address IPv6 value": { + currentIpAddr: customtypes.NewIPv46AddressValue("0:0:0:0:0:0:0:0"), + givenIpAddr: basetypes.NewStringValue("0:0:0:0:0:0:0:0"), + expectedMatch: false, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Semantic Equality Check Error", + "An unexpected value type was received while performing semantic equality checks. "+ + "Please report this to the provider developers.\n\n"+ + "Expected Value Type: customtypes.IPv46Address\n"+ + "Got Value Type: basetypes.StringValue", + ), + }, + }, + } + for name, testCase := range testCases { + name, testCase := name, testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + match, diags := testCase.currentIpAddr.StringSemanticEquals(context.Background(), testCase.givenIpAddr) + + if testCase.expectedMatch != match { + t.Errorf("Expected StringSemanticEquals to return: %t, but got: %t", testCase.expectedMatch, match) + } + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (-got, +expected): %s", diff) + } + }) + } +} + +func TestIPv46AddressValueIPv4Address(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + ipValue iptypes.IPv4Address + expectedIpAddr netip.Addr + expectedDiags diag.Diagnostics + }{ + "IPv4 address value is null ": { + ipValue: iptypes.NewIPv4AddressNull(), + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "IPv4Address ValueIPv4Address Error", + "IPv4 address string value is null", + ), + }, + }, + "IPv4 address value is unknown ": { + ipValue: iptypes.NewIPv4AddressUnknown(), + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "IPv4Address ValueIPv4Address Error", + "IPv4 address string value is unknown", + ), + }, + }, + "valid IPv4 address ": { + ipValue: iptypes.NewIPv4AddressValue("192.0.2.1"), + expectedIpAddr: netip.MustParseAddr("192.0.2.1"), + }, + } + for name, testCase := range testCases { + name, testCase := name, testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + ipAddr, diags := testCase.ipValue.ValueIPv4Address() + + if ipAddr != testCase.expectedIpAddr { + t.Errorf("Unexpected difference in netip.Addr, got: %s, expected: %s", ipAddr, testCase.expectedIpAddr) + } + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (-got, +expected): %s", diff) + } + }) + } +} + +func TestIPv6AddressValueIPv6Address(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + ipValue iptypes.IPv6Address + expectedIpAddr netip.Addr + expectedDiags diag.Diagnostics + }{ + "IPv6 address value is null ": { + ipValue: iptypes.NewIPv6AddressNull(), + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "IPv6Address ValueIPv6Address Error", + "IPv6 address string value is null", + ), + }, + }, + "IPv6 address value is unknown ": { + ipValue: iptypes.NewIPv6AddressUnknown(), + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "IPv6Address ValueIPv6Address Error", + "IPv6 address string value is unknown", + ), + }, + }, + "valid IPv6 address ": { + ipValue: iptypes.NewIPv6AddressValue("2001:DB8::8:800:200C:417A"), + expectedIpAddr: netip.MustParseAddr("2001:DB8::8:800:200C:417A"), + }, + } + for name, testCase := range testCases { + name, testCase := name, testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + ipAddr, diags := testCase.ipValue.ValueIPv6Address() + + if ipAddr != testCase.expectedIpAddr { + t.Errorf("Unexpected difference in netip.Addr, got: %s, expected: %s", ipAddr, testCase.expectedIpAddr) + } + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (-got, +expected): %s", diff) + } + }) + } +} diff --git a/apstra/helpers.go b/apstra/helpers.go index 3657e05c..5bd53b91 100644 --- a/apstra/helpers.go +++ b/apstra/helpers.go @@ -1,9 +1,12 @@ package tfapstra import ( + "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/diag" "golang.org/x/exp/constraints" "math/rand" + "strings" "unsafe" ) @@ -38,3 +41,29 @@ func FillWithRandomIntegers[A constraints.Integer](a []A) { a[i] = A(rand.Uint64()) } } + +// SplitImportId splits string 'in' into len(fieldNames) strings using the first character as +// a separator for the fields. fieldNames is used mainly for its length. The values in fieldNames +// are ignored unless there's a need to print an error message. +func SplitImportId(_ context.Context, in string, fieldNames []string, diags *diag.Diagnostics) []string { + if len(in) < 2 { + diags.AddError("invalid import ID", "import ID minimum length is 2") + return nil + } + + sep := in[:1] + parts := strings.Split(in[:], sep)[1:] + + if len(fieldNames) != len(parts) { + form := "<" + strings.Join(fieldNames, "><") + ">" + diags.AddError( + fmt.Sprintf("cannot parse import ID: %q", in), + fmt.Sprintf("ID string for resource import must take this form:\n\n"+ + " %s\n\n"+ + "where is any single character not found in any of the delimited fields. "+ + "Expected %d parts after splitting on '%s', got %d parts", form, len(fieldNames), sep, len(parts))) + return nil + } + + return parts +} diff --git a/apstra/helpers_test.go b/apstra/helpers_test.go index 370d04e3..01f17745 100644 --- a/apstra/helpers_test.go +++ b/apstra/helpers_test.go @@ -1,7 +1,10 @@ package tfapstra import ( + "context" "github.com/Juniper/terraform-provider-apstra/apstra/utils" + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" "golang.org/x/exp/constraints" "testing" ) @@ -167,3 +170,92 @@ func TestRandomIntegers(t *testing.T) { t.Fail() } } + +func TestSplitImportId(t *testing.T) { + ctx := context.Background() + t.Parallel() + + type testCase struct { + in string + fields []string + expected []string + expectedDiags diag.Diagnostics + } + + testCases := map[string]testCase{ + "|1": { + in: "|foo", + fields: []string{"foo"}, + expected: []string{"foo"}, + }, + ".2": { + in: ".foo.bar", + fields: []string{"foo", "bar"}, + expected: []string{"foo", "bar"}, + }, + "nil": { + in: "", + fields: []string{}, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "invalid import ID", + "import ID minimum length is 2", + ), + }, + }, + "empty": { + in: "", + fields: []string{}, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "invalid import ID", + "import ID minimum length is 2", + ), + }, + }, + "too short": { + in: ".", + fields: []string{}, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "invalid import ID", + "import ID minimum length is 2", + ), + }, + }, + "fail embedded separator": { + in: ".abc.def.ghi", + fields: []string{"abc", "defghi"}, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + `cannot parse import ID: ".abc.def.ghi"`, + "ID string for resource import must take this form:\n\n"+ + " \n\n"+ + "where is any single character not found in any of the delimited fields. "+ + "Expected 2 parts after splitting on '.', got 3 parts", + ), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + var diags diag.Diagnostics + + t.Run(name, func(t *testing.T) { + t.Parallel() + + parts := SplitImportId(ctx, testCase.in, testCase.fields, &diags) + + if diff := cmp.Diff(testCase.expectedDiags, diags); diff != "" { + t.Fatalf("Unexpected diagnostics (-expected ,+got): %s", diff) + } + + if len(testCase.expectedDiags) == 0 { + if diff := cmp.Diff(testCase.expected, parts); diff != "" { + t.Fatalf("Unexpected result (-expected ,+got): %s", diff) + } + } + }) + } +} diff --git a/apstra/resource_datacenter_external_gateway.go b/apstra/resource_datacenter_external_gateway.go new file mode 100644 index 00000000..19361e2f --- /dev/null +++ b/apstra/resource_datacenter_external_gateway.go @@ -0,0 +1,230 @@ +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 = &resourceDatacenterExternalGateway{} +var _ resource.ResourceWithImportState = &resourceDatacenterExternalGateway{} + +type resourceDatacenterExternalGateway struct { + client *apstra.Client + lockFunc func(context.Context, string) error +} + +func (o *resourceDatacenterExternalGateway) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_datacenter_external_gateway" +} + +func (o *resourceDatacenterExternalGateway) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + o.client = ResourceGetClient(ctx, req, resp) + o.lockFunc = ResourceGetBlueprintLockFunc(ctx, req, resp) +} + +func (o *resourceDatacenterExternalGateway) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: docCategoryDatacenter + "This resource creates an External Gateway within a Blueprint. " + + "Prior to Apstra 4.2 these were called \"EVPN Remote Gateway\"", + Attributes: blueprint.DatacenterExternalGateway{}.ResourceAttributes(), + } +} + +func (o *resourceDatacenterExternalGateway) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // req.ID takes the form: "blueprint_id:external_gateway_id" + fieldNames := []string{ + "blueprint_id", + "external_gateway_id", + } + + // split the supplied ID into the required fields + parts := SplitImportId(ctx, req.ID, fieldNames, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // create a state object preloaded with the critical details we need in advance + state := blueprint.DatacenterExternalGateway{ + BlueprintId: types.StringValue(parts[0]), + Id: types.StringValue(parts[1]), + } + + // create a client for the datacenter reference design + bp, err := o.client.NewTwoStageL3ClosClient(ctx, apstra.ObjectId(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(fmt.Sprintf(blueprint.ErrDCBlueprintCreate, state.BlueprintId), err.Error()) + return + } + + state.Read(ctx, bp, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (o *resourceDatacenterExternalGateway) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan. + var plan blueprint.DatacenterExternalGateway + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // create a client for the datacenter reference design + bp, err := o.client.NewTwoStageL3ClosClient(ctx, apstra.ObjectId(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(fmt.Sprintf(blueprint.ErrDCBlueprintCreate, plan.BlueprintId), 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 + } + + id, err := bp.CreateRemoteGateway(ctx, request) + if err != nil { + resp.Diagnostics.AddError("error creating external gateway", err.Error()) + return + } + + plan.Id = types.StringValue(id.String()) + plan.Read(ctx, bp, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (o *resourceDatacenterExternalGateway) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Retrieve values from state. + var state blueprint.DatacenterExternalGateway + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + // Create a blueprint client + bp, err := o.client.NewTwoStageL3ClosClient(ctx, apstra.ObjectId(state.BlueprintId.ValueString())) + if err != nil { + if utils.IsApstra404(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError(fmt.Sprintf(blueprint.ErrDCBlueprintCreate, state.BlueprintId), err.Error()) + return + } + + state.Read(ctx, bp, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (o *resourceDatacenterExternalGateway) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Retrieve values from plan. + var plan blueprint.DatacenterExternalGateway + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // Create a blueprint client + bp, err := o.client.NewTwoStageL3ClosClient(ctx, apstra.ObjectId(plan.BlueprintId.ValueString())) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf(blueprint.ErrDCBlueprintCreate, plan.BlueprintId), 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 + } + + err = bp.UpdateRemoteGateway(ctx, apstra.ObjectId(plan.Id.ValueString()), request) + if err != nil { + resp.Diagnostics.AddError("error updating remote gateway", err.Error()) + return + } + + plan.Read(ctx, bp, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (o *resourceDatacenterExternalGateway) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state. + var state blueprint.DatacenterExternalGateway + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + // Create a client for the datacenter reference design + bp, err := o.client.NewTwoStageL3ClosClient(ctx, apstra.ObjectId(state.BlueprintId.ValueString())) + if err != nil { + if utils.IsApstra404(err) { + return // 404 is okay + } + resp.Diagnostics.AddError(fmt.Sprintf(blueprint.ErrDCBlueprintCreate, state.BlueprintId), 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 the remote gateway + err = bp.DeleteRemoteGateway(ctx, apstra.ObjectId(state.Id.ValueString())) + if err != nil { + if utils.IsApstra404(err) { + return // 404 is okay + } + resp.Diagnostics.AddError("error deleting remote gateway", err.Error()) + } +} diff --git a/go.mod b/go.mod index a5c59545..ea45a70e 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,9 @@ go 1.20 require ( github.com/IBM/netaddr v1.5.0 - github.com/Juniper/apstra-go-sdk v0.0.0-20231024231608-5d733c01a440 + github.com/Juniper/apstra-go-sdk v0.0.0-20231102202421-a97e1d145d70 github.com/chrismarget-j/go-licenses v0.0.0-20230424163011-d60082a506e0 + github.com/google/go-cmp v0.5.9 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/terraform-plugin-docs v0.13.0 github.com/hashicorp/terraform-plugin-framework v1.3.3 @@ -14,7 +15,7 @@ require ( github.com/hashicorp/terraform-plugin-go v0.18.0 github.com/hashicorp/terraform-plugin-testing v1.2.0 github.com/mitchellh/go-homedir v1.1.0 - golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 + golang.org/x/exp v0.0.0-20231006140011-7918f672742d honnef.co/go/tools v0.4.3 ) @@ -33,7 +34,6 @@ require ( github.com/go-logr/logr v1.2.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-licenses v1.6.0 // indirect github.com/google/licenseclassifier v0.0.0-20210722185704-3043a050f148 // indirect github.com/google/uuid v1.3.0 // indirect @@ -85,13 +85,13 @@ require ( github.com/xanzy/ssh-agent v0.3.0 // indirect github.com/zclconf/go-cty v1.13.1 // indirect go.opencensus.io v0.23.0 // indirect - golang.org/x/crypto v0.10.0 // indirect + golang.org/x/crypto v0.14.0 // indirect golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a // indirect - golang.org/x/mod v0.11.0 // indirect - golang.org/x/net v0.11.0 // indirect - golang.org/x/sys v0.9.0 // indirect - golang.org/x/text v0.10.0 // indirect - golang.org/x/tools v0.6.0 // indirect + golang.org/x/mod v0.13.0 // indirect + golang.org/x/net v0.16.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + golang.org/x/tools v0.14.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/grpc v1.56.1 // indirect diff --git a/go.sum b/go.sum index 5cdd2427..51710137 100644 --- a/go.sum +++ b/go.sum @@ -63,8 +63,8 @@ github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 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-20231024231608-5d733c01a440 h1:zAE3k3T4EhBCJpoNrCWbv3Sow92+11O1vAhF1R/wsD0= -github.com/Juniper/apstra-go-sdk v0.0.0-20231024231608-5d733c01a440/go.mod h1:It+5cLgOj77K+s+7m5Nsnbms3Tu/4ObqDuBjsTR2Oh0= +github.com/Juniper/apstra-go-sdk v0.0.0-20231102202421-a97e1d145d70 h1:nUEmp2qfDjHt05muO+73JSj08D2DFQVooXPH+fZz9ag= +github.com/Juniper/apstra-go-sdk v0.0.0-20231102202421-a97e1d145d70/go.mod h1:BB8X+PSov7CoCIAQ5P1z8bHc7DwtDN51fWCLJ93oeHY= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= @@ -486,8 +486,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= -golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= -golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -498,8 +498,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a h1:Jw5wfR+h9mnIYH+OtGT2im5wV1YGGDora5vTv/aa5bE= golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= @@ -531,8 +531,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= -golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -581,8 +581,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= -golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= +golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -618,8 +618,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -693,15 +693,15 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -715,8 +715,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -774,8 +774,9 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=