Skip to content

Commit

Permalink
juanfont#2140 Fixed reflection of hostname change (juanfont#2199)
Browse files Browse the repository at this point in the history
* juanfont#2140 Fixed updating of hostname and givenName when it is updated in HostInfo

* juanfont#2140 Added integration tests

* juanfont#2140 Fix unit tests

* Changed IsAutomaticNameMode to GivenNameHasBeenChanged. Fixed errors in files according to golangci-lint rules
  • Loading branch information
hopleus committed Oct 25, 2024
1 parent cb4d984 commit 6b14f6e
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 2 deletions.
1 change: 1 addition & 0 deletions .github/workflows/test-integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ jobs:
- TestEphemeral2006DeletedTooQuickly
- TestPingAllByHostname
- TestTaildrop
- TestUpdateHostnameFromClient
- TestExpireNode
- TestNodeOnlineStatus
- TestPingAllByIPManyUpDown
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- Allow nodes to use SSH agent forwarding [#2145](https://github.com/juanfont/headscale/pull/2145)
- Fixed processing of fields in post request in MoveNode rpc [#2179](https://github.com/juanfont/headscale/pull/2179)
- Added conversion of 'Hostname' to 'givenName' in a node with FQDN rules applied [#2198](https://github.com/juanfont/headscale/pull/2198)
- Fixed updating of hostname and givenName when it is updated in HostInfo [#2199](https://github.com/juanfont/headscale/pull/2199)

## 0.23.0 (2024-09-18)

Expand Down
11 changes: 9 additions & 2 deletions hscontrol/poll.go
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ func (m *mapSession) handleEndpointUpdate() {

// Check if the Hostinfo of the node has changed.
// If it has changed, check if there has been a change to
// the routable IPs of the host and update update them in
// the routable IPs of the host and update them in
// the database. Then send a Changed update
// (containing the whole node object) to peers to inform about
// the route change.
Expand Down Expand Up @@ -510,6 +510,12 @@ func (m *mapSession) handleEndpointUpdate() {
m.node.ID)
}

// Check if there has been a change to Hostname and update them
// in the database. Then send a Changed update
// (containing the whole node object) to peers to inform about
// the hostname change.
m.node.ApplyHostnameFromHostInfo(m.req.Hostinfo)

if err := m.h.db.DB.Save(m.node).Error; err != nil {
m.errf(err, "Failed to persist/update node in the database")
http.Error(m.w, "", http.StatusInternalServerError)
Expand All @@ -526,7 +532,8 @@ func (m *mapSession) handleEndpointUpdate() {
ChangeNodes: []types.NodeID{m.node.ID},
Message: "called from handlePoll -> update",
},
m.node.ID)
m.node.ID,
)

m.w.WriteHeader(http.StatusOK)
mapResponseEndpointUpdates.WithLabelValues("ok").Inc()
Expand Down
20 changes: 20 additions & 0 deletions hscontrol/types/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ type (
Nodes []*Node
)

// GivenNameHasBeenChanged returns whether the `givenName` can be automatically changed based on the `Hostname` of the node.
func (node *Node) GivenNameHasBeenChanged() bool {
return node.GivenName == util.ConvertWithFQDNRules(node.Hostname)
}

// IsExpired returns whether the node registration has expired.
func (node Node) IsExpired() bool {
// If Expiry is not set, the client has not indicated that
Expand Down Expand Up @@ -354,6 +359,21 @@ func (node *Node) RegisterMethodToV1Enum() v1.RegisterMethod {
}
}

// ApplyHostnameFromHostInfo takes a Hostinfo struct and updates the node.
func (node *Node) ApplyHostnameFromHostInfo(hostInfo *tailcfg.Hostinfo) {
if hostInfo == nil {
return
}

if node.Hostname != hostInfo.Hostname {
if node.GivenNameHasBeenChanged() {
node.GivenName = util.ConvertWithFQDNRules(hostInfo.Hostname)
}

node.Hostname = hostInfo.Hostname
}
}

// ApplyPeerChange takes a PeerChange struct and updates the node.
func (node *Node) ApplyPeerChange(change *tailcfg.PeerChange) {
if change.Key != nil {
Expand Down
60 changes: 60 additions & 0 deletions hscontrol/types/node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,66 @@ func TestPeerChangeFromMapRequest(t *testing.T) {
}
}

func TestApplyHostnameFromHostInfo(t *testing.T) {
tests := []struct {
name string
nodeBefore Node
change *tailcfg.Hostinfo
want Node
}{
{
name: "hostinfo-not-exists",
nodeBefore: Node{
GivenName: "manual-test.local",
Hostname: "TestHost.Local",
},
change: nil,
want: Node{
GivenName: "manual-test.local",
Hostname: "TestHost.Local",
},
},
{
name: "hostinfo-exists-no-automatic-givenName",
nodeBefore: Node{
GivenName: "manual-test.local",
Hostname: "TestHost.Local",
},
change: &tailcfg.Hostinfo{
Hostname: "NewHostName.Local",
},
want: Node{
GivenName: "manual-test.local",
Hostname: "NewHostName.Local",
},
},
{
name: "hostinfo-exists-automatic-givenName",
nodeBefore: Node{
GivenName: "automaticname.test",
Hostname: "AutomaticName.Test",
},
change: &tailcfg.Hostinfo{
Hostname: "NewHostName.Local",
},
want: Node{
GivenName: "newhostname.local",
Hostname: "NewHostName.Local",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.nodeBefore.ApplyHostnameFromHostInfo(tt.change)

if diff := cmp.Diff(tt.want, tt.nodeBefore, util.Comparers...); diff != "" {
t.Errorf("Patch unexpected result (-want +got):\n%s", diff)
}
})
}
}

func TestApplyPeerChange(t *testing.T) {
tests := []struct {
name string
Expand Down
130 changes: 130 additions & 0 deletions integration/general_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import (
"encoding/json"
"fmt"
"net/netip"
"strconv"
"strings"
"testing"
"time"

v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/juanfont/headscale/integration/hsic"
"github.com/juanfont/headscale/integration/tsic"
"github.com/rs/zerolog/log"
Expand Down Expand Up @@ -654,6 +656,134 @@ func TestTaildrop(t *testing.T) {
}
}

func TestUpdateHostnameFromClient(t *testing.T) {
IntegrationSkip(t)
t.Parallel()

user := "update-hostname-from-client"

hostnames := map[string]string{
"1": "user1-host",
"2": "User2-Host",
"3": "user3-host",
}

scenario, err := NewScenario(dockertestMaxWait())
assertNoErrf(t, "failed to create scenario: %s", err)
defer scenario.ShutdownAssertNoPanics(t)

spec := map[string]int{
user: 3,
}

err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("updatehostname"))
assertNoErrHeadscaleEnv(t, err)

allClients, err := scenario.ListTailscaleClients()
assertNoErrListClients(t, err)

err = scenario.WaitForTailscaleSync()
assertNoErrSync(t, err)

headscale, err := scenario.Headscale()
assertNoErrGetHeadscale(t, err)

// update hostnames using the up command
for _, client := range allClients {
status, err := client.Status()
assertNoErr(t, err)

command := []string{
"tailscale",
"set",
"--hostname=" + hostnames[string(status.Self.ID)],
}
_, _, err = client.Execute(command)
assertNoErrf(t, "failed to set hostname: %s", err)
}

err = scenario.WaitForTailscaleSync()
assertNoErrSync(t, err)

var nodes []*v1.Node
err = executeAndUnmarshal(
headscale,
[]string{
"headscale",
"node",
"list",
"--output",
"json",
},
&nodes,
)

assertNoErr(t, err)
assert.Len(t, nodes, 3)

for _, node := range nodes {
hostname := hostnames[strconv.FormatUint(node.GetId(), 10)]
assert.Equal(t, hostname, node.GetName())
assert.Equal(t, util.ConvertWithFQDNRules(hostname), node.GetGivenName())
}

// Rename givenName in nodes
for _, node := range nodes {
givenName := fmt.Sprintf("%d-givenname", node.GetId())
_, err = headscale.Execute(
[]string{
"headscale",
"node",
"rename",
givenName,
"--identifier",
strconv.FormatUint(node.GetId(), 10),
})
assertNoErr(t, err)
}

time.Sleep(5 * time.Second)

// Verify that the clients can see the new hostname, but no givenName
for _, client := range allClients {
status, err := client.Status()
assertNoErr(t, err)

command := []string{
"tailscale",
"set",
"--hostname=" + hostnames[string(status.Self.ID)] + "NEW",
}
_, _, err = client.Execute(command)
assertNoErrf(t, "failed to set hostname: %s", err)
}

err = scenario.WaitForTailscaleSync()
assertNoErrSync(t, err)

err = executeAndUnmarshal(
headscale,
[]string{
"headscale",
"node",
"list",
"--output",
"json",
},
&nodes,
)

assertNoErr(t, err)
assert.Len(t, nodes, 3)

for _, node := range nodes {
hostname := hostnames[strconv.FormatUint(node.GetId(), 10)]
givenName := fmt.Sprintf("%d-givenname", node.GetId())
assert.Equal(t, hostname+"NEW", node.GetName())
assert.Equal(t, givenName, node.GetGivenName())
}
}

func TestExpireNode(t *testing.T) {
IntegrationSkip(t)
t.Parallel()
Expand Down

0 comments on commit 6b14f6e

Please sign in to comment.