diff --git a/enterprise/cli/provisionerdaemons.go b/enterprise/cli/provisionerdaemons.go index 286e53a34bb9f..a10d1cc261660 100644 --- a/enterprise/cli/provisionerdaemons.go +++ b/enterprise/cli/provisionerdaemons.go @@ -1,38 +1,6 @@ -//go:build !slim - package cli -import ( - "context" - "errors" - "fmt" - "net/http" - "os" - "regexp" - "time" - - "github.com/google/uuid" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/collectors" - "github.com/prometheus/client_golang/prometheus/promhttp" - "golang.org/x/xerrors" - - "cdr.dev/slog" - "cdr.dev/slog/sloggers/sloghuman" - agpl "github.com/coder/coder/v2/cli" - "github.com/coder/coder/v2/cli/clilog" - "github.com/coder/coder/v2/cli/cliui" - "github.com/coder/coder/v2/cli/cliutil" - "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/codersdk/drpc" - "github.com/coder/coder/v2/provisioner/terraform" - "github.com/coder/coder/v2/provisionerd" - provisionerdproto "github.com/coder/coder/v2/provisionerd/proto" - "github.com/coder/coder/v2/provisionersdk" - "github.com/coder/coder/v2/provisionersdk/proto" - "github.com/coder/serpent" -) +import "github.com/coder/serpent" func (r *RootCmd) provisionerDaemons() *serpent.Command { cmd := &serpent.Command{ @@ -50,337 +18,3 @@ func (r *RootCmd) provisionerDaemons() *serpent.Command { return cmd } - -func validateProvisionerDaemonName(name string) error { - if len(name) > 64 { - return xerrors.Errorf("name cannot be greater than 64 characters in length") - } - if ok, err := regexp.MatchString(`^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$`, name); err != nil || !ok { - return xerrors.Errorf("name %q is not a valid hostname", name) - } - return nil -} - -func (r *RootCmd) provisionerDaemonStart() *serpent.Command { - var ( - cacheDir string - logHuman string - logJSON string - logStackdriver string - logFilter []string - name string - rawTags []string - pollInterval time.Duration - pollJitter time.Duration - preSharedKey string - verbose bool - - prometheusEnable bool - prometheusAddress string - ) - orgContext := agpl.NewOrganizationContext() - client := new(codersdk.Client) - cmd := &serpent.Command{ - Use: "start", - Short: "Run a provisioner daemon", - Middleware: serpent.Chain( - // disable checks and warnings because this command starts a daemon; it is - // not meant for humans typing commands. Furthermore, the checks are - // incompatible with PSK auth that this command uses - r.InitClient(client), - ), - Handler: func(inv *serpent.Invocation) error { - ctx, cancel := context.WithCancel(inv.Context()) - defer cancel() - - stopCtx, stopCancel := inv.SignalNotifyContext(ctx, agpl.StopSignalsNoInterrupt...) - defer stopCancel() - interruptCtx, interruptCancel := inv.SignalNotifyContext(ctx, agpl.InterruptSignals...) - defer interruptCancel() - - // This can fail to get the current organization - // if the client is not authenticated as a user, - // like when only PSK is provided. - // This will be cleaner once PSK is replaced - // with org scoped authentication tokens. - org, err := orgContext.Selected(inv, client) - if err != nil { - var cErr *codersdk.Error - if !errors.As(err, &cErr) || cErr.StatusCode() != http.StatusUnauthorized { - return xerrors.Errorf("current organization: %w", err) - } - - if preSharedKey == "" { - return xerrors.New("must provide a pre-shared key when not authenticated as a user") - } - - org = codersdk.Organization{MinimalOrganization: codersdk.MinimalOrganization{ID: uuid.Nil}} - if orgContext.FlagSelect != "" { - // If we are using PSK, we can't fetch the organization - // to validate org name so we need the user to provide - // a valid organization ID. - orgID, err := uuid.Parse(orgContext.FlagSelect) - if err != nil { - return xerrors.New("must provide an org ID when not authenticated as a user and organization is specified") - } - org = codersdk.Organization{MinimalOrganization: codersdk.MinimalOrganization{ID: orgID}} - } - } - - tags, err := agpl.ParseProvisionerTags(rawTags) - if err != nil { - return err - } - - if name == "" { - name = cliutil.Hostname() - } - - if err := validateProvisionerDaemonName(name); err != nil { - return err - } - - logOpts := []clilog.Option{ - clilog.WithFilter(logFilter...), - clilog.WithHuman(logHuman), - clilog.WithJSON(logJSON), - clilog.WithStackdriver(logStackdriver), - } - if verbose { - logOpts = append(logOpts, clilog.WithVerbose()) - } - - logger, closeLogger, err := clilog.New(logOpts...).Build(inv) - if err != nil { - // Fall back to a basic logger - logger = slog.Make(sloghuman.Sink(inv.Stderr)) - logger.Error(ctx, "failed to initialize logger", slog.Error(err)) - } else { - defer closeLogger() - } - - if len(tags) == 0 { - logger.Info(ctx, "note: untagged provisioners can only pick up jobs from untagged templates") - } - - // When authorizing with a PSK, we automatically scope the provisionerd - // to organization. Scoping to user with PSK auth is not a valid configuration. - if preSharedKey != "" { - logger.Info(ctx, "psk auth automatically sets tag "+provisionersdk.TagScope+"="+provisionersdk.ScopeOrganization) - tags[provisionersdk.TagScope] = provisionersdk.ScopeOrganization - } - - err = os.MkdirAll(cacheDir, 0o700) - if err != nil { - return xerrors.Errorf("mkdir %q: %w", cacheDir, err) - } - - tempDir, err := os.MkdirTemp("", "provisionerd") - if err != nil { - return err - } - - terraformClient, terraformServer := drpc.MemTransportPipe() - go func() { - <-ctx.Done() - _ = terraformClient.Close() - _ = terraformServer.Close() - }() - - errCh := make(chan error, 1) - go func() { - defer cancel() - - err := terraform.Serve(ctx, &terraform.ServeOptions{ - ServeOptions: &provisionersdk.ServeOptions{ - Listener: terraformServer, - Logger: logger.Named("terraform"), - WorkDirectory: tempDir, - }, - CachePath: cacheDir, - }) - if err != nil && !xerrors.Is(err, context.Canceled) { - select { - case errCh <- err: - default: - } - } - }() - - var metrics *provisionerd.Metrics - if prometheusEnable { - logger.Info(ctx, "starting Prometheus endpoint", slog.F("address", prometheusAddress)) - - prometheusRegistry := prometheus.NewRegistry() - prometheusRegistry.MustRegister(collectors.NewGoCollector()) - prometheusRegistry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) - - m := provisionerd.NewMetrics(prometheusRegistry) - m.Runner.NumDaemons.Set(float64(1)) // Set numDaemons to 1 as this is standalone mode. - metrics = &m - - closeFunc := agpl.ServeHandler(ctx, logger, promhttp.InstrumentMetricHandler( - prometheusRegistry, promhttp.HandlerFor(prometheusRegistry, promhttp.HandlerOpts{}), - ), prometheusAddress, "prometheus") - defer closeFunc() - } - - logger.Info(ctx, "starting provisioner daemon", slog.F("tags", tags), slog.F("name", name)) - - connector := provisionerd.LocalProvisioners{ - string(database.ProvisionerTypeTerraform): proto.NewDRPCProvisionerClient(terraformClient), - } - srv := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) { - return client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ - ID: uuid.New(), - Name: name, - Provisioners: []codersdk.ProvisionerType{ - codersdk.ProvisionerTypeTerraform, - }, - Tags: tags, - PreSharedKey: preSharedKey, - Organization: org.ID, - }) - }, &provisionerd.Options{ - Logger: logger, - UpdateInterval: 500 * time.Millisecond, - Connector: connector, - Metrics: metrics, - }) - - waitForProvisionerJobs := false - var exitErr error - select { - case <-stopCtx.Done(): - exitErr = stopCtx.Err() - _, _ = fmt.Fprintln(inv.Stdout, cliui.Bold( - "Stop caught, waiting for provisioner jobs to complete and gracefully exiting. Use ctrl+\\ to force quit", - )) - waitForProvisionerJobs = true - case <-interruptCtx.Done(): - exitErr = interruptCtx.Err() - _, _ = fmt.Fprintln(inv.Stdout, cliui.Bold( - "Interrupt caught, gracefully exiting. Use ctrl+\\ to force quit", - )) - case exitErr = <-errCh: - } - if exitErr != nil && !xerrors.Is(exitErr, context.Canceled) { - cliui.Errorf(inv.Stderr, "Unexpected error, shutting down server: %s\n", exitErr) - } - - err = srv.Shutdown(ctx, waitForProvisionerJobs) - if err != nil { - return xerrors.Errorf("shutdown: %w", err) - } - - // Shutdown does not call close. Must call it manually. - err = srv.Close() - if err != nil { - return xerrors.Errorf("close server: %w", err) - } - - cancel() - if xerrors.Is(exitErr, context.Canceled) { - return nil - } - return exitErr - }, - } - - cmd.Options = serpent.OptionSet{ - { - Flag: "cache-dir", - FlagShorthand: "c", - Env: "CODER_CACHE_DIRECTORY", - Description: "Directory to store cached data.", - Default: codersdk.DefaultCacheDir(), - Value: serpent.StringOf(&cacheDir), - }, - { - Flag: "tag", - FlagShorthand: "t", - Env: "CODER_PROVISIONERD_TAGS", - Description: "Tags to filter provisioner jobs by.", - Value: serpent.StringArrayOf(&rawTags), - }, - { - Flag: "poll-interval", - Env: "CODER_PROVISIONERD_POLL_INTERVAL", - Default: time.Second.String(), - Description: "Deprecated and ignored.", - Value: serpent.DurationOf(&pollInterval), - }, - { - Flag: "poll-jitter", - Env: "CODER_PROVISIONERD_POLL_JITTER", - Description: "Deprecated and ignored.", - Default: (100 * time.Millisecond).String(), - Value: serpent.DurationOf(&pollJitter), - }, - { - Flag: "psk", - Env: "CODER_PROVISIONER_DAEMON_PSK", - Description: "Pre-shared key to authenticate with Coder server.", - Value: serpent.StringOf(&preSharedKey), - }, - { - Flag: "name", - Env: "CODER_PROVISIONER_DAEMON_NAME", - Description: "Name of this provisioner daemon. Defaults to the current hostname without FQDN.", - Value: serpent.StringOf(&name), - Default: "", - }, - { - Flag: "verbose", - Env: "CODER_PROVISIONER_DAEMON_VERBOSE", - Description: "Output debug-level logs.", - Value: serpent.BoolOf(&verbose), - Default: "false", - }, - { - Flag: "log-human", - Env: "CODER_PROVISIONER_DAEMON_LOGGING_HUMAN", - Description: "Output human-readable logs to a given file.", - Value: serpent.StringOf(&logHuman), - Default: "/dev/stderr", - }, - { - Flag: "log-json", - Env: "CODER_PROVISIONER_DAEMON_LOGGING_JSON", - Description: "Output JSON logs to a given file.", - Value: serpent.StringOf(&logJSON), - Default: "", - }, - { - Flag: "log-stackdriver", - Env: "CODER_PROVISIONER_DAEMON_LOGGING_STACKDRIVER", - Description: "Output Stackdriver compatible logs to a given file.", - Value: serpent.StringOf(&logStackdriver), - Default: "", - }, - { - Flag: "log-filter", - Env: "CODER_PROVISIONER_DAEMON_LOG_FILTER", - Description: "Filter debug logs by matching against a given regex. Use .* to match all debug logs.", - Value: serpent.StringArrayOf(&logFilter), - Default: "", - }, - { - Flag: "prometheus-enable", - Env: "CODER_PROMETHEUS_ENABLE", - Description: "Serve prometheus metrics on the address defined by prometheus address.", - Value: serpent.BoolOf(&prometheusEnable), - Default: "false", - }, - { - Flag: "prometheus-address", - Env: "CODER_PROMETHEUS_ADDRESS", - Description: "The bind address to serve prometheus metrics.", - Value: serpent.StringOf(&prometheusAddress), - Default: "127.0.0.1:2112", - }, - } - orgContext.AttachOptions(cmd) - - return cmd -} diff --git a/enterprise/cli/provisionerdaemonstart.go b/enterprise/cli/provisionerdaemonstart.go new file mode 100644 index 0000000000000..8acff05a84e69 --- /dev/null +++ b/enterprise/cli/provisionerdaemonstart.go @@ -0,0 +1,369 @@ +//go:build !slim + +package cli + +import ( + "context" + "errors" + "fmt" + "net/http" + "os" + "regexp" + "time" + + "github.com/google/uuid" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" + "github.com/prometheus/client_golang/prometheus/promhttp" + "golang.org/x/xerrors" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/sloghuman" + agpl "github.com/coder/coder/v2/cli" + "github.com/coder/coder/v2/cli/clilog" + "github.com/coder/coder/v2/cli/cliui" + "github.com/coder/coder/v2/cli/cliutil" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/codersdk/drpc" + "github.com/coder/coder/v2/provisioner/terraform" + "github.com/coder/coder/v2/provisionerd" + provisionerdproto "github.com/coder/coder/v2/provisionerd/proto" + "github.com/coder/coder/v2/provisionersdk" + "github.com/coder/coder/v2/provisionersdk/proto" + "github.com/coder/serpent" +) + +func (r *RootCmd) provisionerDaemonStart() *serpent.Command { + var ( + cacheDir string + logHuman string + logJSON string + logStackdriver string + logFilter []string + name string + rawTags []string + pollInterval time.Duration + pollJitter time.Duration + preSharedKey string + verbose bool + + prometheusEnable bool + prometheusAddress string + ) + orgContext := agpl.NewOrganizationContext() + client := new(codersdk.Client) + cmd := &serpent.Command{ + Use: "start", + Short: "Run a provisioner daemon", + Middleware: serpent.Chain( + // disable checks and warnings because this command starts a daemon; it is + // not meant for humans typing commands. Furthermore, the checks are + // incompatible with PSK auth that this command uses + r.InitClient(client), + ), + Handler: func(inv *serpent.Invocation) error { + ctx, cancel := context.WithCancel(inv.Context()) + defer cancel() + + stopCtx, stopCancel := inv.SignalNotifyContext(ctx, agpl.StopSignalsNoInterrupt...) + defer stopCancel() + interruptCtx, interruptCancel := inv.SignalNotifyContext(ctx, agpl.InterruptSignals...) + defer interruptCancel() + + // This can fail to get the current organization + // if the client is not authenticated as a user, + // like when only PSK is provided. + // This will be cleaner once PSK is replaced + // with org scoped authentication tokens. + org, err := orgContext.Selected(inv, client) + if err != nil { + var cErr *codersdk.Error + if !errors.As(err, &cErr) || cErr.StatusCode() != http.StatusUnauthorized { + return xerrors.Errorf("current organization: %w", err) + } + + if preSharedKey == "" { + return xerrors.New("must provide a pre-shared key when not authenticated as a user") + } + + org = codersdk.Organization{MinimalOrganization: codersdk.MinimalOrganization{ID: uuid.Nil}} + if orgContext.FlagSelect != "" { + // If we are using PSK, we can't fetch the organization + // to validate org name so we need the user to provide + // a valid organization ID. + orgID, err := uuid.Parse(orgContext.FlagSelect) + if err != nil { + return xerrors.New("must provide an org ID when not authenticated as a user and organization is specified") + } + org = codersdk.Organization{MinimalOrganization: codersdk.MinimalOrganization{ID: orgID}} + } + } + + tags, err := agpl.ParseProvisionerTags(rawTags) + if err != nil { + return err + } + + if name == "" { + name = cliutil.Hostname() + } + + if err := validateProvisionerDaemonName(name); err != nil { + return err + } + + logOpts := []clilog.Option{ + clilog.WithFilter(logFilter...), + clilog.WithHuman(logHuman), + clilog.WithJSON(logJSON), + clilog.WithStackdriver(logStackdriver), + } + if verbose { + logOpts = append(logOpts, clilog.WithVerbose()) + } + + logger, closeLogger, err := clilog.New(logOpts...).Build(inv) + if err != nil { + // Fall back to a basic logger + logger = slog.Make(sloghuman.Sink(inv.Stderr)) + logger.Error(ctx, "failed to initialize logger", slog.Error(err)) + } else { + defer closeLogger() + } + + if len(tags) == 0 { + logger.Info(ctx, "note: untagged provisioners can only pick up jobs from untagged templates") + } + + // When authorizing with a PSK, we automatically scope the provisionerd + // to organization. Scoping to user with PSK auth is not a valid configuration. + if preSharedKey != "" { + logger.Info(ctx, "psk auth automatically sets tag "+provisionersdk.TagScope+"="+provisionersdk.ScopeOrganization) + tags[provisionersdk.TagScope] = provisionersdk.ScopeOrganization + } + + err = os.MkdirAll(cacheDir, 0o700) + if err != nil { + return xerrors.Errorf("mkdir %q: %w", cacheDir, err) + } + + tempDir, err := os.MkdirTemp("", "provisionerd") + if err != nil { + return err + } + + terraformClient, terraformServer := drpc.MemTransportPipe() + go func() { + <-ctx.Done() + _ = terraformClient.Close() + _ = terraformServer.Close() + }() + + errCh := make(chan error, 1) + go func() { + defer cancel() + + err := terraform.Serve(ctx, &terraform.ServeOptions{ + ServeOptions: &provisionersdk.ServeOptions{ + Listener: terraformServer, + Logger: logger.Named("terraform"), + WorkDirectory: tempDir, + }, + CachePath: cacheDir, + }) + if err != nil && !xerrors.Is(err, context.Canceled) { + select { + case errCh <- err: + default: + } + } + }() + + var metrics *provisionerd.Metrics + if prometheusEnable { + logger.Info(ctx, "starting Prometheus endpoint", slog.F("address", prometheusAddress)) + + prometheusRegistry := prometheus.NewRegistry() + prometheusRegistry.MustRegister(collectors.NewGoCollector()) + prometheusRegistry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) + + m := provisionerd.NewMetrics(prometheusRegistry) + m.Runner.NumDaemons.Set(float64(1)) // Set numDaemons to 1 as this is standalone mode. + metrics = &m + + closeFunc := agpl.ServeHandler(ctx, logger, promhttp.InstrumentMetricHandler( + prometheusRegistry, promhttp.HandlerFor(prometheusRegistry, promhttp.HandlerOpts{}), + ), prometheusAddress, "prometheus") + defer closeFunc() + } + + logger.Info(ctx, "starting provisioner daemon", slog.F("tags", tags), slog.F("name", name)) + + connector := provisionerd.LocalProvisioners{ + string(database.ProvisionerTypeTerraform): proto.NewDRPCProvisionerClient(terraformClient), + } + srv := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) { + return client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ + ID: uuid.New(), + Name: name, + Provisioners: []codersdk.ProvisionerType{ + codersdk.ProvisionerTypeTerraform, + }, + Tags: tags, + PreSharedKey: preSharedKey, + Organization: org.ID, + }) + }, &provisionerd.Options{ + Logger: logger, + UpdateInterval: 500 * time.Millisecond, + Connector: connector, + Metrics: metrics, + }) + + waitForProvisionerJobs := false + var exitErr error + select { + case <-stopCtx.Done(): + exitErr = stopCtx.Err() + _, _ = fmt.Fprintln(inv.Stdout, cliui.Bold( + "Stop caught, waiting for provisioner jobs to complete and gracefully exiting. Use ctrl+\\ to force quit", + )) + waitForProvisionerJobs = true + case <-interruptCtx.Done(): + exitErr = interruptCtx.Err() + _, _ = fmt.Fprintln(inv.Stdout, cliui.Bold( + "Interrupt caught, gracefully exiting. Use ctrl+\\ to force quit", + )) + case exitErr = <-errCh: + } + if exitErr != nil && !xerrors.Is(exitErr, context.Canceled) { + cliui.Errorf(inv.Stderr, "Unexpected error, shutting down server: %s\n", exitErr) + } + + err = srv.Shutdown(ctx, waitForProvisionerJobs) + if err != nil { + return xerrors.Errorf("shutdown: %w", err) + } + + // Shutdown does not call close. Must call it manually. + err = srv.Close() + if err != nil { + return xerrors.Errorf("close server: %w", err) + } + + cancel() + if xerrors.Is(exitErr, context.Canceled) { + return nil + } + return exitErr + }, + } + + cmd.Options = serpent.OptionSet{ + { + Flag: "cache-dir", + FlagShorthand: "c", + Env: "CODER_CACHE_DIRECTORY", + Description: "Directory to store cached data.", + Default: codersdk.DefaultCacheDir(), + Value: serpent.StringOf(&cacheDir), + }, + { + Flag: "tag", + FlagShorthand: "t", + Env: "CODER_PROVISIONERD_TAGS", + Description: "Tags to filter provisioner jobs by.", + Value: serpent.StringArrayOf(&rawTags), + }, + { + Flag: "poll-interval", + Env: "CODER_PROVISIONERD_POLL_INTERVAL", + Default: time.Second.String(), + Description: "Deprecated and ignored.", + Value: serpent.DurationOf(&pollInterval), + }, + { + Flag: "poll-jitter", + Env: "CODER_PROVISIONERD_POLL_JITTER", + Description: "Deprecated and ignored.", + Default: (100 * time.Millisecond).String(), + Value: serpent.DurationOf(&pollJitter), + }, + { + Flag: "psk", + Env: "CODER_PROVISIONER_DAEMON_PSK", + Description: "Pre-shared key to authenticate with Coder server.", + Value: serpent.StringOf(&preSharedKey), + }, + { + Flag: "name", + Env: "CODER_PROVISIONER_DAEMON_NAME", + Description: "Name of this provisioner daemon. Defaults to the current hostname without FQDN.", + Value: serpent.StringOf(&name), + Default: "", + }, + { + Flag: "verbose", + Env: "CODER_PROVISIONER_DAEMON_VERBOSE", + Description: "Output debug-level logs.", + Value: serpent.BoolOf(&verbose), + Default: "false", + }, + { + Flag: "log-human", + Env: "CODER_PROVISIONER_DAEMON_LOGGING_HUMAN", + Description: "Output human-readable logs to a given file.", + Value: serpent.StringOf(&logHuman), + Default: "/dev/stderr", + }, + { + Flag: "log-json", + Env: "CODER_PROVISIONER_DAEMON_LOGGING_JSON", + Description: "Output JSON logs to a given file.", + Value: serpent.StringOf(&logJSON), + Default: "", + }, + { + Flag: "log-stackdriver", + Env: "CODER_PROVISIONER_DAEMON_LOGGING_STACKDRIVER", + Description: "Output Stackdriver compatible logs to a given file.", + Value: serpent.StringOf(&logStackdriver), + Default: "", + }, + { + Flag: "log-filter", + Env: "CODER_PROVISIONER_DAEMON_LOG_FILTER", + Description: "Filter debug logs by matching against a given regex. Use .* to match all debug logs.", + Value: serpent.StringArrayOf(&logFilter), + Default: "", + }, + { + Flag: "prometheus-enable", + Env: "CODER_PROMETHEUS_ENABLE", + Description: "Serve prometheus metrics on the address defined by prometheus address.", + Value: serpent.BoolOf(&prometheusEnable), + Default: "false", + }, + { + Flag: "prometheus-address", + Env: "CODER_PROMETHEUS_ADDRESS", + Description: "The bind address to serve prometheus metrics.", + Value: serpent.StringOf(&prometheusAddress), + Default: "127.0.0.1:2112", + }, + } + orgContext.AttachOptions(cmd) + + return cmd +} + +func validateProvisionerDaemonName(name string) error { + if len(name) > 64 { + return xerrors.Errorf("name cannot be greater than 64 characters in length") + } + if ok, err := regexp.MatchString(`^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$`, name); err != nil || !ok { + return xerrors.Errorf("name %q is not a valid hostname", name) + } + return nil +} diff --git a/enterprise/cli/provisionerdaemons_slim.go b/enterprise/cli/provisionerdaemonstart_slim.go similarity index 64% rename from enterprise/cli/provisionerdaemons_slim.go rename to enterprise/cli/provisionerdaemonstart_slim.go index ee868f638117b..aa399e9b9a46c 100644 --- a/enterprise/cli/provisionerdaemons_slim.go +++ b/enterprise/cli/provisionerdaemonstart_slim.go @@ -7,15 +7,15 @@ import ( "github.com/coder/serpent" ) -func (r *RootCmd) provisionerDaemons() *serpent.Command { +func (r *RootCmd) provisionerDaemonStart() *serpent.Command { cmd := &serpent.Command{ - Use: "provisionerd", - Short: "Manage provisioner daemons", + Use: "start", + Short: "Run a provisioner daemon", // We accept RawArgs so all commands and flags are accepted. RawArgs: true, Hidden: true, Handler: func(inv *serpent.Invocation) error { - agplcli.SlimUnsupported(inv.Stderr, "provisionerd") + agplcli.SlimUnsupported(inv.Stderr, "provisionerd start") return nil }, } diff --git a/enterprise/cli/provisionerdaemons_test.go b/enterprise/cli/provisionerdaemonstart_test.go similarity index 100% rename from enterprise/cli/provisionerdaemons_test.go rename to enterprise/cli/provisionerdaemonstart_test.go