-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
introduce resource apstra_datacenter_external_gateway (needs tests)
- Loading branch information
1 parent
a78a705
commit 07b0094
Showing
11 changed files
with
1,196 additions
and
36 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.