Skip to content

Commit 1e349f0

Browse files
authored
feat(cli): allow specifying name of provisioner daemon (#11077)
- Adds a --name argument to provisionerd start - Plumbs through name to integrated and external provisioners - Defaults to hostname if not specified for external, hostname-N for integrated - Adds cliutil.Hostname
1 parent 8aea604 commit 1e349f0

16 files changed

+170
-22
lines changed

cli/cliutil/hostname.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package cliutil
2+
3+
import (
4+
"os"
5+
"strings"
6+
"sync"
7+
)
8+
9+
var (
10+
hostname string
11+
hostnameOnce sync.Once
12+
)
13+
14+
// Hostname returns the hostname of the machine, lowercased,
15+
// with any trailing domain suffix stripped.
16+
// It is cached after the first call.
17+
// If the hostname cannot be determined, for any reason,
18+
// localhost will be returned instead.
19+
func Hostname() string {
20+
hostnameOnce.Do(func() { hostname = getHostname() })
21+
return hostname
22+
}
23+
24+
func getHostname() string {
25+
h, err := os.Hostname()
26+
if err != nil {
27+
// Something must be very wrong if this fails.
28+
// We'll just return localhost and hope for the best.
29+
return "localhost"
30+
}
31+
32+
// On some platforms, the hostname can be an FQDN. We only want the hostname.
33+
if idx := strings.Index(h, "."); idx != -1 {
34+
h = h[:idx]
35+
}
36+
37+
// For the sake of consistency, we also want to lowercase the hostname.
38+
// Per RFC 4343, DNS lookups must be case-insensitive.
39+
return strings.ToLower(h)
40+
}

cli/server.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import (
6262
"github.com/coder/coder/v2/buildinfo"
6363
"github.com/coder/coder/v2/cli/clibase"
6464
"github.com/coder/coder/v2/cli/cliui"
65+
"github.com/coder/coder/v2/cli/cliutil"
6566
"github.com/coder/coder/v2/cli/config"
6667
"github.com/coder/coder/v2/coderd"
6768
"github.com/coder/coder/v2/coderd/autobuild"
@@ -86,6 +87,7 @@ import (
8687
"github.com/coder/coder/v2/coderd/unhanger"
8788
"github.com/coder/coder/v2/coderd/updatecheck"
8889
"github.com/coder/coder/v2/coderd/util/slice"
90+
stringutil "github.com/coder/coder/v2/coderd/util/strings"
8991
"github.com/coder/coder/v2/coderd/workspaceapps"
9092
"github.com/coder/coder/v2/codersdk"
9193
"github.com/coder/coder/v2/cryptorand"
@@ -875,9 +877,14 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
875877
defer provisionerdWaitGroup.Wait()
876878
provisionerdMetrics := provisionerd.NewMetrics(options.PrometheusRegistry)
877879
for i := int64(0); i < vals.Provisioner.Daemons.Value(); i++ {
880+
suffix := fmt.Sprintf("%d", i)
881+
// The suffix is added to the hostname, so we may need to trim to fit into
882+
// the 64 character limit.
883+
hostname := stringutil.Truncate(cliutil.Hostname(), 63-len(suffix))
884+
name := fmt.Sprintf("%s-%s", hostname, suffix)
878885
daemonCacheDir := filepath.Join(cacheDir, fmt.Sprintf("provisioner-%d", i))
879886
daemon, err := newProvisionerDaemon(
880-
ctx, coderAPI, provisionerdMetrics, logger, vals, daemonCacheDir, errCh, &provisionerdWaitGroup,
887+
ctx, coderAPI, provisionerdMetrics, logger, vals, daemonCacheDir, errCh, &provisionerdWaitGroup, name,
881888
)
882889
if err != nil {
883890
return xerrors.Errorf("create provisioner daemon: %w", err)
@@ -1243,6 +1250,7 @@ func newProvisionerDaemon(
12431250
cacheDir string,
12441251
errCh chan error,
12451252
wg *sync.WaitGroup,
1253+
name string,
12461254
) (srv *provisionerd.Server, err error) {
12471255
ctx, cancel := context.WithCancel(ctx)
12481256
defer func() {
@@ -1334,9 +1342,9 @@ func newProvisionerDaemon(
13341342
return provisionerd.New(func(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) {
13351343
// This debounces calls to listen every second. Read the comment
13361344
// in provisionerdserver.go to learn more!
1337-
return coderAPI.CreateInMemoryProvisionerDaemon(ctx)
1345+
return coderAPI.CreateInMemoryProvisionerDaemon(ctx, name)
13381346
}, &provisionerd.Options{
1339-
Logger: logger.Named("provisionerd"),
1347+
Logger: logger.Named(fmt.Sprintf("provisionerd-%s", name)),
13401348
UpdateInterval: time.Second,
13411349
ForceCancelInterval: cfg.Provisioner.ForceCancelInterval.Value(),
13421350
Connector: connector,

coderd/coderd.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import (
2121
"github.com/go-chi/chi/v5/middleware"
2222
"github.com/google/uuid"
2323
"github.com/klauspost/compress/zstd"
24-
"github.com/moby/moby/pkg/namesgenerator"
2524
"github.com/prometheus/client_golang/prometheus"
2625
httpSwagger "github.com/swaggo/http-swagger/v2"
2726
"go.opentelemetry.io/otel/trace"
@@ -1150,7 +1149,7 @@ func compressHandler(h http.Handler) http.Handler {
11501149

11511150
// CreateInMemoryProvisionerDaemon is an in-memory connection to a provisionerd.
11521151
// Useful when starting coderd and provisionerd in the same process.
1153-
func (api *API) CreateInMemoryProvisionerDaemon(ctx context.Context) (client proto.DRPCProvisionerDaemonClient, err error) {
1152+
func (api *API) CreateInMemoryProvisionerDaemon(ctx context.Context, name string) (client proto.DRPCProvisionerDaemonClient, err error) {
11541153
tracer := api.TracerProvider.Tracer(tracing.TracerName)
11551154
clientSession, serverSession := provisionersdk.MemTransportPipe()
11561155
defer func() {
@@ -1165,9 +1164,8 @@ func (api *API) CreateInMemoryProvisionerDaemon(ctx context.Context) (client pro
11651164
}
11661165

11671166
mux := drpcmux.New()
1168-
name := namesgenerator.GetRandomName(1)
1167+
api.Logger.Info(ctx, "starting in-memory provisioner daemon", slog.F("name", name))
11691168
logger := api.Logger.Named(fmt.Sprintf("inmem-provisionerd-%s", name))
1170-
logger.Info(ctx, "starting in-memory provisioner daemon")
11711169
srv, err := provisionerdserver.NewServer(
11721170
api.ctx,
11731171
api.AccessURL,

coderd/coderdtest/coderdtest.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,7 @@ func NewProvisionerDaemon(t testing.TB, coderAPI *coderd.API) io.Closer {
530530
}()
531531

532532
daemon := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) {
533-
return coderAPI.CreateInMemoryProvisionerDaemon(ctx)
533+
return coderAPI.CreateInMemoryProvisionerDaemon(ctx, t.Name())
534534
}, &provisionerd.Options{
535535
Logger: coderAPI.Logger.Named("provisionerd").Leveled(slog.LevelDebug),
536536
UpdateInterval: 250 * time.Millisecond,
@@ -567,6 +567,8 @@ func NewExternalProvisionerDaemon(t testing.TB, client *codersdk.Client, org uui
567567

568568
daemon := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) {
569569
return client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
570+
ID: uuid.New(),
571+
Name: t.Name(),
570572
Organization: org,
571573
Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho},
572574
Tags: tags,

coderd/util/strings/strings.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,14 @@ func JoinWithConjunction(s []string) string {
1717
s[last],
1818
)
1919
}
20+
21+
// Truncate returns the first n characters of s.
22+
func Truncate(s string, n int) string {
23+
if n < 1 {
24+
return ""
25+
}
26+
if len(s) <= n {
27+
return s
28+
}
29+
return s[:n]
30+
}

coderd/util/strings/strings_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,27 @@ func TestJoinWithConjunction(t *testing.T) {
1414
require.Equal(t, "foo and bar", strings.JoinWithConjunction([]string{"foo", "bar"}))
1515
require.Equal(t, "foo, bar and baz", strings.JoinWithConjunction([]string{"foo", "bar", "baz"}))
1616
}
17+
18+
func TestTruncate(t *testing.T) {
19+
t.Parallel()
20+
21+
for _, tt := range []struct {
22+
s string
23+
n int
24+
expected string
25+
}{
26+
{"foo", 4, "foo"},
27+
{"foo", 3, "foo"},
28+
{"foo", 2, "fo"},
29+
{"foo", 1, "f"},
30+
{"foo", 0, ""},
31+
{"foo", -1, ""},
32+
} {
33+
tt := tt
34+
t.Run(tt.expected, func(t *testing.T) {
35+
t.Parallel()
36+
actual := strings.Truncate(tt.s, tt.n)
37+
require.Equal(t, tt.expected, actual)
38+
})
39+
}
40+
}

codersdk/provisionerdaemons.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ func (c *Client) provisionerJobLogsAfter(ctx context.Context, path string, after
177177
type ServeProvisionerDaemonRequest struct {
178178
// ID is a unique ID for a provisioner daemon.
179179
ID uuid.UUID `json:"id" format:"uuid"`
180+
// Name is the human-readable unique identifier for the daemon.
181+
Name string `json:"name" example:"my-cool-provisioner-daemon"`
180182
// Organization is the organization for the URL. At present provisioner daemons ARE NOT scoped to organizations
181183
// and so the organization ID is optional.
182184
Organization uuid.UUID `json:"organization" format:"uuid"`
@@ -198,6 +200,7 @@ func (c *Client) ServeProvisionerDaemon(ctx context.Context, req ServeProvisione
198200
}
199201
query := serverURL.Query()
200202
query.Add("id", req.ID.String())
203+
query.Add("name", req.Name)
201204
for _, provisioner := range req.Provisioners {
202205
query.Add("provisioner", string(provisioner))
203206
}

docs/cli/provisionerd_start.md

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

enterprise/cli/provisionerdaemons.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"context"
77
"fmt"
88
"os"
9+
"regexp"
910
"time"
1011

1112
"github.com/google/uuid"
@@ -16,6 +17,7 @@ import (
1617
agpl "github.com/coder/coder/v2/cli"
1718
"github.com/coder/coder/v2/cli/clibase"
1819
"github.com/coder/coder/v2/cli/cliui"
20+
"github.com/coder/coder/v2/cli/cliutil"
1921
"github.com/coder/coder/v2/coderd/database"
2022
"github.com/coder/coder/v2/coderd/provisionerdserver"
2123
"github.com/coder/coder/v2/codersdk"
@@ -41,13 +43,24 @@ func (r *RootCmd) provisionerDaemons() *clibase.Cmd {
4143
return cmd
4244
}
4345

46+
func validateProvisionerDaemonName(name string) error {
47+
if len(name) > 64 {
48+
return xerrors.Errorf("name cannot be greater than 64 characters in length")
49+
}
50+
if ok, err := regexp.MatchString(`^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$`, name); err != nil || !ok {
51+
return xerrors.Errorf("name %q is not a valid hostname", name)
52+
}
53+
return nil
54+
}
55+
4456
func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd {
4557
var (
4658
cacheDir string
4759
rawTags []string
4860
pollInterval time.Duration
4961
pollJitter time.Duration
5062
preSharedKey string
63+
name string
5164
)
5265
client := new(codersdk.Client)
5366
cmd := &clibase.Cmd{
@@ -68,6 +81,14 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd {
6881
return err
6982
}
7083

84+
if name == "" {
85+
name = cliutil.Hostname()
86+
}
87+
88+
if err := validateProvisionerDaemonName(name); err != nil {
89+
return err
90+
}
91+
7192
logger := slog.Make(sloghuman.Sink(inv.Stderr))
7293
if ok, _ := inv.ParsedFlags().GetBool("verbose"); ok {
7394
logger = logger.Leveled(slog.LevelDebug)
@@ -122,15 +143,16 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd {
122143
}
123144
}()
124145

125-
logger.Info(ctx, "starting provisioner daemon", slog.F("tags", tags))
146+
logger.Info(ctx, "starting provisioner daemon", slog.F("tags", tags), slog.F("name", name))
126147

127148
connector := provisionerd.LocalProvisioners{
128149
string(database.ProvisionerTypeTerraform): proto.NewDRPCProvisionerClient(terraformClient),
129150
}
130151
id := uuid.New()
131152
srv := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) {
132153
return client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
133-
ID: id,
154+
ID: id,
155+
Name: name,
134156
Provisioners: []codersdk.ProvisionerType{
135157
codersdk.ProvisionerTypeTerraform,
136158
},
@@ -205,6 +227,13 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd {
205227
Description: "Pre-shared key to authenticate with Coder server.",
206228
Value: clibase.StringOf(&preSharedKey),
207229
},
230+
{
231+
Flag: "name",
232+
Env: "CODER_PROVISIONER_DAEMON_NAME",
233+
Description: "Name of this provisioner daemon. Defaults to the current hostname without FQDN.",
234+
Value: clibase.StringOf(&name),
235+
Default: "",
236+
},
208237
}
209238

210239
return cmd

enterprise/cli/provisionerdaemons_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,15 @@ func TestProvisionerDaemon_PSK(t *testing.T) {
2626
},
2727
},
2828
})
29-
inv, conf := newCLI(t, "provisionerd", "start", "--psk=provisionersftw")
29+
inv, conf := newCLI(t, "provisionerd", "start", "--psk=provisionersftw", "--name=matt-daemon")
3030
err := conf.URL().Write(client.URL.String())
3131
require.NoError(t, err)
3232
pty := ptytest.New(t).Attach(inv)
3333
ctx, cancel := context.WithTimeout(inv.Context(), testutil.WaitLong)
3434
defer cancel()
3535
clitest.Start(t, inv)
3636
pty.ExpectMatchContext(ctx, "starting provisioner daemon")
37+
pty.ExpectMatchContext(ctx, "matt-daemon")
3738
}
3839

3940
func TestProvisionerDaemon_SessionToken(t *testing.T) {

enterprise/cli/testdata/coder_provisionerd_start_--help.golden

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ OPTIONS:
99
-c, --cache-dir string, $CODER_CACHE_DIRECTORY (default: [cache dir])
1010
Directory to store cached data.
1111

12+
--name string, $CODER_PROVISIONER_DAEMON_NAME
13+
Name of this provisioner daemon. Defaults to the current hostname
14+
without FQDN.
15+
1216
--poll-interval duration, $CODER_PROVISIONERD_POLL_INTERVAL (default: 1s)
1317
Deprecated and ignored.
1418

enterprise/coderd/provisionerdaemons.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,13 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request)
178178
}
179179
}
180180

181+
name := namesgenerator.GetRandomName(10)
182+
if vals, ok := r.URL.Query()["name"]; ok && len(vals) > 0 {
183+
name = vals[0]
184+
} else {
185+
api.Logger.Warn(ctx, "unnamed provisioner daemon")
186+
}
187+
181188
tags, authorized := api.provisionerDaemonAuth.authorize(r, tags)
182189
if !authorized {
183190
api.Logger.Warn(ctx, "unauthorized provisioner daemon serve request", slog.F("tags", tags))
@@ -206,7 +213,6 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request)
206213
}
207214
}
208215

209-
name := namesgenerator.GetRandomName(1)
210216
log := api.Logger.With(
211217
slog.F("name", name),
212218
slog.F("provisioners", provisioners),

0 commit comments

Comments
 (0)