ensure online status and route changes are propagated (#1564)
This commit is contained in:
parent
0153e26392
commit
f65f4eca35
40 changed files with 3170 additions and 857 deletions
|
@ -84,20 +84,31 @@ type StateUpdateType int
|
|||
|
||||
const (
|
||||
StateFullUpdate StateUpdateType = iota
|
||||
// StatePeerChanged is used for updates that needs
|
||||
// to be calculated with all peers and all policy rules.
|
||||
// This would typically be things that include tags, routes
|
||||
// and similar.
|
||||
StatePeerChanged
|
||||
StatePeerChangedPatch
|
||||
StatePeerRemoved
|
||||
StateDERPUpdated
|
||||
)
|
||||
|
||||
// StateUpdate is an internal message containing information about
|
||||
// a state change that has happened to the network.
|
||||
// If type is StateFullUpdate, all fields are ignored.
|
||||
type StateUpdate struct {
|
||||
// The type of update
|
||||
Type StateUpdateType
|
||||
|
||||
// Changed must be set when Type is StatePeerChanged and
|
||||
// contain the Node IDs of nodes that have changed.
|
||||
Changed Nodes
|
||||
// ChangeNodes must be set when Type is StatePeerAdded
|
||||
// and StatePeerChanged and contains the full node
|
||||
// object for added nodes.
|
||||
ChangeNodes Nodes
|
||||
|
||||
// ChangePatches must be set when Type is StatePeerChangedPatch
|
||||
// and contains a populated PeerChange object.
|
||||
ChangePatches []*tailcfg.PeerChange
|
||||
|
||||
// Removed must be set when Type is StatePeerRemoved and
|
||||
// contain a list of the nodes that has been removed from
|
||||
|
@ -106,5 +117,36 @@ type StateUpdate struct {
|
|||
|
||||
// DERPMap must be set when Type is StateDERPUpdated and
|
||||
// contain the new DERP Map.
|
||||
DERPMap tailcfg.DERPMap
|
||||
DERPMap *tailcfg.DERPMap
|
||||
|
||||
// Additional message for tracking origin or what being
|
||||
// updated, useful for ambiguous updates like StatePeerChanged.
|
||||
Message string
|
||||
}
|
||||
|
||||
// Valid reports if a StateUpdate is correctly filled and
|
||||
// panics if the mandatory fields for a type is not
|
||||
// filled.
|
||||
// Reports true if valid.
|
||||
func (su *StateUpdate) Valid() bool {
|
||||
switch su.Type {
|
||||
case StatePeerChanged:
|
||||
if su.ChangeNodes == nil {
|
||||
panic("Mandatory field ChangeNodes is not set on StatePeerChanged update")
|
||||
}
|
||||
case StatePeerChangedPatch:
|
||||
if su.ChangePatches == nil {
|
||||
panic("Mandatory field ChangePatches is not set on StatePeerChangedPatch update")
|
||||
}
|
||||
case StatePeerRemoved:
|
||||
if su.Removed == nil {
|
||||
panic("Mandatory field Removed is not set on StatePeerRemove update")
|
||||
}
|
||||
case StateDERPUpdated:
|
||||
if su.DERPMap == nil {
|
||||
panic("Mandatory field DERPMap is not set on StateDERPUpdated update")
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -21,7 +21,9 @@ import (
|
|||
|
||||
var (
|
||||
ErrNodeAddressesInvalid = errors.New("failed to parse node addresses")
|
||||
ErrHostnameTooLong = errors.New("hostname too long")
|
||||
ErrHostnameTooLong = errors.New("hostname too long, cannot except 255 ASCII chars")
|
||||
ErrNodeHasNoGivenName = errors.New("node has no given name")
|
||||
ErrNodeUserHasNoName = errors.New("node user has no name")
|
||||
)
|
||||
|
||||
// Node is a Headscale client.
|
||||
|
@ -95,22 +97,14 @@ type Node struct {
|
|||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
|
||||
IsOnline *bool `gorm:"-"`
|
||||
}
|
||||
|
||||
type (
|
||||
Nodes []*Node
|
||||
)
|
||||
|
||||
func (nodes Nodes) OnlineNodeMap() map[tailcfg.NodeID]bool {
|
||||
ret := make(map[tailcfg.NodeID]bool)
|
||||
|
||||
for _, node := range nodes {
|
||||
ret[tailcfg.NodeID(node.ID)] = node.IsOnline()
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
type NodeAddresses []netip.Addr
|
||||
|
||||
func (na NodeAddresses) Sort() {
|
||||
|
@ -206,21 +200,6 @@ func (node Node) IsExpired() bool {
|
|||
return time.Now().UTC().After(*node.Expiry)
|
||||
}
|
||||
|
||||
// IsOnline returns if the node is connected to Headscale.
|
||||
// This is really a naive implementation, as we don't really see
|
||||
// if there is a working connection between the client and the server.
|
||||
func (node *Node) IsOnline() bool {
|
||||
if node.LastSeen == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if node.IsExpired() {
|
||||
return false
|
||||
}
|
||||
|
||||
return node.LastSeen.After(time.Now().Add(-KeepAliveInterval))
|
||||
}
|
||||
|
||||
// IsEphemeral returns if the node is registered as an Ephemeral node.
|
||||
// https://tailscale.com/kb/1111/ephemeral-nodes/
|
||||
func (node *Node) IsEphemeral() bool {
|
||||
|
@ -339,7 +318,6 @@ func (node *Node) Proto() *v1.Node {
|
|||
GivenName: node.GivenName,
|
||||
User: node.User.Proto(),
|
||||
ForcedTags: node.ForcedTags,
|
||||
Online: node.IsOnline(),
|
||||
|
||||
// TODO(kradalby): Implement register method enum converter
|
||||
// RegisterMethod: ,
|
||||
|
@ -365,6 +343,14 @@ func (node *Node) Proto() *v1.Node {
|
|||
func (node *Node) GetFQDN(dnsConfig *tailcfg.DNSConfig, baseDomain string) (string, error) {
|
||||
var hostname string
|
||||
if dnsConfig != nil && dnsConfig.Proxied { // MagicDNS
|
||||
if node.GivenName == "" {
|
||||
return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeHasNoGivenName)
|
||||
}
|
||||
|
||||
if node.User.Name == "" {
|
||||
return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeUserHasNoName)
|
||||
}
|
||||
|
||||
hostname = fmt.Sprintf(
|
||||
"%s.%s.%s",
|
||||
node.GivenName,
|
||||
|
@ -373,7 +359,7 @@ func (node *Node) GetFQDN(dnsConfig *tailcfg.DNSConfig, baseDomain string) (stri
|
|||
)
|
||||
if len(hostname) > MaxHostnameLength {
|
||||
return "", fmt.Errorf(
|
||||
"hostname %q is too long it cannot except 255 ASCII chars: %w",
|
||||
"failed to create valid FQDN (%s): %w",
|
||||
hostname,
|
||||
ErrHostnameTooLong,
|
||||
)
|
||||
|
@ -385,8 +371,98 @@ func (node *Node) GetFQDN(dnsConfig *tailcfg.DNSConfig, baseDomain string) (stri
|
|||
return hostname, nil
|
||||
}
|
||||
|
||||
func (node Node) String() string {
|
||||
return node.Hostname
|
||||
// func (node *Node) String() string {
|
||||
// return node.Hostname
|
||||
// }
|
||||
|
||||
// PeerChangeFromMapRequest takes a MapRequest and compares it to the node
|
||||
// to produce a PeerChange struct that can be used to updated the node and
|
||||
// inform peers about smaller changes to the node.
|
||||
// When a field is added to this function, remember to also add it to:
|
||||
// - node.ApplyPeerChange
|
||||
// - logTracePeerChange in poll.go
|
||||
func (node *Node) PeerChangeFromMapRequest(req tailcfg.MapRequest) tailcfg.PeerChange {
|
||||
ret := tailcfg.PeerChange{
|
||||
NodeID: tailcfg.NodeID(node.ID),
|
||||
}
|
||||
|
||||
if node.NodeKey.String() != req.NodeKey.String() {
|
||||
ret.Key = &req.NodeKey
|
||||
}
|
||||
|
||||
if node.DiscoKey.String() != req.DiscoKey.String() {
|
||||
ret.DiscoKey = &req.DiscoKey
|
||||
}
|
||||
|
||||
if node.Hostinfo != nil &&
|
||||
node.Hostinfo.NetInfo != nil &&
|
||||
req.Hostinfo != nil &&
|
||||
req.Hostinfo.NetInfo != nil &&
|
||||
node.Hostinfo.NetInfo.PreferredDERP != req.Hostinfo.NetInfo.PreferredDERP {
|
||||
ret.DERPRegion = req.Hostinfo.NetInfo.PreferredDERP
|
||||
}
|
||||
|
||||
if req.Hostinfo != nil && req.Hostinfo.NetInfo != nil {
|
||||
// If there is no stored Hostinfo or NetInfo, use
|
||||
// the new PreferredDERP.
|
||||
if node.Hostinfo == nil {
|
||||
ret.DERPRegion = req.Hostinfo.NetInfo.PreferredDERP
|
||||
} else if node.Hostinfo.NetInfo == nil {
|
||||
ret.DERPRegion = req.Hostinfo.NetInfo.PreferredDERP
|
||||
} else {
|
||||
// If there is a PreferredDERP check if it has changed.
|
||||
if node.Hostinfo.NetInfo.PreferredDERP != req.Hostinfo.NetInfo.PreferredDERP {
|
||||
ret.DERPRegion = req.Hostinfo.NetInfo.PreferredDERP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(kradalby): Find a good way to compare updates
|
||||
ret.Endpoints = req.Endpoints
|
||||
|
||||
now := time.Now()
|
||||
ret.LastSeen = &now
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// ApplyPeerChange takes a PeerChange struct and updates the node.
|
||||
func (node *Node) ApplyPeerChange(change *tailcfg.PeerChange) {
|
||||
if change.Key != nil {
|
||||
node.NodeKey = *change.Key
|
||||
}
|
||||
|
||||
if change.DiscoKey != nil {
|
||||
node.DiscoKey = *change.DiscoKey
|
||||
}
|
||||
|
||||
if change.Online != nil {
|
||||
node.IsOnline = change.Online
|
||||
}
|
||||
|
||||
if change.Endpoints != nil {
|
||||
node.Endpoints = change.Endpoints
|
||||
}
|
||||
|
||||
// This might technically not be useful as we replace
|
||||
// the whole hostinfo blob when it has changed.
|
||||
if change.DERPRegion != 0 {
|
||||
if node.Hostinfo == nil {
|
||||
node.Hostinfo = &tailcfg.Hostinfo{
|
||||
NetInfo: &tailcfg.NetInfo{
|
||||
PreferredDERP: change.DERPRegion,
|
||||
},
|
||||
}
|
||||
} else if node.Hostinfo.NetInfo == nil {
|
||||
node.Hostinfo.NetInfo = &tailcfg.NetInfo{
|
||||
PreferredDERP: change.DERPRegion,
|
||||
}
|
||||
} else {
|
||||
node.Hostinfo.NetInfo.PreferredDERP = change.DERPRegion
|
||||
}
|
||||
}
|
||||
|
||||
node.LastSeen = change.LastSeen
|
||||
}
|
||||
|
||||
func (nodes Nodes) String() string {
|
||||
|
|
|
@ -4,7 +4,10 @@ import (
|
|||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
func Test_NodeCanAccess(t *testing.T) {
|
||||
|
@ -139,3 +142,227 @@ func TestNodeAddressesOrder(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeFQDN(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
node Node
|
||||
dns tailcfg.DNSConfig
|
||||
domain string
|
||||
want string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "all-set",
|
||||
node: Node{
|
||||
GivenName: "test",
|
||||
User: User{
|
||||
Name: "user",
|
||||
},
|
||||
},
|
||||
dns: tailcfg.DNSConfig{
|
||||
Proxied: true,
|
||||
},
|
||||
domain: "example.com",
|
||||
want: "test.user.example.com",
|
||||
},
|
||||
{
|
||||
name: "no-given-name",
|
||||
node: Node{
|
||||
User: User{
|
||||
Name: "user",
|
||||
},
|
||||
},
|
||||
dns: tailcfg.DNSConfig{
|
||||
Proxied: true,
|
||||
},
|
||||
domain: "example.com",
|
||||
wantErr: "failed to create valid FQDN: node has no given name",
|
||||
},
|
||||
{
|
||||
name: "no-user-name",
|
||||
node: Node{
|
||||
GivenName: "test",
|
||||
User: User{},
|
||||
},
|
||||
dns: tailcfg.DNSConfig{
|
||||
Proxied: true,
|
||||
},
|
||||
domain: "example.com",
|
||||
wantErr: "failed to create valid FQDN: node user has no name",
|
||||
},
|
||||
{
|
||||
name: "no-magic-dns",
|
||||
node: Node{
|
||||
GivenName: "test",
|
||||
User: User{
|
||||
Name: "user",
|
||||
},
|
||||
},
|
||||
dns: tailcfg.DNSConfig{
|
||||
Proxied: false,
|
||||
},
|
||||
domain: "example.com",
|
||||
want: "test",
|
||||
},
|
||||
{
|
||||
name: "no-dnsconfig",
|
||||
node: Node{
|
||||
GivenName: "test",
|
||||
User: User{
|
||||
Name: "user",
|
||||
},
|
||||
},
|
||||
domain: "example.com",
|
||||
want: "test",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := tc.node.GetFQDN(&tc.dns, tc.domain)
|
||||
|
||||
if (err != nil) && (err.Error() != tc.wantErr) {
|
||||
t.Errorf("GetFQDN() error = %s, wantErr %s", err, tc.wantErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tc.want, got); diff != "" {
|
||||
t.Errorf("GetFQDN unexpected result (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeerChangeFromMapRequest(t *testing.T) {
|
||||
nKeys := []key.NodePublic{
|
||||
key.NewNode().Public(),
|
||||
key.NewNode().Public(),
|
||||
key.NewNode().Public(),
|
||||
}
|
||||
|
||||
dKeys := []key.DiscoPublic{
|
||||
key.NewDisco().Public(),
|
||||
key.NewDisco().Public(),
|
||||
key.NewDisco().Public(),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
node Node
|
||||
mapReq tailcfg.MapRequest
|
||||
want tailcfg.PeerChange
|
||||
}{
|
||||
{
|
||||
name: "preferred-derp-changed",
|
||||
node: Node{
|
||||
ID: 1,
|
||||
NodeKey: nKeys[0],
|
||||
DiscoKey: dKeys[0],
|
||||
Endpoints: []netip.AddrPort{},
|
||||
Hostinfo: &tailcfg.Hostinfo{
|
||||
NetInfo: &tailcfg.NetInfo{
|
||||
PreferredDERP: 998,
|
||||
},
|
||||
},
|
||||
},
|
||||
mapReq: tailcfg.MapRequest{
|
||||
NodeKey: nKeys[0],
|
||||
DiscoKey: dKeys[0],
|
||||
Hostinfo: &tailcfg.Hostinfo{
|
||||
NetInfo: &tailcfg.NetInfo{
|
||||
PreferredDERP: 999,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: tailcfg.PeerChange{
|
||||
NodeID: 1,
|
||||
DERPRegion: 999,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "preferred-derp-no-changed",
|
||||
node: Node{
|
||||
ID: 1,
|
||||
NodeKey: nKeys[0],
|
||||
DiscoKey: dKeys[0],
|
||||
Endpoints: []netip.AddrPort{},
|
||||
Hostinfo: &tailcfg.Hostinfo{
|
||||
NetInfo: &tailcfg.NetInfo{
|
||||
PreferredDERP: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
mapReq: tailcfg.MapRequest{
|
||||
NodeKey: nKeys[0],
|
||||
DiscoKey: dKeys[0],
|
||||
Hostinfo: &tailcfg.Hostinfo{
|
||||
NetInfo: &tailcfg.NetInfo{
|
||||
PreferredDERP: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: tailcfg.PeerChange{
|
||||
NodeID: 1,
|
||||
DERPRegion: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "preferred-derp-no-mapreq-netinfo",
|
||||
node: Node{
|
||||
ID: 1,
|
||||
NodeKey: nKeys[0],
|
||||
DiscoKey: dKeys[0],
|
||||
Endpoints: []netip.AddrPort{},
|
||||
Hostinfo: &tailcfg.Hostinfo{
|
||||
NetInfo: &tailcfg.NetInfo{
|
||||
PreferredDERP: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
mapReq: tailcfg.MapRequest{
|
||||
NodeKey: nKeys[0],
|
||||
DiscoKey: dKeys[0],
|
||||
Hostinfo: &tailcfg.Hostinfo{},
|
||||
},
|
||||
want: tailcfg.PeerChange{
|
||||
NodeID: 1,
|
||||
DERPRegion: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "preferred-derp-no-node-netinfo",
|
||||
node: Node{
|
||||
ID: 1,
|
||||
NodeKey: nKeys[0],
|
||||
DiscoKey: dKeys[0],
|
||||
Endpoints: []netip.AddrPort{},
|
||||
Hostinfo: &tailcfg.Hostinfo{},
|
||||
},
|
||||
mapReq: tailcfg.MapRequest{
|
||||
NodeKey: nKeys[0],
|
||||
DiscoKey: dKeys[0],
|
||||
Hostinfo: &tailcfg.Hostinfo{
|
||||
NetInfo: &tailcfg.NetInfo{
|
||||
PreferredDERP: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: tailcfg.PeerChange{
|
||||
NodeID: 1,
|
||||
DERPRegion: 200,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := tc.node.PeerChangeFromMapRequest(tc.mapReq)
|
||||
|
||||
if diff := cmp.Diff(tc.want, got, cmpopts.IgnoreFields(tailcfg.PeerChange{}, "LastSeen")); diff != "" {
|
||||
t.Errorf("Patch unexpected result (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ type Route struct {
|
|||
|
||||
NodeID uint64
|
||||
Node Node
|
||||
|
||||
// TODO(kradalby): change this custom type to netip.Prefix
|
||||
Prefix IPPrefix
|
||||
|
||||
Advertised bool
|
||||
|
@ -29,13 +31,17 @@ type Route struct {
|
|||
type Routes []Route
|
||||
|
||||
func (r *Route) String() string {
|
||||
return fmt.Sprintf("%s:%s", r.Node, netip.Prefix(r.Prefix).String())
|
||||
return fmt.Sprintf("%s:%s", r.Node.Hostname, netip.Prefix(r.Prefix).String())
|
||||
}
|
||||
|
||||
func (r *Route) IsExitRoute() bool {
|
||||
return netip.Prefix(r.Prefix) == ExitRouteV4 || netip.Prefix(r.Prefix) == ExitRouteV6
|
||||
}
|
||||
|
||||
func (r *Route) IsAnnouncable() bool {
|
||||
return r.Advertised && r.Enabled
|
||||
}
|
||||
|
||||
func (rs Routes) Prefixes() []netip.Prefix {
|
||||
prefixes := make([]netip.Prefix, len(rs))
|
||||
for i, r := range rs {
|
||||
|
@ -45,6 +51,32 @@ func (rs Routes) Prefixes() []netip.Prefix {
|
|||
return prefixes
|
||||
}
|
||||
|
||||
// Primaries returns Primary routes from a list of routes.
|
||||
func (rs Routes) Primaries() Routes {
|
||||
res := make(Routes, 0)
|
||||
for _, route := range rs {
|
||||
if route.IsPrimary {
|
||||
res = append(res, route)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (rs Routes) PrefixMap() map[IPPrefix][]Route {
|
||||
res := map[IPPrefix][]Route{}
|
||||
|
||||
for _, route := range rs {
|
||||
if _, ok := res[route.Prefix]; ok {
|
||||
res[route.Prefix] = append(res[route.Prefix], route)
|
||||
} else {
|
||||
res[route.Prefix] = []Route{route}
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (rs Routes) Proto() []*v1.Route {
|
||||
protoRoutes := []*v1.Route{}
|
||||
|
||||
|
|
94
hscontrol/types/routes_test.go
Normal file
94
hscontrol/types/routes_test.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
)
|
||||
|
||||
func TestPrefixMap(t *testing.T) {
|
||||
ipp := func(s string) IPPrefix { return IPPrefix(netip.MustParsePrefix(s)) }
|
||||
|
||||
// TODO(kradalby): Remove when we have gotten rid of IPPrefix type
|
||||
prefixComparer := cmp.Comparer(func(x, y IPPrefix) bool {
|
||||
return x == y
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
rs Routes
|
||||
want map[IPPrefix][]Route
|
||||
}{
|
||||
{
|
||||
rs: Routes{
|
||||
Route{
|
||||
Prefix: ipp("10.0.0.0/24"),
|
||||
},
|
||||
},
|
||||
want: map[IPPrefix][]Route{
|
||||
ipp("10.0.0.0/24"): Routes{
|
||||
Route{
|
||||
Prefix: ipp("10.0.0.0/24"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rs: Routes{
|
||||
Route{
|
||||
Prefix: ipp("10.0.0.0/24"),
|
||||
},
|
||||
Route{
|
||||
Prefix: ipp("10.0.1.0/24"),
|
||||
},
|
||||
},
|
||||
want: map[IPPrefix][]Route{
|
||||
ipp("10.0.0.0/24"): Routes{
|
||||
Route{
|
||||
Prefix: ipp("10.0.0.0/24"),
|
||||
},
|
||||
},
|
||||
ipp("10.0.1.0/24"): Routes{
|
||||
Route{
|
||||
Prefix: ipp("10.0.1.0/24"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rs: Routes{
|
||||
Route{
|
||||
Prefix: ipp("10.0.0.0/24"),
|
||||
Enabled: true,
|
||||
},
|
||||
Route{
|
||||
Prefix: ipp("10.0.0.0/24"),
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
want: map[IPPrefix][]Route{
|
||||
ipp("10.0.0.0/24"): Routes{
|
||||
Route{
|
||||
Prefix: ipp("10.0.0.0/24"),
|
||||
Enabled: true,
|
||||
},
|
||||
Route{
|
||||
Prefix: ipp("10.0.0.0/24"),
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for idx, tt := range tests {
|
||||
t.Run(fmt.Sprintf("test-%d", idx), func(t *testing.T) {
|
||||
got := tt.rs.PrefixMap()
|
||||
if diff := cmp.Diff(tt.want, got, prefixComparer, util.MkeyComparer, util.NkeyComparer, util.DkeyComparer); diff != "" {
|
||||
t.Errorf("PrefixMap() unexpected result (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue