Merge pull request #103 from juanfont/shared-nodes
Add support for sharing nodes across namespaces
This commit is contained in:
commit
e27753e46e
11 changed files with 576 additions and 42 deletions
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
|
@ -19,7 +19,7 @@ jobs:
|
||||||
# golangci-lint manually in the `Run lint` step.
|
# golangci-lint manually in the `Run lint` step.
|
||||||
- uses: golangci/golangci-lint-action@v2
|
- uses: golangci/golangci-lint-action@v2
|
||||||
with:
|
with:
|
||||||
args: --timeout 2m
|
args: --timeout 4m
|
||||||
|
|
||||||
# Setup Go
|
# Setup Go
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
|
@ -36,4 +36,6 @@ jobs:
|
||||||
sudo apt install -y make
|
sudo apt install -y make
|
||||||
|
|
||||||
- name: Run lint
|
- name: Run lint
|
||||||
|
with:
|
||||||
|
args: --timeout 4m
|
||||||
run: make lint
|
run: make lint
|
||||||
|
|
|
@ -26,14 +26,12 @@ Headscale implements this coordination server.
|
||||||
- [X] ACLs
|
- [X] ACLs
|
||||||
- [X] Support for alternative IP ranges in the tailnets (default Tailscale's 100.64.0.0/10)
|
- [X] Support for alternative IP ranges in the tailnets (default Tailscale's 100.64.0.0/10)
|
||||||
- [X] DNS (passing DNS servers to nodes)
|
- [X] DNS (passing DNS servers to nodes)
|
||||||
- [ ] Share nodes between ~~users~~ namespaces
|
- [X] Share nodes between ~~users~~ namespaces
|
||||||
- [ ] MagicDNS / Smart DNS
|
- [ ] MagicDNS / Smart DNS
|
||||||
|
|
||||||
|
|
||||||
## Roadmap 🤷
|
## Roadmap 🤷
|
||||||
|
|
||||||
We are now focusing on adding integration tests with the official clients.
|
|
||||||
|
|
||||||
Suggestions/PRs welcomed!
|
Suggestions/PRs welcomed!
|
||||||
|
|
||||||
|
|
||||||
|
|
5
api.go
5
api.go
|
@ -33,8 +33,6 @@ func (h *Headscale) RegisterWebAPI(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// spew.Dump(c.Params)
|
|
||||||
|
|
||||||
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(fmt.Sprintf(`
|
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(fmt.Sprintf(`
|
||||||
<html>
|
<html>
|
||||||
<body>
|
<body>
|
||||||
|
@ -220,7 +218,7 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Mac
|
||||||
Str("func", "getMapResponse").
|
Str("func", "getMapResponse").
|
||||||
Str("machine", req.Hostinfo.Hostname).
|
Str("machine", req.Hostinfo.Hostname).
|
||||||
Msg("Creating Map response")
|
Msg("Creating Map response")
|
||||||
node, err := m.toNode()
|
node, err := m.toNode(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Str("func", "getMapResponse").
|
Str("func", "getMapResponse").
|
||||||
|
@ -280,7 +278,6 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Mac
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// spew.Dump(resp)
|
|
||||||
// declare the incoming size on the first 4 bytes
|
// declare the incoming size on the first 4 bytes
|
||||||
data := make([]byte, 4)
|
data := make([]byte, 4)
|
||||||
binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
|
binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
|
||||||
|
|
|
@ -25,6 +25,7 @@ func init() {
|
||||||
nodeCmd.AddCommand(listNodesCmd)
|
nodeCmd.AddCommand(listNodesCmd)
|
||||||
nodeCmd.AddCommand(registerNodeCmd)
|
nodeCmd.AddCommand(registerNodeCmd)
|
||||||
nodeCmd.AddCommand(deleteNodeCmd)
|
nodeCmd.AddCommand(deleteNodeCmd)
|
||||||
|
nodeCmd.AddCommand(shareMachineCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
var nodeCmd = &cobra.Command{
|
var nodeCmd = &cobra.Command{
|
||||||
|
@ -79,9 +80,26 @@ var listNodesCmd = &cobra.Command{
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error initializing: %s", err)
|
log.Fatalf("Error initializing: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace, err := h.GetNamespace(n)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error fetching namespace: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
machines, err := h.ListMachinesInNamespace(n)
|
machines, err := h.ListMachinesInNamespace(n)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error fetching machines: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sharedMachines, err := h.ListSharedMachinesInNamespace(n)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error fetching shared machines: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
allMachines := append(*machines, *sharedMachines...)
|
||||||
|
|
||||||
if strings.HasPrefix(o, "json") {
|
if strings.HasPrefix(o, "json") {
|
||||||
JsonOutput(machines, err, o)
|
JsonOutput(allMachines, err, o)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +107,7 @@ var listNodesCmd = &cobra.Command{
|
||||||
log.Fatalf("Error getting nodes: %s", err)
|
log.Fatalf("Error getting nodes: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
d, err := nodesToPtables(*machines)
|
d, err := nodesToPtables(*namespace, allMachines)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error converting to table: %s", err)
|
log.Fatalf("Error converting to table: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -145,21 +163,75 @@ var deleteNodeCmd = &cobra.Command{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func nodesToPtables(m []headscale.Machine) (pterm.TableData, error) {
|
var shareMachineCmd = &cobra.Command{
|
||||||
d := pterm.TableData{{"ID", "Name", "NodeKey", "IP address", "Ephemeral", "Last seen", "Online"}}
|
Use: "share ID namespace",
|
||||||
|
Short: "Shares a node from the current namespace to the specified one",
|
||||||
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) < 2 {
|
||||||
|
return fmt.Errorf("missing parameters")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
namespace, err := cmd.Flags().GetString("namespace")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error getting namespace: %s", err)
|
||||||
|
}
|
||||||
|
output, _ := cmd.Flags().GetString("output")
|
||||||
|
|
||||||
for _, m := range m {
|
h, err := getHeadscaleApp()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error initializing: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = h.GetNamespace(namespace)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error fetching origin namespace: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
destinationNamespace, err := h.GetNamespace(args[1])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error fetching destination namespace: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(args[0])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error converting ID to integer: %s", err)
|
||||||
|
}
|
||||||
|
machine, err := h.GetMachineByID(uint64(id))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error getting node: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.AddSharedMachineToNamespace(machine, destinationNamespace)
|
||||||
|
if strings.HasPrefix(output, "json") {
|
||||||
|
JsonOutput(map[string]string{"Result": "Node shared"}, err, output)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error sharing node: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Node shared!")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func nodesToPtables(currentNamespace headscale.Namespace, machines []headscale.Machine) (pterm.TableData, error) {
|
||||||
|
d := pterm.TableData{{"ID", "Name", "NodeKey", "Namespace", "IP address", "Ephemeral", "Last seen", "Online"}}
|
||||||
|
|
||||||
|
for _, machine := range machines {
|
||||||
var ephemeral bool
|
var ephemeral bool
|
||||||
if m.AuthKey != nil && m.AuthKey.Ephemeral {
|
if machine.AuthKey != nil && machine.AuthKey.Ephemeral {
|
||||||
ephemeral = true
|
ephemeral = true
|
||||||
}
|
}
|
||||||
var lastSeen time.Time
|
var lastSeen time.Time
|
||||||
var lastSeenTime string
|
var lastSeenTime string
|
||||||
if m.LastSeen != nil {
|
if machine.LastSeen != nil {
|
||||||
lastSeen = *m.LastSeen
|
lastSeen = *machine.LastSeen
|
||||||
lastSeenTime = lastSeen.Format("2006-01-02 15:04:05")
|
lastSeenTime = lastSeen.Format("2006-01-02 15:04:05")
|
||||||
}
|
}
|
||||||
nKey, err := wgkey.ParseHex(m.NodeKey)
|
nKey, err := wgkey.ParseHex(machine.NodeKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -171,7 +243,14 @@ func nodesToPtables(m []headscale.Machine) (pterm.TableData, error) {
|
||||||
} else {
|
} else {
|
||||||
online = pterm.LightRed("false")
|
online = pterm.LightRed("false")
|
||||||
}
|
}
|
||||||
d = append(d, []string{strconv.FormatUint(m.ID, 10), m.Name, nodeKey.ShortString(), m.IPAddress, strconv.FormatBool(ephemeral), lastSeenTime, online})
|
|
||||||
|
var namespace string
|
||||||
|
if currentNamespace.ID == machine.NamespaceID {
|
||||||
|
namespace = pterm.LightMagenta(machine.Namespace.Name)
|
||||||
|
} else {
|
||||||
|
namespace = pterm.LightYellow(machine.Namespace.Name)
|
||||||
|
}
|
||||||
|
d = append(d, []string{strconv.FormatUint(machine.ID, 10), machine.Name, nodeKey.ShortString(), namespace, machine.IPAddress, strconv.FormatBool(ephemeral), lastSeenTime, online})
|
||||||
}
|
}
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
5
db.go
5
db.go
|
@ -44,6 +44,11 @@ func (h *Headscale) initDB() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = db.AutoMigrate(&SharedMachine{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
err = h.setValue("db_version", dbVersion)
|
err = h.setValue("db_version", dbVersion)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
75
machine.go
75
machine.go
|
@ -50,7 +50,9 @@ func (m Machine) isAlreadyRegistered() bool {
|
||||||
return m.Registered
|
return m.Registered
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Machine) toNode() (*tailcfg.Node, error) {
|
// toNode converts a Machine into a Tailscale Node. includeRoutes is false for shared nodes
|
||||||
|
// as per the expected behaviour in the official SaaS
|
||||||
|
func (m Machine) toNode(includeRoutes bool) (*tailcfg.Node, error) {
|
||||||
nKey, err := wgkey.ParseHex(m.NodeKey)
|
nKey, err := wgkey.ParseHex(m.NodeKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -85,24 +87,26 @@ func (m Machine) toNode() (*tailcfg.Node, error) {
|
||||||
allowedIPs := []netaddr.IPPrefix{}
|
allowedIPs := []netaddr.IPPrefix{}
|
||||||
allowedIPs = append(allowedIPs, ip) // we append the node own IP, as it is required by the clients
|
allowedIPs = append(allowedIPs, ip) // we append the node own IP, as it is required by the clients
|
||||||
|
|
||||||
routesStr := []string{}
|
if includeRoutes {
|
||||||
if len(m.EnabledRoutes) != 0 {
|
routesStr := []string{}
|
||||||
allwIps, err := m.EnabledRoutes.MarshalJSON()
|
if len(m.EnabledRoutes) != 0 {
|
||||||
if err != nil {
|
allwIps, err := m.EnabledRoutes.MarshalJSON()
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(allwIps, &routesStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
err = json.Unmarshal(allwIps, &routesStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, aip := range routesStr {
|
for _, routeStr := range routesStr {
|
||||||
ip, err := netaddr.ParseIPPrefix(aip)
|
ip, err := netaddr.ParseIPPrefix(routeStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
}
|
||||||
|
allowedIPs = append(allowedIPs, ip)
|
||||||
}
|
}
|
||||||
allowedIPs = append(allowedIPs, ip)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoints := []string{}
|
endpoints := []string{}
|
||||||
|
@ -136,13 +140,20 @@ func (m Machine) toNode() (*tailcfg.Node, error) {
|
||||||
derp = "127.3.3.40:0" // Zero means disconnected or unknown.
|
derp = "127.3.3.40:0" // Zero means disconnected or unknown.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var keyExpiry time.Time
|
||||||
|
if m.Expiry != nil {
|
||||||
|
keyExpiry = *m.Expiry
|
||||||
|
} else {
|
||||||
|
keyExpiry = time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
n := tailcfg.Node{
|
n := tailcfg.Node{
|
||||||
ID: tailcfg.NodeID(m.ID), // this is the actual ID
|
ID: tailcfg.NodeID(m.ID), // this is the actual ID
|
||||||
StableID: tailcfg.StableNodeID(strconv.FormatUint(m.ID, 10)), // in headscale, unlike tailcontrol server, IDs are permanent
|
StableID: tailcfg.StableNodeID(strconv.FormatUint(m.ID, 10)), // in headscale, unlike tailcontrol server, IDs are permanent
|
||||||
Name: hostinfo.Hostname,
|
Name: hostinfo.Hostname,
|
||||||
User: tailcfg.UserID(m.NamespaceID),
|
User: tailcfg.UserID(m.NamespaceID),
|
||||||
Key: tailcfg.NodeKey(nKey),
|
Key: tailcfg.NodeKey(nKey),
|
||||||
KeyExpiry: *m.Expiry,
|
KeyExpiry: keyExpiry,
|
||||||
Machine: tailcfg.MachineKey(mKey),
|
Machine: tailcfg.MachineKey(mKey),
|
||||||
DiscoKey: discoKey,
|
DiscoKey: discoKey,
|
||||||
Addresses: addrs,
|
Addresses: addrs,
|
||||||
|
@ -165,6 +176,7 @@ func (h *Headscale) getPeers(m Machine) (*[]*tailcfg.Node, error) {
|
||||||
Str("func", "getPeers").
|
Str("func", "getPeers").
|
||||||
Str("machine", m.Name).
|
Str("machine", m.Name).
|
||||||
Msg("Finding peers")
|
Msg("Finding peers")
|
||||||
|
|
||||||
machines := []Machine{}
|
machines := []Machine{}
|
||||||
if err := h.db.Where("namespace_id = ? AND machine_key <> ? AND registered",
|
if err := h.db.Where("namespace_id = ? AND machine_key <> ? AND registered",
|
||||||
m.NamespaceID, m.MachineKey).Find(&machines).Error; err != nil {
|
m.NamespaceID, m.MachineKey).Find(&machines).Error; err != nil {
|
||||||
|
@ -172,9 +184,23 @@ func (h *Headscale) getPeers(m Machine) (*[]*tailcfg.Node, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We fetch here machines that are shared to the `Namespace` of the machine we are getting peers for
|
||||||
|
sharedMachines := []SharedMachine{}
|
||||||
|
if err := h.db.Preload("Namespace").Preload("Machine").Where("namespace_id = ?",
|
||||||
|
m.NamespaceID).Find(&sharedMachines).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
peers := []*tailcfg.Node{}
|
peers := []*tailcfg.Node{}
|
||||||
for _, mn := range machines {
|
for _, mn := range machines {
|
||||||
peer, err := mn.toNode()
|
peer, err := mn.toNode(true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
peers = append(peers, peer)
|
||||||
|
}
|
||||||
|
for _, sharedMachine := range sharedMachines {
|
||||||
|
peer, err := sharedMachine.Machine.toNode(false) // shared nodes do not expose their routes
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -201,13 +227,13 @@ func (h *Headscale) GetMachine(namespace string, name string) (*Machine, error)
|
||||||
return &m, nil
|
return &m, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("not found")
|
return nil, fmt.Errorf("machine not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMachineByID finds a Machine by ID and returns the Machine struct
|
// GetMachineByID finds a Machine by ID and returns the Machine struct
|
||||||
func (h *Headscale) GetMachineByID(id uint64) (*Machine, error) {
|
func (h *Headscale) GetMachineByID(id uint64) (*Machine, error) {
|
||||||
m := Machine{}
|
m := Machine{}
|
||||||
if result := h.db.Find(&Machine{ID: id}).First(&m); result.Error != nil {
|
if result := h.db.Preload("Namespace").Find(&Machine{ID: id}).First(&m); result.Error != nil {
|
||||||
return nil, result.Error
|
return nil, result.Error
|
||||||
}
|
}
|
||||||
return &m, nil
|
return &m, nil
|
||||||
|
@ -260,7 +286,14 @@ func (m *Machine) GetHostInfo() (*tailcfg.Hostinfo, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) notifyChangesToPeers(m *Machine) {
|
func (h *Headscale) notifyChangesToPeers(m *Machine) {
|
||||||
peers, _ := h.getPeers(*m)
|
peers, err := h.getPeers(*m)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Str("func", "notifyChangesToPeers").
|
||||||
|
Str("machine", m.Name).
|
||||||
|
Msgf("Error getting peers: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
for _, p := range *peers {
|
for _, p := range *peers {
|
||||||
log.Info().
|
log.Info().
|
||||||
Str("func", "notifyChangesToPeers").
|
Str("func", "notifyChangesToPeers").
|
||||||
|
|
|
@ -91,12 +91,34 @@ func (h *Headscale) ListMachinesInNamespace(name string) (*[]Machine, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
machines := []Machine{}
|
machines := []Machine{}
|
||||||
if err := h.db.Preload("AuthKey").Where(&Machine{NamespaceID: n.ID}).Find(&machines).Error; err != nil {
|
if err := h.db.Preload("AuthKey").Preload("Namespace").Where(&Machine{NamespaceID: n.ID}).Find(&machines).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &machines, nil
|
return &machines, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListSharedMachinesInNamespace returns all the machines that are shared to the specified namespace
|
||||||
|
func (h *Headscale) ListSharedMachinesInNamespace(name string) (*[]Machine, error) {
|
||||||
|
namespace, err := h.GetNamespace(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sharedMachines := []SharedMachine{}
|
||||||
|
if err := h.db.Preload("Namespace").Where(&SharedMachine{NamespaceID: namespace.ID}).Find(&sharedMachines).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
machines := []Machine{}
|
||||||
|
for _, sharedMachine := range sharedMachines {
|
||||||
|
machine, err := h.GetMachineByID(sharedMachine.MachineID) // otherwise not everything comes filled
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
machines = append(machines, *machine)
|
||||||
|
}
|
||||||
|
return &machines, nil
|
||||||
|
}
|
||||||
|
|
||||||
// SetMachineNamespace assigns a Machine to a namespace
|
// SetMachineNamespace assigns a Machine to a namespace
|
||||||
func (h *Headscale) SetMachineNamespace(m *Machine, namespaceName string) error {
|
func (h *Headscale) SetMachineNamespace(m *Machine, namespaceName string) error {
|
||||||
n, err := h.GetNamespace(namespaceName)
|
n, err := h.GetNamespace(namespaceName)
|
||||||
|
|
2
poll.go
2
poll.go
|
@ -440,7 +440,7 @@ func (h *Headscale) scheduledPollWorker(
|
||||||
case <-updateCheckerTicker.C:
|
case <-updateCheckerTicker.C:
|
||||||
// Send an update request regardless of outdated or not, if data is sent
|
// Send an update request regardless of outdated or not, if data is sent
|
||||||
// to the node is determined in the updateChan consumer block
|
// to the node is determined in the updateChan consumer block
|
||||||
n, _ := m.toNode()
|
n, _ := m.toNode(true)
|
||||||
err := h.sendRequestOnUpdateChannel(n)
|
err := h.sendRequestOnUpdateChannel(n)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
|
|
|
@ -56,6 +56,7 @@ func (h *Headscale) GetEnabledNodeRoutes(namespace string, nodeName string) ([]n
|
||||||
return routes, nil
|
return routes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsNodeRouteEnabled checks if a certain route has been enabled
|
||||||
func (h *Headscale) IsNodeRouteEnabled(namespace string, nodeName string, routeStr string) bool {
|
func (h *Headscale) IsNodeRouteEnabled(namespace string, nodeName string, routeStr string) bool {
|
||||||
route, err := netaddr.ParseIPPrefix(routeStr)
|
route, err := netaddr.ParseIPPrefix(routeStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -129,6 +130,7 @@ func (h *Headscale) EnableNodeRoute(namespace string, nodeName string, routeStr
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RoutesToPtables converts the list of routes to a nice table
|
||||||
func (h *Headscale) RoutesToPtables(namespace string, nodeName string, availableRoutes []netaddr.IPPrefix) pterm.TableData {
|
func (h *Headscale) RoutesToPtables(namespace string, nodeName string, availableRoutes []netaddr.IPPrefix) pterm.TableData {
|
||||||
d := pterm.TableData{{"Route", "Enabled"}}
|
d := pterm.TableData{{"Route", "Enabled"}}
|
||||||
|
|
||||||
|
|
37
sharing.go
Normal file
37
sharing.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package headscale
|
||||||
|
|
||||||
|
import "gorm.io/gorm"
|
||||||
|
|
||||||
|
const errorSameNamespace = Error("Destination namespace same as origin")
|
||||||
|
const errorMachineAlreadyShared = Error("Node already shared to this namespace")
|
||||||
|
|
||||||
|
// SharedMachine is a join table to support sharing nodes between namespaces
|
||||||
|
type SharedMachine struct {
|
||||||
|
gorm.Model
|
||||||
|
MachineID uint64
|
||||||
|
Machine Machine
|
||||||
|
NamespaceID uint
|
||||||
|
Namespace Namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSharedMachineToNamespace adds a machine as a shared node to a namespace
|
||||||
|
func (h *Headscale) AddSharedMachineToNamespace(m *Machine, ns *Namespace) error {
|
||||||
|
if m.NamespaceID == ns.ID {
|
||||||
|
return errorSameNamespace
|
||||||
|
}
|
||||||
|
|
||||||
|
sharedMachine := SharedMachine{}
|
||||||
|
if err := h.db.Where("machine_id = ? AND namespace_id", m.ID, ns.ID).First(&sharedMachine).Error; err == nil {
|
||||||
|
return errorMachineAlreadyShared
|
||||||
|
}
|
||||||
|
|
||||||
|
sharedMachine = SharedMachine{
|
||||||
|
MachineID: m.ID,
|
||||||
|
Machine: *m,
|
||||||
|
NamespaceID: ns.ID,
|
||||||
|
Namespace: *ns,
|
||||||
|
}
|
||||||
|
h.db.Save(&sharedMachine)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
359
sharing_test.go
Normal file
359
sharing_test.go
Normal file
|
@ -0,0 +1,359 @@
|
||||||
|
package headscale
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gopkg.in/check.v1"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Suite) TestBasicSharedNodesInNamespace(c *check.C) {
|
||||||
|
n1, err := h.CreateNamespace("shared1")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
n2, err := h.CreateNamespace("shared2")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
pak1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
pak2, err := h.CreatePreAuthKey(n2.Name, false, false, nil)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
_, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1")
|
||||||
|
c.Assert(err, check.NotNil)
|
||||||
|
|
||||||
|
m1 := Machine{
|
||||||
|
ID: 0,
|
||||||
|
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||||
|
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||||
|
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||||
|
Name: "test_get_shared_nodes_1",
|
||||||
|
NamespaceID: n1.ID,
|
||||||
|
Registered: true,
|
||||||
|
RegisterMethod: "authKey",
|
||||||
|
IPAddress: "100.64.0.1",
|
||||||
|
AuthKeyID: uint(pak1.ID),
|
||||||
|
}
|
||||||
|
h.db.Save(&m1)
|
||||||
|
|
||||||
|
_, err = h.GetMachine(n1.Name, m1.Name)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
m2 := Machine{
|
||||||
|
ID: 1,
|
||||||
|
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||||
|
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||||
|
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||||
|
Name: "test_get_shared_nodes_2",
|
||||||
|
NamespaceID: n2.ID,
|
||||||
|
Registered: true,
|
||||||
|
RegisterMethod: "authKey",
|
||||||
|
IPAddress: "100.64.0.2",
|
||||||
|
AuthKeyID: uint(pak2.ID),
|
||||||
|
}
|
||||||
|
h.db.Save(&m2)
|
||||||
|
|
||||||
|
_, err = h.GetMachine(n2.Name, m2.Name)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
p1s, err := h.getPeers(m1)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(len(*p1s), check.Equals, 0)
|
||||||
|
|
||||||
|
err = h.AddSharedMachineToNamespace(&m2, n1)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
p1sAfter, err := h.getPeers(m1)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(len(*p1sAfter), check.Equals, 1)
|
||||||
|
c.Assert((*p1sAfter)[0].ID, check.Equals, tailcfg.NodeID(m2.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Suite) TestSameNamespace(c *check.C) {
|
||||||
|
n1, err := h.CreateNamespace("shared1")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
n2, err := h.CreateNamespace("shared2")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
pak1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
pak2, err := h.CreatePreAuthKey(n2.Name, false, false, nil)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
_, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1")
|
||||||
|
c.Assert(err, check.NotNil)
|
||||||
|
|
||||||
|
m1 := Machine{
|
||||||
|
ID: 0,
|
||||||
|
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||||
|
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||||
|
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||||
|
Name: "test_get_shared_nodes_1",
|
||||||
|
NamespaceID: n1.ID,
|
||||||
|
Registered: true,
|
||||||
|
RegisterMethod: "authKey",
|
||||||
|
IPAddress: "100.64.0.1",
|
||||||
|
AuthKeyID: uint(pak1.ID),
|
||||||
|
}
|
||||||
|
h.db.Save(&m1)
|
||||||
|
|
||||||
|
_, err = h.GetMachine(n1.Name, m1.Name)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
m2 := Machine{
|
||||||
|
ID: 1,
|
||||||
|
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||||
|
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||||
|
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||||
|
Name: "test_get_shared_nodes_2",
|
||||||
|
NamespaceID: n2.ID,
|
||||||
|
Registered: true,
|
||||||
|
RegisterMethod: "authKey",
|
||||||
|
IPAddress: "100.64.0.2",
|
||||||
|
AuthKeyID: uint(pak2.ID),
|
||||||
|
}
|
||||||
|
h.db.Save(&m2)
|
||||||
|
|
||||||
|
_, err = h.GetMachine(n2.Name, m2.Name)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
p1s, err := h.getPeers(m1)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(len(*p1s), check.Equals, 0)
|
||||||
|
|
||||||
|
err = h.AddSharedMachineToNamespace(&m1, n1)
|
||||||
|
c.Assert(err, check.Equals, errorSameNamespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Suite) TestAlreadyShared(c *check.C) {
|
||||||
|
n1, err := h.CreateNamespace("shared1")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
n2, err := h.CreateNamespace("shared2")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
pak1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
pak2, err := h.CreatePreAuthKey(n2.Name, false, false, nil)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
_, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1")
|
||||||
|
c.Assert(err, check.NotNil)
|
||||||
|
|
||||||
|
m1 := Machine{
|
||||||
|
ID: 0,
|
||||||
|
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||||
|
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||||
|
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||||
|
Name: "test_get_shared_nodes_1",
|
||||||
|
NamespaceID: n1.ID,
|
||||||
|
Registered: true,
|
||||||
|
RegisterMethod: "authKey",
|
||||||
|
IPAddress: "100.64.0.1",
|
||||||
|
AuthKeyID: uint(pak1.ID),
|
||||||
|
}
|
||||||
|
h.db.Save(&m1)
|
||||||
|
|
||||||
|
_, err = h.GetMachine(n1.Name, m1.Name)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
m2 := Machine{
|
||||||
|
ID: 1,
|
||||||
|
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||||
|
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||||
|
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||||
|
Name: "test_get_shared_nodes_2",
|
||||||
|
NamespaceID: n2.ID,
|
||||||
|
Registered: true,
|
||||||
|
RegisterMethod: "authKey",
|
||||||
|
IPAddress: "100.64.0.2",
|
||||||
|
AuthKeyID: uint(pak2.ID),
|
||||||
|
}
|
||||||
|
h.db.Save(&m2)
|
||||||
|
|
||||||
|
_, err = h.GetMachine(n2.Name, m2.Name)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
p1s, err := h.getPeers(m1)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(len(*p1s), check.Equals, 0)
|
||||||
|
|
||||||
|
err = h.AddSharedMachineToNamespace(&m2, n1)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
err = h.AddSharedMachineToNamespace(&m2, n1)
|
||||||
|
c.Assert(err, check.Equals, errorMachineAlreadyShared)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Suite) TestDoNotIncludeRoutesOnShared(c *check.C) {
|
||||||
|
n1, err := h.CreateNamespace("shared1")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
n2, err := h.CreateNamespace("shared2")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
pak1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
pak2, err := h.CreatePreAuthKey(n2.Name, false, false, nil)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
_, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1")
|
||||||
|
c.Assert(err, check.NotNil)
|
||||||
|
|
||||||
|
m1 := Machine{
|
||||||
|
ID: 0,
|
||||||
|
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||||
|
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||||
|
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||||
|
Name: "test_get_shared_nodes_1",
|
||||||
|
NamespaceID: n1.ID,
|
||||||
|
Registered: true,
|
||||||
|
RegisterMethod: "authKey",
|
||||||
|
IPAddress: "100.64.0.1",
|
||||||
|
AuthKeyID: uint(pak1.ID),
|
||||||
|
}
|
||||||
|
h.db.Save(&m1)
|
||||||
|
|
||||||
|
_, err = h.GetMachine(n1.Name, m1.Name)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
m2 := Machine{
|
||||||
|
ID: 1,
|
||||||
|
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||||
|
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||||
|
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||||
|
Name: "test_get_shared_nodes_2",
|
||||||
|
NamespaceID: n2.ID,
|
||||||
|
Registered: true,
|
||||||
|
RegisterMethod: "authKey",
|
||||||
|
IPAddress: "100.64.0.2",
|
||||||
|
AuthKeyID: uint(pak2.ID),
|
||||||
|
}
|
||||||
|
h.db.Save(&m2)
|
||||||
|
|
||||||
|
_, err = h.GetMachine(n2.Name, m2.Name)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
p1s, err := h.getPeers(m1)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(len(*p1s), check.Equals, 0)
|
||||||
|
|
||||||
|
err = h.AddSharedMachineToNamespace(&m2, n1)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
p1sAfter, err := h.getPeers(m1)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(len(*p1sAfter), check.Equals, 1)
|
||||||
|
c.Assert(len((*p1sAfter)[0].AllowedIPs), check.Equals, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) {
|
||||||
|
n1, err := h.CreateNamespace("shared1")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
n2, err := h.CreateNamespace("shared2")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
n3, err := h.CreateNamespace("shared3")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
pak1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
pak2, err := h.CreatePreAuthKey(n2.Name, false, false, nil)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
pak3, err := h.CreatePreAuthKey(n3.Name, false, false, nil)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
pak4, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
_, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1")
|
||||||
|
c.Assert(err, check.NotNil)
|
||||||
|
|
||||||
|
m1 := Machine{
|
||||||
|
ID: 0,
|
||||||
|
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||||
|
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||||
|
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
|
||||||
|
Name: "test_get_shared_nodes_1",
|
||||||
|
NamespaceID: n1.ID,
|
||||||
|
Registered: true,
|
||||||
|
RegisterMethod: "authKey",
|
||||||
|
IPAddress: "100.64.0.1",
|
||||||
|
AuthKeyID: uint(pak1.ID),
|
||||||
|
}
|
||||||
|
h.db.Save(&m1)
|
||||||
|
|
||||||
|
_, err = h.GetMachine(n1.Name, m1.Name)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
m2 := Machine{
|
||||||
|
ID: 1,
|
||||||
|
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||||
|
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||||
|
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||||
|
Name: "test_get_shared_nodes_2",
|
||||||
|
NamespaceID: n2.ID,
|
||||||
|
Registered: true,
|
||||||
|
RegisterMethod: "authKey",
|
||||||
|
IPAddress: "100.64.0.2",
|
||||||
|
AuthKeyID: uint(pak2.ID),
|
||||||
|
}
|
||||||
|
h.db.Save(&m2)
|
||||||
|
|
||||||
|
_, err = h.GetMachine(n2.Name, m2.Name)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
m3 := Machine{
|
||||||
|
ID: 2,
|
||||||
|
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||||
|
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||||
|
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||||
|
Name: "test_get_shared_nodes_3",
|
||||||
|
NamespaceID: n3.ID,
|
||||||
|
Registered: true,
|
||||||
|
RegisterMethod: "authKey",
|
||||||
|
IPAddress: "100.64.0.3",
|
||||||
|
AuthKeyID: uint(pak3.ID),
|
||||||
|
}
|
||||||
|
h.db.Save(&m3)
|
||||||
|
|
||||||
|
_, err = h.GetMachine(n3.Name, m3.Name)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
m4 := Machine{
|
||||||
|
ID: 3,
|
||||||
|
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||||
|
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||||
|
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
|
||||||
|
Name: "test_get_shared_nodes_4",
|
||||||
|
NamespaceID: n1.ID,
|
||||||
|
Registered: true,
|
||||||
|
RegisterMethod: "authKey",
|
||||||
|
IPAddress: "100.64.0.4",
|
||||||
|
AuthKeyID: uint(pak4.ID),
|
||||||
|
}
|
||||||
|
h.db.Save(&m4)
|
||||||
|
|
||||||
|
_, err = h.GetMachine(n1.Name, m4.Name)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
p1s, err := h.getPeers(m1)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(len(*p1s), check.Equals, 1) // nodes 1 and 4
|
||||||
|
|
||||||
|
err = h.AddSharedMachineToNamespace(&m2, n1)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
p1sAfter, err := h.getPeers(m1)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(len(*p1sAfter), check.Equals, 2) // nodes 1, 2, 4
|
||||||
|
|
||||||
|
pAlone, err := h.getPeers(m3)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(len(*pAlone), check.Equals, 0) // node 3 is alone
|
||||||
|
}
|
Loading…
Reference in a new issue