Skip to content

chore: move provisioner keys commands into slim build #13993

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
368 changes: 1 addition & 367 deletions enterprise/cli/provisionerdaemons.go
Original file line number Diff line number Diff line change
@@ -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{
Expand All @@ -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
}
Loading
Loading