Skip to content

Commit

Permalink
introduce resource apstra_datacenter_external_gateway (needs tests)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrismarget-j committed Nov 3, 2023
1 parent a78a705 commit 07b0094
Show file tree
Hide file tree
Showing 11 changed files with 1,196 additions and 36 deletions.
20 changes: 10 additions & 10 deletions Third_Party_Code/NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
171 changes: 171 additions & 0 deletions apstra/blueprint/datacenter_external_gateway.go
Original file line number Diff line number Diff line change
@@ -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)
}
137 changes: 137 additions & 0 deletions apstra/custom_types/ipv46_address_type.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 07b0094

Please sign in to comment.