Skip to content

Commit

Permalink
introduce external gateway data source
Browse files Browse the repository at this point in the history
  • Loading branch information
chrismarget-j committed Nov 4, 2023
1 parent 96157d8 commit af2f51d
Show file tree
Hide file tree
Showing 5 changed files with 319 additions and 17 deletions.
96 changes: 92 additions & 4 deletions apstra/blueprint/datacenter_external_gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import (
"github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
dataSourceSchema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
resourceSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
Expand Down Expand Up @@ -104,7 +106,70 @@ func (o DatacenterExternalGateway) ResourceAttributes() map[string]resourceSchem
MarkdownDescription: "BGP TCP authentication password",
Optional: true,
Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
//Sensitive: true,
Sensitive: true,
},
}
}

func (o DatacenterExternalGateway) DataSourceAttributes() map[string]dataSourceSchema.Attribute {
return map[string]dataSourceSchema.Attribute{
"id": dataSourceSchema.StringAttribute{
MarkdownDescription: "Apstra Object ID.",
Optional: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
stringvalidator.ExactlyOneOf(path.Expressions{
path.MatchRelative(),
path.MatchRoot("name"),
}...),
},
},
"blueprint_id": dataSourceSchema.StringAttribute{
MarkdownDescription: "Apstra ID of the Blueprint in which the External Gateway should be created.",
Required: true,
Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
},
"name": dataSourceSchema.StringAttribute{
MarkdownDescription: "External Gateway name",
Optional: true,
Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
},
"ip_address": dataSourceSchema.StringAttribute{
MarkdownDescription: "External Gateway IP address",
Computed: true,
CustomType: iptypes.IPv4AddressType{},
},
"asn": dataSourceSchema.Int64Attribute{
MarkdownDescription: "External Gateway AS Number",
Computed: true,
},
"ttl": dataSourceSchema.Int64Attribute{
MarkdownDescription: "BGP Time To Live. Omit to use device defaults.",
Computed: true,
},
"keepalive_time": dataSourceSchema.Int64Attribute{
MarkdownDescription: "BGP keepalive time (seconds).",
Computed: true,
},
"hold_time": dataSourceSchema.Int64Attribute{
MarkdownDescription: "BGP hold time (seconds).",
Computed: true,
},
"evpn_route_types": dataSourceSchema.StringAttribute{
MarkdownDescription: fmt.Sprintf(`EVPN route types. Valid values are: ["%s"]. Default: %q`,
strings.Join(apstra.RemoteGatewayRouteTypesEnum.Values(), `", "`),
apstra.RemoteGatewayRouteTypesAll.Value),
Computed: true,
},
"local_gateway_nodes": dataSourceSchema.SetAttribute{
MarkdownDescription: "Set of IDs of switch nodes which will be configured to peer with the External Gateway",
Computed: true,
ElementType: types.StringType,
},
"password": dataSourceSchema.StringAttribute{
MarkdownDescription: "BGP TCP authentication password",
Computed: true,
Sensitive: true,
},
}
}
Expand Down Expand Up @@ -157,13 +222,36 @@ func (o *DatacenterExternalGateway) Request(ctx context.Context, diags *diag.Dia
}

func (o *DatacenterExternalGateway) Read(ctx context.Context, bp *apstra.TwoStageL3ClosClient, diags *diag.Diagnostics) {
remoteGateway, err := bp.GetRemoteGateway(ctx, apstra.ObjectId(o.Id.ValueString()))
var err error
var api *apstra.RemoteGateway

switch {
case !o.Id.IsNull():
api, err = bp.GetRemoteGateway(ctx, apstra.ObjectId(o.Id.ValueString()))
if utils.IsApstra404(err) {
diags.AddAttributeError(
path.Root("id"),
"External Gateway not found",
fmt.Sprintf("External Gateway with ID %s not found", o.Id))
return
}
case !o.Name.IsNull():
api, err = bp.GetRemoteGatewayByName(ctx, o.Name.ValueString())
if utils.IsApstra404(err) {
diags.AddAttributeError(
path.Root("name"),
"External Gateway not found",
fmt.Sprintf("External Gateway with Name %s not found", o.Name))
return
}
o.Id = types.StringValue(api.Id.String())
}
if err != nil {
diags.AddError("failed to fetch remote gateway", err.Error())
diags.AddError("Failed reading Remote Gateway", err.Error())
return
}

o.loadApiData(ctx, remoteGateway.Data, diags)
o.loadApiData(ctx, api.Data, diags)
if diags.HasError() {
return
}
Expand Down
61 changes: 61 additions & 0 deletions apstra/data_source_datacenter_external_gateway.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package tfapstra

import (
"context"
"fmt"
"github.com/Juniper/apstra-go-sdk/apstra"
"github.com/Juniper/terraform-provider-apstra/apstra/blueprint"
"github.com/Juniper/terraform-provider-apstra/apstra/utils"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
)

var _ datasource.DataSourceWithConfigure = &dataSourceDatacenterExternalGateway{}

type dataSourceDatacenterExternalGateway struct {
client *apstra.Client
}

func (o *dataSourceDatacenterExternalGateway) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_datacenter_external_gateway"
}

