Skip to content

Commit 2d4543b

Browse files
willnorriskdevan
andcommitted
add config option for node hostname
this overrides the name used to refer to the node in the caddy config, and is mostly useful because it can include environment variables. Closes #18 Co-authored-by: kdevan <kaidevan@gmail.com> Signed-off-by: Will Norris <will@tailscale.com>
1 parent 21b5e30 commit 2d4543b

File tree

3 files changed

+117
-1
lines changed

3 files changed

+117
-1
lines changed

app.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ type Node struct {
4747
// Ephemeral specifies whether the node should be registered as ephemeral.
4848
Ephemeral bool `json:"ephemeral,omitempty" caddy:"namespace=tailscale.ephemeral"`
4949

50+
// Hostname is the hostname to use when registering the node.
51+
Hostname string `json:"hostname,omitempty" caddy:"namespace=tailscale.hostname"`
52+
5053
name string
5154
}
5255

@@ -137,6 +140,11 @@ func parseNodeConfig(d *caddyfile.Dispenser) (Node, error) {
137140
node.ControlURL = segment.Val()
138141
case "ephemeral":
139142
node.Ephemeral = true
143+
case "hostname":
144+
if !segment.NextArg() {
145+
return node, segment.ArgErr()
146+
}
147+
node.Hostname = segment.Val()
140148
default:
141149
return node, segment.Errf("unrecognized subdirective: %s", segment.Val())
142150
}

module.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@ func getNode(ctx caddy.Context, name string) (*tailscaleNode, error) {
103103

104104
s, _, err := nodes.LoadOrNew(name, func() (caddy.Destructor, error) {
105105
s := &tsnet.Server{
106-
Hostname: name,
107106
Logf: func(format string, args ...any) {
108107
app.logger.Sugar().Debugf(format, args...)
109108
},
@@ -116,6 +115,9 @@ func getNode(ctx caddy.Context, name string) (*tailscaleNode, error) {
116115
if s.ControlURL, err = getControlURL(name, app); err != nil {
117116
return nil, err
118117
}
118+
if s.Hostname, err = getHostname(name, app); err != nil {
119+
return nil, err
120+
}
119121

120122
if name != "" {
121123
// Set config directory for tsnet. By default, tsnet will use the name of the
@@ -182,6 +184,19 @@ func getEphemeral(name string, app *App) bool {
182184
return app.Ephemeral
183185
}
184186

187+
func getHostname(name string, app *App) (string, error) {
188+
if app == nil {
189+
return name, nil
190+
}
191+
if node, ok := app.Nodes[name]; ok {
192+
if node.Hostname != "" {
193+
return repl.ReplaceOrErr(node.Hostname, true, true)
194+
}
195+
}
196+
197+
return name, nil
198+
}
199+
185200
// tailscaleNode is a wrapper around a tsnet.Server that provides a fully self-contained Tailscale node.
186201
// This node can listen on the tailscale network interface, or be used to connect to other nodes in the tailnet.
187202
type tailscaleNode struct {

module_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,99 @@ func Test_GetAuthKey(t *testing.T) {
8181
}
8282
}
8383

84+
func Test_GetControlURL(t *testing.T) {
85+
const nodeName = "node"
86+
tests := map[string]struct {
87+
env map[string]string // env vars to set
88+
defaultURL string // default control_url in caddy config
89+
nodeURL string // node control_url in caddy config
90+
want string
91+
}{
92+
"default empty URL": {
93+
want: "",
94+
},
95+
"custom URL from app config": {
96+
defaultURL: "http://custom.example.com",
97+
want: "http://custom.example.com",
98+
},
99+
"custom URL from node config": {
100+
defaultURL: "xxx",
101+
nodeURL: "http://custom.example.com",
102+
want: "http://custom.example.com",
103+
},
104+
"custom URL from env on app config": {
105+
env: map[string]string{"CONTROL_URL": "http://env.example.com"},
106+
defaultURL: "{env.CONTROL_URL}",
107+
want: "http://env.example.com",
108+
},
109+
"custom URL from env on node config": {
110+
env: map[string]string{"CONTROL_URL": "http://env.example.com"},
111+
defaultURL: "xxx",
112+
nodeURL: "{env.CONTROL_URL}",
113+
want: "http://env.example.com",
114+
},
115+
}
116+
for tn, tt := range tests {
117+
t.Run(tn, func(t *testing.T) {
118+
app := &App{
119+
ControlURL: tt.defaultURL,
120+
Nodes: make(map[string]Node),
121+
}
122+
if tt.nodeURL != "" {
123+
app.Nodes[nodeName] = Node{
124+
ControlURL: tt.nodeURL,
125+
}
126+
}
127+
app.Provision(caddy.Context{})
128+
for k, v := range tt.env {
129+
t.Setenv(k, v)
130+
}
131+
132+
got, _ := getControlURL(nodeName, app)
133+
if got != tt.want {
134+
t.Errorf("GetControlURL() = %v, want %v", got, tt.want)
135+
}
136+
})
137+
}
138+
}
139+
func Test_GetHostname(t *testing.T) {
140+
const nodeName = "node"
141+
tests := map[string]struct {
142+
env map[string]string // env vars to set
143+
hostname string // hostname value in caddy config
144+
want string
145+
}{
146+
"default hostname from node name": {
147+
want: nodeName,
148+
},
149+
"custom hostname from node config": {
150+
hostname: "custom",
151+
want: "custom",
152+
},
153+
"custom hostname with env vars": {
154+
env: map[string]string{"REGION": "eu", "ENV": "prod"},
155+
hostname: "custom-{env.REGION}-{env.ENV}",
156+
want: "custom-eu-prod",
157+
},
158+
}
159+
for tn, tt := range tests {
160+
t.Run(tn, func(t *testing.T) {
161+
app := &App{Nodes: map[string]Node{
162+
nodeName: {Hostname: tt.hostname},
163+
}}
164+
app.Provision(caddy.Context{})
165+
for k, v := range tt.env {
166+
t.Setenv(k, v)
167+
}
168+
169+
got, _ := getHostname(nodeName, app)
170+
if got != tt.want {
171+
t.Errorf("GetHostname() = %v, want %v", got, tt.want)
172+
}
173+
})
174+
}
175+
}
176+
84177
func Test_Listen(t *testing.T) {
85178
must.Do(caddy.Run(new(caddy.Config)))
86179
ctx := caddy.ActiveContext()

0 commit comments

Comments
 (0)