func (o *dataSourceDatacenterExternalGateway) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
o.client = DataSourceGetClient(ctx, req, resp)
}

func (o *dataSourceDatacenterExternalGateway) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: docCategoryDatacenter + "This resource returns details of a Routing Zone within a Datacenter Blueprint.\n\n" +
"At least one optional attribute is required.",
Attributes: blueprint.DatacenterExternalGateway{}.DataSourceAttributes(),
}
}

func (o *dataSourceDatacenterExternalGateway) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
// Retrieve values from config.
var config blueprint.DatacenterExternalGateway
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}

bp, err := o.client.NewTwoStageL3ClosClient(ctx, apstra.ObjectId(config.BlueprintId.ValueString()))
if err != nil {
if utils.IsApstra404(err) {
resp.Diagnostics.AddError(fmt.Sprintf("blueprint %s not found",
config.BlueprintId), err.Error())
return
}
resp.Diagnostics.AddError(fmt.Sprintf(blueprint.ErrDCBlueprintCreate, config.BlueprintId), err.Error())
return
}

config.Read(ctx, bp, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}

// set state
resp.Diagnostics.Append(resp.State.Set(ctx, &config)...)
}
150 changes: 150 additions & 0 deletions apstra/data_source_datacenter_external_gateway_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package tfapstra_test

import (
"context"
"errors"
"fmt"
"github.com/Juniper/apstra-go-sdk/apstra"
testutils "github.com/Juniper/terraform-provider-apstra/apstra/test_utils"
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"math/rand"
"testing"
)

const (
dataSourceDataCenterExternalGatewayByIdHCL = `
data "apstra_datacenter_external_gateway" "test" {
blueprint_id = "%s"
id = "%s"
}
`

dataSourceDataCenterExternalGatewayByNameHCL = `
data "apstra_datacenter_external_gateway" "test" {
blueprint_id = "%s"
name = "%s"
}
`
)

func TestDatacenterExternalGateway(t *testing.T) {
ctx := context.Background()

bp, bpDelete, err := testutils.BlueprintC(ctx)
if err != nil {
t.Fatal(errors.Join(err, bpDelete(ctx)))
}
defer func() {
err = bpDelete(ctx)
if err != nil {
t.Error(err)
}
}()

leafIdStrings := systemIds(ctx, t, bp, "leaf")
leafIds := make([]apstra.ObjectId, len(leafIdStrings))
for i, id := range leafIdStrings {
leafIds[i] = apstra.ObjectId(id)
}

ttl := uint8(5)
keepalive := uint16(6)
hold := uint16(18)
password := "big secret"

rgConfigs := []apstra.RemoteGatewayData{
{
RouteTypes: apstra.RemoteGatewayRouteTypesAll,
LocalGwNodes: leafIds,
GwAsn: rand.Uint32(),
GwIp: randIpAddressMust(t, "10.0.0.0/8"),
GwName: acctest.RandString(5),
Ttl: &ttl,
KeepaliveTimer: &keepalive,
HoldtimeTimer: &hold,
Password: &password,
},
{
RouteTypes: apstra.RemoteGatewayRouteTypesFiveOnly,
LocalGwNodes: leafIds,
GwAsn: rand.Uint32(),
GwIp: randIpAddressMust(t, "10.0.0.0/8"),
GwName: acctest.RandString(5),
Ttl: &ttl,
KeepaliveTimer: &keepalive,
HoldtimeTimer: &hold,
Password: &password,
},
}

rgIds := make([]apstra.ObjectId, len(rgConfigs))
for i, rgConfig := range rgConfigs {
rgIds[i], err = bp.CreateRemoteGateway(ctx, &rgConfig)
if err != nil {
t.Fatal(err)
}
}

rgs := make([]apstra.RemoteGateway, len(rgIds))
for i, rgData := range rgConfigs {
rgData := rgData
rgs[i] = apstra.RemoteGateway{
Id: rgIds[i],
Data: &rgData,
}
}

genTestCheckFuncs := func(rg apstra.RemoteGateway) []resource.TestCheckFunc {
result := []resource.TestCheckFunc{
resource.TestCheckResourceAttr("data.apstra_datacenter_external_gateway.test", "id", rg.Id.String()),
resource.TestCheckResourceAttr("data.apstra_datacenter_external_gateway.test", "blueprint_id", bp.Id().String()),
resource.TestCheckResourceAttr("data.apstra_datacenter_external_gateway.test", "name", rg.Data.GwName),
resource.TestCheckResourceAttr("data.apstra_datacenter_external_gateway.test", "ip_address", rg.Data.GwIp.String()),
resource.TestCheckResourceAttr("data.apstra_datacenter_external_gateway.test", "asn", fmt.Sprintf("%d", rg.Data.GwAsn)),
resource.TestCheckResourceAttr("data.apstra_datacenter_external_gateway.test", "ttl", fmt.Sprintf("%d", *rg.Data.Ttl)),
resource.TestCheckResourceAttr("data.apstra_datacenter_external_gateway.test", "keepalive_time", fmt.Sprintf("%d", *rg.Data.KeepaliveTimer)),
resource.TestCheckResourceAttr("data.apstra_datacenter_external_gateway.test", "hold_time", fmt.Sprintf("%d", *rg.Data.HoldtimeTimer)),
resource.TestCheckResourceAttr("data.apstra_datacenter_external_gateway.test", "evpn_route_types", rg.Data.RouteTypes.Value),
resource.TestCheckResourceAttr("data.apstra_datacenter_external_gateway.test", "local_gateway_nodes.#", fmt.Sprintf("%d", len(rg.Data.LocalGwNodes))),
resource.TestCheckResourceAttr("data.apstra_datacenter_external_gateway.test", "password", *rg.Data.Password),
}

for _, id := range leafIdStrings {
tcf := resource.TestCheckTypeSetElemAttr(
"data.apstra_datacenter_external_gateway.test",
"local_gateway_nodes.*", id,
)
result = append(result, tcf)
}

return result
}

testCheckFuncsByRgId := make(map[apstra.ObjectId][]resource.TestCheckFunc, len(rgs))
for _, rg := range rgs {
testCheckFuncsByRgId[rg.Id] = genTestCheckFuncs(rg)
}

stepsById := make([]resource.TestStep, len(rgs))
for i, rg := range rgs {
stepsById[i] = resource.TestStep{
Config: insecureProviderConfigHCL + fmt.Sprintf(dataSourceDataCenterExternalGatewayByIdHCL, bp.Id(), rg.Id),
Check: resource.ComposeAggregateTestCheckFunc(testCheckFuncsByRgId[rg.Id]...),
}
}

stepsByName := make([]resource.TestStep, len(rgs))
for i, rg := range rgs {
stepsByName[i] = resource.TestStep{
Config: insecureProviderConfigHCL + fmt.Sprintf(dataSourceDataCenterExternalGatewayByNameHCL, bp.Id(), rg.Data.GwName),
Check: resource.ComposeAggregateTestCheckFunc(genTestCheckFuncs(rg)...),
}
}

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
//Steps: stepsById,
Steps: append(stepsById, stepsByName...),
})
}
1 change: 1 addition & 0 deletions apstra/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource
func() datasource.DataSource { return &dataSourceDatacenterCtStaticRoute{} },
func() datasource.DataSource { return &dataSourceDatacenterCtVnSingle{} },
func() datasource.DataSource { return &dataSourceDatacenterCtVnMultiple{} },
func() datasource.DataSource { return &dataSourceDatacenterExternalGateway{} },
func() datasource.DataSource { return &dataSourceDatacenterPropertySet{} },
func() datasource.DataSource { return &dataSourceDatacenterPropertySets{} },
func() datasource.DataSource { return &dataSourceDatacenterRoutingPolicies{} },
Expand Down
28 changes: 15 additions & 13 deletions apstra/test_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"context"
"fmt"
"github.com/Juniper/apstra-go-sdk/apstra"
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"golang.org/x/exp/constraints"
"net"
"testing"
)

Expand Down Expand Up @@ -68,16 +70,16 @@ func intPtrOrNull[A constraints.Integer](in *A) string {
// return `"` + in.String() + `"`
//}

//func randIpAddressMust(t *testing.T, cidrBlock string) net.IP {
// s, err := acctest.RandIpAddress(cidrBlock)
// if err != nil {
// t.Fatal(err)
// }
//
// ip := net.ParseIP(s)
// if ip == nil {
// t.Fatalf("randIpAddressMust failed to parse IP address %q", s)
// }
//
// return ip
//}
func randIpAddressMust(t *testing.T, cidrBlock string) net.IP {
s, err := acctest.RandIpAddress(cidrBlock)
if err != nil {
t.Fatal(err)
}

ip := net.ParseIP(s)
if ip == nil {
t.Fatalf("randIpAddressMust failed to parse IP address %q", s)
}

return ip
}

0 comments on commit af2f51d

Please sign in to comment.