diff --git a/agent/agent.go b/agent/agent.go index 18243ee788789..6dff747b8b415 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -33,6 +33,7 @@ import ( "cdr.dev/slog" "github.com/coder/coder/agent/usershell" + "github.com/coder/coder/codersdk" "github.com/coder/coder/pty" "github.com/coder/coder/tailnet" "github.com/coder/retry" @@ -49,39 +50,23 @@ const ( MagicSessionErrorCode = 229 ) -var ( - // tailnetIP is a static IPv6 address with the Tailscale prefix that is used to route - // connections from clients to this node. A dynamic address is not required because a Tailnet - // client only dials a single agent at a time. - tailnetIP = netip.MustParseAddr("fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4") - tailnetSSHPort = 1 - tailnetReconnectingPTYPort = 2 - tailnetSpeedtestPort = 3 -) - type Options struct { - CoordinatorDialer CoordinatorDialer - FetchMetadata FetchMetadata - - StatsReporter StatsReporter - ReconnectingPTYTimeout time.Duration - EnvironmentVariables map[string]string - Logger slog.Logger -} - -type Metadata struct { - DERPMap *tailcfg.DERPMap `json:"derpmap"` - EnvironmentVariables map[string]string `json:"environment_variables"` - StartupScript string `json:"startup_script"` - Directory string `json:"directory"` + CoordinatorDialer CoordinatorDialer + FetchMetadata FetchMetadata + StatsReporter StatsReporter + WorkspaceAgentApps WorkspaceAgentApps + PostWorkspaceAgentAppHealth PostWorkspaceAgentAppHealth + ReconnectingPTYTimeout time.Duration + EnvironmentVariables map[string]string + Logger slog.Logger } // CoordinatorDialer is a function that constructs a new broker. // A dialer must be passed in to allow for reconnects. -type CoordinatorDialer func(ctx context.Context) (net.Conn, error) +type CoordinatorDialer func(context.Context) (net.Conn, error) // FetchMetadata is a function to obtain metadata for the agent. -type FetchMetadata func(ctx context.Context) (Metadata, error) +type FetchMetadata func(context.Context) (codersdk.WorkspaceAgentMetadata, error) func New(options Options) io.Closer { if options.ReconnectingPTYTimeout == 0 { @@ -89,15 +74,17 @@ func New(options Options) io.Closer { } ctx, cancelFunc := context.WithCancel(context.Background()) server := &agent{ - reconnectingPTYTimeout: options.ReconnectingPTYTimeout, - logger: options.Logger, - closeCancel: cancelFunc, - closed: make(chan struct{}), - envVars: options.EnvironmentVariables, - coordinatorDialer: options.CoordinatorDialer, - fetchMetadata: options.FetchMetadata, - stats: &Stats{}, - statsReporter: options.StatsReporter, + reconnectingPTYTimeout: options.ReconnectingPTYTimeout, + logger: options.Logger, + closeCancel: cancelFunc, + closed: make(chan struct{}), + envVars: options.EnvironmentVariables, + coordinatorDialer: options.CoordinatorDialer, + fetchMetadata: options.FetchMetadata, + stats: &Stats{}, + statsReporter: options.StatsReporter, + workspaceAgentApps: options.WorkspaceAgentApps, + postWorkspaceAgentAppHealth: options.PostWorkspaceAgentAppHealth, } server.init(ctx) return server @@ -120,14 +107,16 @@ type agent struct { fetchMetadata FetchMetadata sshServer *ssh.Server - network *tailnet.Conn - coordinatorDialer CoordinatorDialer - stats *Stats - statsReporter StatsReporter + network *tailnet.Conn + coordinatorDialer CoordinatorDialer + stats *Stats + statsReporter StatsReporter + workspaceAgentApps WorkspaceAgentApps + postWorkspaceAgentAppHealth PostWorkspaceAgentAppHealth } func (a *agent) run(ctx context.Context) { - var metadata Metadata + var metadata codersdk.WorkspaceAgentMetadata var err error // An exponential back-off occurs when the connection is failing to dial. // This is to prevent server spam in case of a coderd outage. @@ -168,6 +157,10 @@ func (a *agent) run(ctx context.Context) { if metadata.DERPMap != nil { go a.runTailnet(ctx, metadata.DERPMap) } + + if a.workspaceAgentApps != nil && a.postWorkspaceAgentAppHealth != nil { + go NewWorkspaceAppHealthReporter(a.logger, a.workspaceAgentApps, a.postWorkspaceAgentAppHealth)(ctx) + } } func (a *agent) runTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) { @@ -182,7 +175,7 @@ func (a *agent) runTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) { } var err error a.network, err = tailnet.NewConn(&tailnet.Options{ - Addresses: []netip.Prefix{netip.PrefixFrom(tailnetIP, 128)}, + Addresses: []netip.Prefix{netip.PrefixFrom(codersdk.TailnetIP, 128)}, DERPMap: derpMap, Logger: a.logger.Named("tailnet"), }) @@ -199,7 +192,7 @@ func (a *agent) runTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) { }) go a.runCoordinator(ctx) - sshListener, err := a.network.Listen("tcp", ":"+strconv.Itoa(tailnetSSHPort)) + sshListener, err := a.network.Listen("tcp", ":"+strconv.Itoa(codersdk.TailnetSSHPort)) if err != nil { a.logger.Critical(ctx, "listen for ssh", slog.Error(err)) return @@ -213,7 +206,7 @@ func (a *agent) runTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) { go a.sshServer.HandleConn(a.stats.wrapConn(conn)) } }() - reconnectingPTYListener, err := a.network.Listen("tcp", ":"+strconv.Itoa(tailnetReconnectingPTYPort)) + reconnectingPTYListener, err := a.network.Listen("tcp", ":"+strconv.Itoa(codersdk.TailnetReconnectingPTYPort)) if err != nil { a.logger.Critical(ctx, "listen for reconnecting pty", slog.Error(err)) return @@ -239,7 +232,7 @@ func (a *agent) runTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) { if err != nil { continue } - var msg reconnectingPTYInit + var msg codersdk.ReconnectingPTYInit err = json.Unmarshal(data, &msg) if err != nil { continue @@ -247,7 +240,7 @@ func (a *agent) runTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) { go a.handleReconnectingPTY(ctx, msg, conn) } }() - speedtestListener, err := a.network.Listen("tcp", ":"+strconv.Itoa(tailnetSpeedtestPort)) + speedtestListener, err := a.network.Listen("tcp", ":"+strconv.Itoa(codersdk.TailnetSpeedtestPort)) if err != nil { a.logger.Critical(ctx, "listen for speedtest", slog.Error(err)) return @@ -434,7 +427,7 @@ func (a *agent) init(ctx context.Context) { go a.run(ctx) if a.statsReporter != nil { - cl, err := a.statsReporter(ctx, a.logger, func() *Stats { + cl, err := a.statsReporter(ctx, a.logger, func() *codersdk.AgentStats { return a.stats.Copy() }) if err != nil { @@ -469,7 +462,7 @@ func (a *agent) createCommand(ctx context.Context, rawCommand string, env []stri if rawMetadata == nil { return nil, xerrors.Errorf("no metadata was provided: %w", err) } - metadata, valid := rawMetadata.(Metadata) + metadata, valid := rawMetadata.(codersdk.WorkspaceAgentMetadata) if !valid { return nil, xerrors.Errorf("metadata is the wrong type: %T", metadata) } @@ -625,7 +618,7 @@ func (a *agent) handleSSHSession(session ssh.Session) (retErr error) { return cmd.Wait() } -func (a *agent) handleReconnectingPTY(ctx context.Context, msg reconnectingPTYInit, conn net.Conn) { +func (a *agent) handleReconnectingPTY(ctx context.Context, msg codersdk.ReconnectingPTYInit, conn net.Conn) { defer conn.Close() var rpty *reconnectingPTY @@ -766,7 +759,7 @@ func (a *agent) handleReconnectingPTY(ctx context.Context, msg reconnectingPTYIn rpty.activeConnsMutex.Unlock() }() decoder := json.NewDecoder(conn) - var req ReconnectingPTYRequest + var req codersdk.ReconnectingPTYRequest for { err = decoder.Decode(&req) if xerrors.Is(err, io.EOF) { diff --git a/agent/agent_test.go b/agent/agent_test.go index 08c7918765319..3499ef5663414 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -35,6 +35,7 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/agent" + "github.com/coder/coder/codersdk" "github.com/coder/coder/pty/ptytest" "github.com/coder/coder/tailnet" "github.com/coder/coder/tailnet/tailnettest" @@ -52,7 +53,7 @@ func TestAgent(t *testing.T) { t.Run("SSH", func(t *testing.T) { t.Parallel() - conn, stats := setupAgent(t, agent.Metadata{}, 0) + conn, stats := setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0) sshClient, err := conn.SSHClient() require.NoError(t, err) @@ -69,20 +70,20 @@ func TestAgent(t *testing.T) { t.Run("ReconnectingPTY", func(t *testing.T) { t.Parallel() - conn, stats := setupAgent(t, agent.Metadata{}, 0) + conn, stats := setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0) ptyConn, err := conn.ReconnectingPTY(uuid.NewString(), 128, 128, "/bin/bash") require.NoError(t, err) defer ptyConn.Close() - data, err := json.Marshal(agent.ReconnectingPTYRequest{ + data, err := json.Marshal(codersdk.ReconnectingPTYRequest{ Data: "echo test\r\n", }) require.NoError(t, err) _, err = ptyConn.Write(data) require.NoError(t, err) - var s *agent.Stats + var s *codersdk.AgentStats require.Eventuallyf(t, func() bool { var ok bool s, ok = (<-stats) @@ -95,7 +96,7 @@ func TestAgent(t *testing.T) { t.Run("SessionExec", func(t *testing.T) { t.Parallel() - session := setupSSHSession(t, agent.Metadata{}) + session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{}) command := "echo test" if runtime.GOOS == "windows" { @@ -108,7 +109,7 @@ func TestAgent(t *testing.T) { t.Run("GitSSH", func(t *testing.T) { t.Parallel() - session := setupSSHSession(t, agent.Metadata{}) + session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{}) command := "sh -c 'echo $GIT_SSH_COMMAND'" if runtime.GOOS == "windows" { command = "cmd.exe /c echo %GIT_SSH_COMMAND%" @@ -126,7 +127,7 @@ func TestAgent(t *testing.T) { // it seems like it could be either. t.Skip("ConPTY appears to be inconsistent on Windows.") } - session := setupSSHSession(t, agent.Metadata{}) + session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{}) command := "bash" if runtime.GOOS == "windows" { command = "cmd.exe" @@ -154,7 +155,7 @@ func TestAgent(t *testing.T) { t.Run("SessionTTYExitCode", func(t *testing.T) { t.Parallel() - session := setupSSHSession(t, agent.Metadata{}) + session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{}) command := "areallynotrealcommand" err := session.RequestPty("xterm", 128, 128, ssh.TerminalModes{}) require.NoError(t, err) @@ -211,7 +212,7 @@ func TestAgent(t *testing.T) { t.Run("SFTP", func(t *testing.T) { t.Parallel() - conn, _ := setupAgent(t, agent.Metadata{}, 0) + conn, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0) sshClient, err := conn.SSHClient() require.NoError(t, err) defer sshClient.Close() @@ -229,7 +230,7 @@ func TestAgent(t *testing.T) { t.Run("SCP", func(t *testing.T) { t.Parallel() - conn, _ := setupAgent(t, agent.Metadata{}, 0) + conn, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0) sshClient, err := conn.SSHClient() require.NoError(t, err) defer sshClient.Close() @@ -247,7 +248,7 @@ func TestAgent(t *testing.T) { t.Parallel() key := "EXAMPLE" value := "value" - session := setupSSHSession(t, agent.Metadata{ + session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{ EnvironmentVariables: map[string]string{ key: value, }, @@ -264,7 +265,7 @@ func TestAgent(t *testing.T) { t.Run("EnvironmentVariableExpansion", func(t *testing.T) { t.Parallel() key := "EXAMPLE" - session := setupSSHSession(t, agent.Metadata{ + session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{ EnvironmentVariables: map[string]string{ key: "$SOMETHINGNOTSET", }, @@ -291,7 +292,7 @@ func TestAgent(t *testing.T) { t.Run(key, func(t *testing.T) { t.Parallel() - session := setupSSHSession(t, agent.Metadata{}) + session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{}) command := "sh -c 'echo $" + key + "'" if runtime.GOOS == "windows" { command = "cmd.exe /c echo %" + key + "%" @@ -314,7 +315,7 @@ func TestAgent(t *testing.T) { t.Run(key, func(t *testing.T) { t.Parallel() - session := setupSSHSession(t, agent.Metadata{}) + session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{}) command := "sh -c 'echo $" + key + "'" if runtime.GOOS == "windows" { command = "cmd.exe /c echo %" + key + "%" @@ -330,7 +331,7 @@ func TestAgent(t *testing.T) { t.Parallel() tempPath := filepath.Join(t.TempDir(), "content.txt") content := "somethingnice" - setupAgent(t, agent.Metadata{ + setupAgent(t, codersdk.WorkspaceAgentMetadata{ StartupScript: fmt.Sprintf("echo %s > %s", content, tempPath), }, 0) @@ -365,7 +366,7 @@ func TestAgent(t *testing.T) { t.Skip("ConPTY appears to be inconsistent on Windows.") } - conn, _ := setupAgent(t, agent.Metadata{}, 0) + conn, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0) id := uuid.NewString() netConn, err := conn.ReconnectingPTY(id, 100, 100, "/bin/bash") require.NoError(t, err) @@ -375,7 +376,7 @@ func TestAgent(t *testing.T) { // the shell is simultaneously sending a prompt. time.Sleep(100 * time.Millisecond) - data, err := json.Marshal(agent.ReconnectingPTYRequest{ + data, err := json.Marshal(codersdk.ReconnectingPTYRequest{ Data: "echo test\r\n", }) require.NoError(t, err) @@ -462,7 +463,7 @@ func TestAgent(t *testing.T) { } }() - conn, _ := setupAgent(t, agent.Metadata{}, 0) + conn, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0) require.Eventually(t, func() bool { _, err := conn.Ping() return err == nil @@ -483,7 +484,7 @@ func TestAgent(t *testing.T) { t.Run("Tailnet", func(t *testing.T) { t.Parallel() derpMap := tailnettest.RunDERPAndSTUN(t) - conn, _ := setupAgent(t, agent.Metadata{ + conn, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{ DERPMap: derpMap, }, 0) defer conn.Close() @@ -499,7 +500,7 @@ func TestAgent(t *testing.T) { t.Skip("The minimum duration for a speedtest is hardcoded in Tailscale to 5s!") } derpMap := tailnettest.RunDERPAndSTUN(t) - conn, _ := setupAgent(t, agent.Metadata{ + conn, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{ DERPMap: derpMap, }, 0) defer conn.Close() @@ -510,7 +511,7 @@ func TestAgent(t *testing.T) { } func setupSSHCommand(t *testing.T, beforeArgs []string, afterArgs []string) *exec.Cmd { - agentConn, _ := setupAgent(t, agent.Metadata{}, 0) + agentConn, _ := setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0) listener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) waitGroup := sync.WaitGroup{} @@ -547,7 +548,7 @@ func setupSSHCommand(t *testing.T, beforeArgs []string, afterArgs []string) *exe return exec.Command("ssh", args...) } -func setupSSHSession(t *testing.T, options agent.Metadata) *ssh.Session { +func setupSSHSession(t *testing.T, options codersdk.WorkspaceAgentMetadata) *ssh.Session { conn, _ := setupAgent(t, options, 0) sshClient, err := conn.SSHClient() require.NoError(t, err) @@ -565,18 +566,18 @@ func (c closeFunc) Close() error { return c() } -func setupAgent(t *testing.T, metadata agent.Metadata, ptyTimeout time.Duration) ( - *agent.Conn, - <-chan *agent.Stats, +func setupAgent(t *testing.T, metadata codersdk.WorkspaceAgentMetadata, ptyTimeout time.Duration) ( + *codersdk.AgentConn, + <-chan *codersdk.AgentStats, ) { if metadata.DERPMap == nil { metadata.DERPMap = tailnettest.RunDERPAndSTUN(t) } coordinator := tailnet.NewCoordinator() agentID := uuid.New() - statsCh := make(chan *agent.Stats) + statsCh := make(chan *codersdk.AgentStats) closer := agent.New(agent.Options{ - FetchMetadata: func(ctx context.Context) (agent.Metadata, error) { + FetchMetadata: func(ctx context.Context) (codersdk.WorkspaceAgentMetadata, error) { return metadata, nil }, CoordinatorDialer: func(ctx context.Context) (net.Conn, error) { @@ -595,7 +596,7 @@ func setupAgent(t *testing.T, metadata agent.Metadata, ptyTimeout time.Duration) }, Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug), ReconnectingPTYTimeout: ptyTimeout, - StatsReporter: func(ctx context.Context, log slog.Logger, statsFn func() *agent.Stats) (io.Closer, error) { + StatsReporter: func(ctx context.Context, log slog.Logger, statsFn func() *codersdk.AgentStats) (io.Closer, error) { doneCh := make(chan struct{}) ctx, cancel := context.WithCancel(ctx) @@ -648,7 +649,7 @@ func setupAgent(t *testing.T, metadata agent.Metadata, ptyTimeout time.Duration) return conn.UpdateNodes(node) }) conn.SetNodeCallback(sendNode) - return &agent.Conn{ + return &codersdk.AgentConn{ Conn: conn, }, statsCh } diff --git a/agent/apphealth.go b/agent/apphealth.go new file mode 100644 index 0000000000000..9ddaa1a52f711 --- /dev/null +++ b/agent/apphealth.go @@ -0,0 +1,184 @@ +package agent + +import ( + "context" + "net/http" + "sync" + "time" + + "golang.org/x/xerrors" + + "cdr.dev/slog" + "github.com/coder/coder/codersdk" + "github.com/coder/retry" +) + +// WorkspaceAgentApps fetches the workspace apps. +type WorkspaceAgentApps func(context.Context) ([]codersdk.WorkspaceApp, error) + +// PostWorkspaceAgentAppHealth updates the workspace app health. +type PostWorkspaceAgentAppHealth func(context.Context, codersdk.PostWorkspaceAppHealthsRequest) error + +// WorkspaceAppHealthReporter is a function that checks and reports the health of the workspace apps until the passed context is canceled. +type WorkspaceAppHealthReporter func(ctx context.Context) + +// NewWorkspaceAppHealthReporter creates a WorkspaceAppHealthReporter that reports app health to coderd. +func NewWorkspaceAppHealthReporter(logger slog.Logger, workspaceAgentApps WorkspaceAgentApps, postWorkspaceAgentAppHealth PostWorkspaceAgentAppHealth) WorkspaceAppHealthReporter { + runHealthcheckLoop := func(ctx context.Context) error { + apps, err := workspaceAgentApps(ctx) + if err != nil { + if xerrors.Is(err, context.Canceled) { + return nil + } + return xerrors.Errorf("getting workspace apps: %w", err) + } + + // no need to run this loop if no apps for this workspace. + if len(apps) == 0 { + return nil + } + + hasHealthchecksEnabled := false + health := make(map[string]codersdk.WorkspaceAppHealth, 0) + for _, app := range apps { + health[app.Name] = app.Health + if !hasHealthchecksEnabled && app.Health != codersdk.WorkspaceAppHealthDisabled { + hasHealthchecksEnabled = true + } + } + + // no need to run this loop if no health checks are configured. + if !hasHealthchecksEnabled { + return nil + } + + // run a ticker for each app health check. + var mu sync.RWMutex + failures := make(map[string]int, 0) + for _, nextApp := range apps { + if !shouldStartTicker(nextApp) { + continue + } + app := nextApp + t := time.NewTicker(time.Duration(app.Healthcheck.Interval) * time.Second) + go func() { + for { + select { + case <-ctx.Done(): + return + case <-t.C: + } + // we set the http timeout to the healthcheck interval to prevent getting too backed up. + client := &http.Client{ + Timeout: time.Duration(app.Healthcheck.Interval) * time.Second, + } + err := func() error { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, app.Healthcheck.URL, nil) + if err != nil { + return err + } + res, err := client.Do(req) + if err != nil { + return err + } + // successful healthcheck is a non-5XX status code + res.Body.Close() + if res.StatusCode >= http.StatusInternalServerError { + return xerrors.Errorf("error status code: %d", res.StatusCode) + } + + return nil + }() + if err != nil { + mu.Lock() + if failures[app.Name] < int(app.Healthcheck.Threshold) { + // increment the failure count and keep status the same. + // we will change it when we hit the threshold. + failures[app.Name]++ + } else { + // set to unhealthy if we hit the failure threshold. + // we stop incrementing at the threshold to prevent the failure value from increasing forever. + health[app.Name] = codersdk.WorkspaceAppHealthUnhealthy + } + mu.Unlock() + } else { + mu.Lock() + // we only need one successful health check to be considered healthy. + health[app.Name] = codersdk.WorkspaceAppHealthHealthy + failures[app.Name] = 0 + mu.Unlock() + } + + t.Reset(time.Duration(app.Healthcheck.Interval)) + } + }() + } + + mu.Lock() + lastHealth := copyHealth(health) + mu.Unlock() + reportTicker := time.NewTicker(time.Second) + // every second we check if the health values of the apps have changed + // and if there is a change we will report the new values. + for { + select { + case <-ctx.Done(): + return nil + case <-reportTicker.C: + mu.RLock() + changed := healthChanged(lastHealth, health) + mu.RUnlock() + if !changed { + continue + } + + mu.Lock() + lastHealth = copyHealth(health) + mu.Unlock() + err := postWorkspaceAgentAppHealth(ctx, codersdk.PostWorkspaceAppHealthsRequest{ + Healths: lastHealth, + }) + if err != nil { + logger.Error(ctx, "failed to report workspace app stat", slog.Error(err)) + } + } + } + } + + return func(ctx context.Context) { + for r := retry.New(time.Second, 30*time.Second); r.Wait(ctx); { + err := runHealthcheckLoop(ctx) + if err == nil || xerrors.Is(err, context.Canceled) || xerrors.Is(err, context.DeadlineExceeded) { + return + } + logger.Error(ctx, "failed running workspace app reporter", slog.Error(err)) + } + } +} + +func shouldStartTicker(app codersdk.WorkspaceApp) bool { + return app.Healthcheck.URL != "" && app.Healthcheck.Interval > 0 && app.Healthcheck.Threshold > 0 +} + +func healthChanged(old map[string]codersdk.WorkspaceAppHealth, new map[string]codersdk.WorkspaceAppHealth) bool { + for name, newValue := range new { + oldValue, found := old[name] + if !found { + return true + } + if newValue != oldValue { + return true + } + } + + return false +} + +func copyHealth(h1 map[string]codersdk.WorkspaceAppHealth) map[string]codersdk.WorkspaceAppHealth { + h2 := make(map[string]codersdk.WorkspaceAppHealth, 0) + for k, v := range h1 { + h2[k] = v + } + + return h2 +} diff --git a/agent/apphealth_test.go b/agent/apphealth_test.go new file mode 100644 index 0000000000000..de4a8d52eb27f --- /dev/null +++ b/agent/apphealth_test.go @@ -0,0 +1,177 @@ +package agent_test + +import ( + "context" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/agent" + "github.com/coder/coder/coderd/httpapi" + "github.com/coder/coder/codersdk" + "github.com/coder/coder/testutil" +) + +func TestAppHealth(t *testing.T) { + t.Parallel() + t.Run("Healthy", func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + apps := []codersdk.WorkspaceApp{ + { + Name: "app1", + Healthcheck: codersdk.Healthcheck{}, + Health: codersdk.WorkspaceAppHealthDisabled, + }, + { + Name: "app2", + Healthcheck: codersdk.Healthcheck{ + // URL: We don't set the URL for this test because the setup will + // create a httptest server for us and set it for us. + Interval: 1, + Threshold: 1, + }, + Health: codersdk.WorkspaceAppHealthInitializing, + }, + } + handlers := []http.Handler{ + nil, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + httpapi.Write(r.Context(), w, http.StatusOK, nil) + }), + } + getApps, closeFn := setupAppReporter(ctx, t, apps, handlers) + defer closeFn() + apps, err := getApps(ctx) + require.NoError(t, err) + require.EqualValues(t, codersdk.WorkspaceAppHealthDisabled, apps[0].Health) + require.Eventually(t, func() bool { + apps, err := getApps(ctx) + if err != nil { + return false + } + + return apps[1].Health == codersdk.WorkspaceAppHealthHealthy + }, testutil.WaitLong, testutil.IntervalSlow) + }) + + t.Run("500", func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + apps := []codersdk.WorkspaceApp{ + { + Name: "app2", + Healthcheck: codersdk.Healthcheck{ + // URL: We don't set the URL for this test because the setup will + // create a httptest server for us and set it for us. + Interval: 1, + Threshold: 1, + }, + Health: codersdk.WorkspaceAppHealthInitializing, + }, + } + handlers := []http.Handler{ + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + httpapi.Write(r.Context(), w, http.StatusInternalServerError, nil) + }), + } + getApps, closeFn := setupAppReporter(ctx, t, apps, handlers) + defer closeFn() + require.Eventually(t, func() bool { + apps, err := getApps(ctx) + if err != nil { + return false + } + + return apps[0].Health == codersdk.WorkspaceAppHealthUnhealthy + }, testutil.WaitLong, testutil.IntervalSlow) + }) + + t.Run("Timeout", func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + apps := []codersdk.WorkspaceApp{ + { + Name: "app2", + Healthcheck: codersdk.Healthcheck{ + // URL: We don't set the URL for this test because the setup will + // create a httptest server for us and set it for us. + Interval: 1, + Threshold: 1, + }, + Health: codersdk.WorkspaceAppHealthInitializing, + }, + } + handlers := []http.Handler{ + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // sleep longer than the interval to cause the health check to time out + time.Sleep(2 * time.Second) + httpapi.Write(r.Context(), w, http.StatusOK, nil) + }), + } + getApps, closeFn := setupAppReporter(ctx, t, apps, handlers) + defer closeFn() + require.Eventually(t, func() bool { + apps, err := getApps(ctx) + if err != nil { + return false + } + + return apps[0].Health == codersdk.WorkspaceAppHealthUnhealthy + }, testutil.WaitLong, testutil.IntervalSlow) + }) +} + +func setupAppReporter(ctx context.Context, t *testing.T, apps []codersdk.WorkspaceApp, handlers []http.Handler) (agent.WorkspaceAgentApps, func()) { + closers := []func(){} + for i, handler := range handlers { + if handler == nil { + continue + } + ts := httptest.NewServer(handler) + app := apps[i] + app.Healthcheck.URL = ts.URL + apps[i] = app + closers = append(closers, ts.Close) + } + + var mu sync.Mutex + workspaceAgentApps := func(context.Context) ([]codersdk.WorkspaceApp, error) { + mu.Lock() + defer mu.Unlock() + var newApps []codersdk.WorkspaceApp + return append(newApps, apps...), nil + } + postWorkspaceAgentAppHealth := func(_ context.Context, req codersdk.PostWorkspaceAppHealthsRequest) error { + mu.Lock() + for name, health := range req.Healths { + for i, app := range apps { + if app.Name != name { + continue + } + app.Health = health + apps[i] = app + } + } + mu.Unlock() + + return nil + } + + go agent.NewWorkspaceAppHealthReporter(slogtest.Make(t, nil).Leveled(slog.LevelDebug), workspaceAgentApps, postWorkspaceAgentAppHealth)(ctx) + + return workspaceAgentApps, func() { + for _, closeFn := range closers { + closeFn() + } + } +} diff --git a/agent/stats.go b/agent/stats.go index 0015a3e4e1fb1..e47bfcdee2157 100644 --- a/agent/stats.go +++ b/agent/stats.go @@ -7,6 +7,7 @@ import ( "sync/atomic" "cdr.dev/slog" + "github.com/coder/coder/codersdk" ) // statsConn wraps a net.Conn with statistics. @@ -40,8 +41,8 @@ type Stats struct { TxBytes int64 `json:"tx_bytes"` } -func (s *Stats) Copy() *Stats { - return &Stats{ +func (s *Stats) Copy() *codersdk.AgentStats { + return &codersdk.AgentStats{ NumConns: atomic.LoadInt64(&s.NumConns), RxBytes: atomic.LoadInt64(&s.RxBytes), TxBytes: atomic.LoadInt64(&s.TxBytes), @@ -63,5 +64,5 @@ func (s *Stats) wrapConn(conn net.Conn) net.Conn { type StatsReporter func( ctx context.Context, log slog.Logger, - stats func() *Stats, + stats func() *codersdk.AgentStats, ) (io.Closer, error) diff --git a/cli/agent.go b/cli/agent.go index 837d30eb37176..052c89d5888dc 100644 --- a/cli/agent.go +++ b/cli/agent.go @@ -189,8 +189,10 @@ func workspaceAgent() *cobra.Command { // shells so "gitssh" works! "CODER_AGENT_TOKEN": client.SessionToken, }, - CoordinatorDialer: client.ListenWorkspaceAgentTailnet, - StatsReporter: client.AgentReportStats, + CoordinatorDialer: client.ListenWorkspaceAgentTailnet, + StatsReporter: client.AgentReportStats, + WorkspaceAgentApps: client.WorkspaceAgentApps, + PostWorkspaceAgentAppHealth: client.PostWorkspaceAgentAppHealth, }) <-cmd.Context().Done() return closer.Close() diff --git a/cli/portforward.go b/cli/portforward.go index 7943291c042c0..2511375922979 100644 --- a/cli/portforward.go +++ b/cli/portforward.go @@ -169,7 +169,7 @@ func portForward() *cobra.Command { return cmd } -func listenAndPortForward(ctx context.Context, cmd *cobra.Command, conn *agent.Conn, wg *sync.WaitGroup, spec portForwardSpec) (net.Listener, error) { +func listenAndPortForward(ctx context.Context, cmd *cobra.Command, conn *codersdk.AgentConn, wg *sync.WaitGroup, spec portForwardSpec) (net.Listener, error) { _, _ = fmt.Fprintf(cmd.OutOrStderr(), "Forwarding '%v://%v' locally to '%v://%v' in the workspace\n", spec.listenNetwork, spec.listenAddress, spec.dialNetwork, spec.dialAddress) var ( diff --git a/coderd/coderd.go b/coderd/coderd.go index a595488687ca5..e9e633506acba 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -410,8 +410,10 @@ func New(options *Options) *API { r.Post("/google-instance-identity", api.postWorkspaceAuthGoogleInstanceIdentity) r.Route("/me", func(r chi.Router) { r.Use(httpmw.ExtractWorkspaceAgent(options.Database)) + r.Get("/apps", api.workspaceAgentApps) r.Get("/metadata", api.workspaceAgentMetadata) r.Post("/version", api.postWorkspaceAgentVersion) + r.Post("/app-health", api.postWorkspaceAppHealth) r.Get("/gitsshkey", api.agentGitSSHKey) r.Get("/coordinate", api.workspaceAgentCoordinate) r.Get("/report-stats", api.workspaceAgentReportStats) diff --git a/coderd/coderdtest/authorize.go b/coderd/coderdtest/authorize.go index 331c105d13179..5173231bf6c4f 100644 --- a/coderd/coderdtest/authorize.go +++ b/coderd/coderdtest/authorize.go @@ -55,10 +55,12 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) { "POST:/api/v2/workspaceagents/aws-instance-identity": {NoAuthorize: true}, "POST:/api/v2/workspaceagents/azure-instance-identity": {NoAuthorize: true}, "POST:/api/v2/workspaceagents/google-instance-identity": {NoAuthorize: true}, + "GET:/api/v2/workspaceagents/me/apps": {NoAuthorize: true}, "GET:/api/v2/workspaceagents/me/gitsshkey": {NoAuthorize: true}, "GET:/api/v2/workspaceagents/me/metadata": {NoAuthorize: true}, "GET:/api/v2/workspaceagents/me/coordinate": {NoAuthorize: true}, "POST:/api/v2/workspaceagents/me/version": {NoAuthorize: true}, + "POST:/api/v2/workspaceagents/me/app-health": {NoAuthorize: true}, "GET:/api/v2/workspaceagents/me/report-stats": {NoAuthorize: true}, // These endpoints have more assertions. This is good, add more endpoints to assert if you can! diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index a63643c29e531..c502780145ccd 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -2019,19 +2019,38 @@ func (q *fakeQuerier) InsertWorkspaceApp(_ context.Context, arg database.InsertW // nolint:gosimple workspaceApp := database.WorkspaceApp{ - ID: arg.ID, - AgentID: arg.AgentID, - CreatedAt: arg.CreatedAt, - Name: arg.Name, - Icon: arg.Icon, - Command: arg.Command, - Url: arg.Url, - RelativePath: arg.RelativePath, + ID: arg.ID, + AgentID: arg.AgentID, + CreatedAt: arg.CreatedAt, + Name: arg.Name, + Icon: arg.Icon, + Command: arg.Command, + Url: arg.Url, + RelativePath: arg.RelativePath, + HealthcheckUrl: arg.HealthcheckUrl, + HealthcheckInterval: arg.HealthcheckInterval, + HealthcheckThreshold: arg.HealthcheckThreshold, + Health: arg.Health, } q.workspaceApps = append(q.workspaceApps, workspaceApp) return workspaceApp, nil } +func (q *fakeQuerier) UpdateWorkspaceAppHealthByID(_ context.Context, arg database.UpdateWorkspaceAppHealthByIDParams) error { + q.mutex.Lock() + defer q.mutex.Unlock() + + for index, app := range q.workspaceApps { + if app.ID != arg.ID { + continue + } + app.Health = arg.Health + q.workspaceApps[index] = app + return nil + } + return sql.ErrNoRows +} + func (q *fakeQuerier) UpdateAPIKeyByID(_ context.Context, arg database.UpdateAPIKeyByIDParams) error { q.mutex.Lock() defer q.mutex.Unlock() diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 81557a9022d00..a09e90e519530 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -88,6 +88,13 @@ CREATE TYPE user_status AS ENUM ( 'suspended' ); +CREATE TYPE workspace_app_health AS ENUM ( + 'disabled', + 'initializing', + 'healthy', + 'unhealthy' +); + CREATE TYPE workspace_transition AS ENUM ( 'start', 'stop', @@ -342,7 +349,11 @@ CREATE TABLE workspace_apps ( icon character varying(256) NOT NULL, command character varying(65534), url character varying(65534), - relative_path boolean DEFAULT false NOT NULL + relative_path boolean DEFAULT false NOT NULL, + healthcheck_url text DEFAULT ''::text NOT NULL, + healthcheck_interval integer DEFAULT 0 NOT NULL, + healthcheck_threshold integer DEFAULT 0 NOT NULL, + health workspace_app_health DEFAULT 'disabled'::public.workspace_app_health NOT NULL ); CREATE TABLE workspace_builds ( diff --git a/coderd/database/migrations/000052_workspace_app_health.down.sql b/coderd/database/migrations/000052_workspace_app_health.down.sql new file mode 100644 index 0000000000000..33508eb9fc3d0 --- /dev/null +++ b/coderd/database/migrations/000052_workspace_app_health.down.sql @@ -0,0 +1,7 @@ +ALTER TABLE ONLY workspace_apps + DROP COLUMN IF EXISTS healthcheck_url, + DROP COLUMN IF EXISTS healthcheck_interval, + DROP COLUMN IF EXISTS healthcheck_threshold, + DROP COLUMN IF EXISTS health; + +DROP TYPE workspace_app_health; diff --git a/coderd/database/migrations/000052_workspace_app_health.up.sql b/coderd/database/migrations/000052_workspace_app_health.up.sql new file mode 100644 index 0000000000000..3546174b40b85 --- /dev/null +++ b/coderd/database/migrations/000052_workspace_app_health.up.sql @@ -0,0 +1,7 @@ +CREATE TYPE workspace_app_health AS ENUM ('disabled', 'initializing', 'healthy', 'unhealthy'); + +ALTER TABLE ONLY workspace_apps + ADD COLUMN IF NOT EXISTS healthcheck_url text NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS healthcheck_interval int NOT NULL DEFAULT 0, + ADD COLUMN IF NOT EXISTS healthcheck_threshold int NOT NULL DEFAULT 0, + ADD COLUMN IF NOT EXISTS health workspace_app_health NOT NULL DEFAULT 'disabled'; diff --git a/coderd/database/models.go b/coderd/database/models.go index b5d48bf6c0c32..0e24548ec450d 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -312,6 +312,27 @@ func (e *UserStatus) Scan(src interface{}) error { return nil } +type WorkspaceAppHealth string + +const ( + WorkspaceAppHealthDisabled WorkspaceAppHealth = "disabled" + WorkspaceAppHealthInitializing WorkspaceAppHealth = "initializing" + WorkspaceAppHealthHealthy WorkspaceAppHealth = "healthy" + WorkspaceAppHealthUnhealthy WorkspaceAppHealth = "unhealthy" +) + +func (e *WorkspaceAppHealth) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = WorkspaceAppHealth(s) + case string: + *e = WorkspaceAppHealth(s) + default: + return fmt.Errorf("unsupported scan type for WorkspaceAppHealth: %T", src) + } + return nil +} + type WorkspaceTransition string const ( @@ -575,14 +596,18 @@ type WorkspaceAgent struct { } type WorkspaceApp struct { - ID uuid.UUID `db:"id" json:"id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - AgentID uuid.UUID `db:"agent_id" json:"agent_id"` - Name string `db:"name" json:"name"` - Icon string `db:"icon" json:"icon"` - Command sql.NullString `db:"command" json:"command"` - Url sql.NullString `db:"url" json:"url"` - RelativePath bool `db:"relative_path" json:"relative_path"` + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + Name string `db:"name" json:"name"` + Icon string `db:"icon" json:"icon"` + Command sql.NullString `db:"command" json:"command"` + Url sql.NullString `db:"url" json:"url"` + RelativePath bool `db:"relative_path" json:"relative_path"` + HealthcheckUrl string `db:"healthcheck_url" json:"healthcheck_url"` + HealthcheckInterval int32 `db:"healthcheck_interval" json:"healthcheck_interval"` + HealthcheckThreshold int32 `db:"healthcheck_threshold" json:"healthcheck_threshold"` + Health WorkspaceAppHealth `db:"health" json:"health"` } type WorkspaceBuild struct { diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 0b38708a2497e..caf3f4ad27b55 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -149,6 +149,7 @@ type querier interface { UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (Workspace, error) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error UpdateWorkspaceAgentVersionByID(ctx context.Context, arg UpdateWorkspaceAgentVersionByIDParams) error + UpdateWorkspaceAppHealthByID(ctx context.Context, arg UpdateWorkspaceAppHealthByIDParams) error UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWorkspaceAutostartParams) error UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) error UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 0da956761bf12..424d72f4efddb 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3849,7 +3849,7 @@ func (q *sqlQuerier) UpdateWorkspaceAgentVersionByID(ctx context.Context, arg Up } const getWorkspaceAppByAgentIDAndName = `-- name: GetWorkspaceAppByAgentIDAndName :one -SELECT id, created_at, agent_id, name, icon, command, url, relative_path FROM workspace_apps WHERE agent_id = $1 AND name = $2 +SELECT id, created_at, agent_id, name, icon, command, url, relative_path, healthcheck_url, healthcheck_interval, healthcheck_threshold, health FROM workspace_apps WHERE agent_id = $1 AND name = $2 ` type GetWorkspaceAppByAgentIDAndNameParams struct { @@ -3869,12 +3869,16 @@ func (q *sqlQuerier) GetWorkspaceAppByAgentIDAndName(ctx context.Context, arg Ge &i.Command, &i.Url, &i.RelativePath, + &i.HealthcheckUrl, + &i.HealthcheckInterval, + &i.HealthcheckThreshold, + &i.Health, ) return i, err } const getWorkspaceAppsByAgentID = `-- name: GetWorkspaceAppsByAgentID :many -SELECT id, created_at, agent_id, name, icon, command, url, relative_path FROM workspace_apps WHERE agent_id = $1 ORDER BY name ASC +SELECT id, created_at, agent_id, name, icon, command, url, relative_path, healthcheck_url, healthcheck_interval, healthcheck_threshold, health FROM workspace_apps WHERE agent_id = $1 ORDER BY name ASC ` func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceApp, error) { @@ -3895,6 +3899,10 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid &i.Command, &i.Url, &i.RelativePath, + &i.HealthcheckUrl, + &i.HealthcheckInterval, + &i.HealthcheckThreshold, + &i.Health, ); err != nil { return nil, err } @@ -3910,7 +3918,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid } const getWorkspaceAppsByAgentIDs = `-- name: GetWorkspaceAppsByAgentIDs :many -SELECT id, created_at, agent_id, name, icon, command, url, relative_path FROM workspace_apps WHERE agent_id = ANY($1 :: uuid [ ]) ORDER BY name ASC +SELECT id, created_at, agent_id, name, icon, command, url, relative_path, healthcheck_url, healthcheck_interval, healthcheck_threshold, health FROM workspace_apps WHERE agent_id = ANY($1 :: uuid [ ]) ORDER BY name ASC ` func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceApp, error) { @@ -3931,6 +3939,10 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid. &i.Command, &i.Url, &i.RelativePath, + &i.HealthcheckUrl, + &i.HealthcheckInterval, + &i.HealthcheckThreshold, + &i.Health, ); err != nil { return nil, err } @@ -3946,7 +3958,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid. } const getWorkspaceAppsCreatedAfter = `-- name: GetWorkspaceAppsCreatedAfter :many -SELECT id, created_at, agent_id, name, icon, command, url, relative_path FROM workspace_apps WHERE created_at > $1 ORDER BY name ASC +SELECT id, created_at, agent_id, name, icon, command, url, relative_path, healthcheck_url, healthcheck_interval, healthcheck_threshold, health FROM workspace_apps WHERE created_at > $1 ORDER BY name ASC ` func (q *sqlQuerier) GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceApp, error) { @@ -3967,6 +3979,10 @@ func (q *sqlQuerier) GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt &i.Command, &i.Url, &i.RelativePath, + &i.HealthcheckUrl, + &i.HealthcheckInterval, + &i.HealthcheckThreshold, + &i.Health, ); err != nil { return nil, err } @@ -3991,21 +4007,29 @@ INSERT INTO icon, command, url, - relative_path + relative_path, + healthcheck_url, + healthcheck_interval, + healthcheck_threshold, + health ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, created_at, agent_id, name, icon, command, url, relative_path + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, agent_id, name, icon, command, url, relative_path, healthcheck_url, healthcheck_interval, healthcheck_threshold, health ` type InsertWorkspaceAppParams struct { - ID uuid.UUID `db:"id" json:"id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - AgentID uuid.UUID `db:"agent_id" json:"agent_id"` - Name string `db:"name" json:"name"` - Icon string `db:"icon" json:"icon"` - Command sql.NullString `db:"command" json:"command"` - Url sql.NullString `db:"url" json:"url"` - RelativePath bool `db:"relative_path" json:"relative_path"` + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + Name string `db:"name" json:"name"` + Icon string `db:"icon" json:"icon"` + Command sql.NullString `db:"command" json:"command"` + Url sql.NullString `db:"url" json:"url"` + RelativePath bool `db:"relative_path" json:"relative_path"` + HealthcheckUrl string `db:"healthcheck_url" json:"healthcheck_url"` + HealthcheckInterval int32 `db:"healthcheck_interval" json:"healthcheck_interval"` + HealthcheckThreshold int32 `db:"healthcheck_threshold" json:"healthcheck_threshold"` + Health WorkspaceAppHealth `db:"health" json:"health"` } func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspaceAppParams) (WorkspaceApp, error) { @@ -4018,6 +4042,10 @@ func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspace arg.Command, arg.Url, arg.RelativePath, + arg.HealthcheckUrl, + arg.HealthcheckInterval, + arg.HealthcheckThreshold, + arg.Health, ) var i WorkspaceApp err := row.Scan( @@ -4029,10 +4057,33 @@ func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspace &i.Command, &i.Url, &i.RelativePath, + &i.HealthcheckUrl, + &i.HealthcheckInterval, + &i.HealthcheckThreshold, + &i.Health, ) return i, err } +const updateWorkspaceAppHealthByID = `-- name: UpdateWorkspaceAppHealthByID :exec +UPDATE + workspace_apps +SET + health = $2 +WHERE + id = $1 +` + +type UpdateWorkspaceAppHealthByIDParams struct { + ID uuid.UUID `db:"id" json:"id"` + Health WorkspaceAppHealth `db:"health" json:"health"` +} + +func (q *sqlQuerier) UpdateWorkspaceAppHealthByID(ctx context.Context, arg UpdateWorkspaceAppHealthByIDParams) error { + _, err := q.db.ExecContext(ctx, updateWorkspaceAppHealthByID, arg.ID, arg.Health) + return err +} + const getLatestWorkspaceBuildByWorkspaceID = `-- name: GetLatestWorkspaceBuildByWorkspaceID :one SELECT id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason diff --git a/coderd/database/queries/workspaceapps.sql b/coderd/database/queries/workspaceapps.sql index f25fd67124187..61ea2d7e397a4 100644 --- a/coderd/database/queries/workspaceapps.sql +++ b/coderd/database/queries/workspaceapps.sql @@ -20,7 +20,19 @@ INSERT INTO icon, command, url, - relative_path + relative_path, + healthcheck_url, + healthcheck_interval, + healthcheck_threshold, + health ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *; + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *; + +-- name: UpdateWorkspaceAppHealthByID :exec +UPDATE + workspace_apps +SET + health = $2 +WHERE + id = $1; diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index 58eb2315e16c2..aca06000dc98a 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -812,6 +812,14 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. snapshot.WorkspaceAgents = append(snapshot.WorkspaceAgents, telemetry.ConvertWorkspaceAgent(dbAgent)) for _, app := range prAgent.Apps { + health := database.WorkspaceAppHealthDisabled + if app.Healthcheck == nil { + app.Healthcheck = &sdkproto.Healthcheck{} + } + if app.Healthcheck.Url != "" { + health = database.WorkspaceAppHealthInitializing + } + dbApp, err := db.InsertWorkspaceApp(ctx, database.InsertWorkspaceAppParams{ ID: uuid.New(), CreatedAt: database.Now(), @@ -826,7 +834,11 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. String: app.Url, Valid: app.Url != "", }, - RelativePath: app.RelativePath, + RelativePath: app.RelativePath, + HealthcheckUrl: app.Healthcheck.Url, + HealthcheckInterval: app.Healthcheck.Interval, + HealthcheckThreshold: app.Healthcheck.Threshold, + Health: health, }) if err != nil { return xerrors.Errorf("insert app: %w", err) diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 691564d600409..89c93e3e3a8f0 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -23,7 +23,6 @@ import ( "tailscale.com/tailcfg" "cdr.dev/slog" - "github.com/coder/coder/agent" "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/coderd/httpmw" @@ -61,6 +60,20 @@ func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusOK, apiAgent) } +func (api *API) workspaceAgentApps(rw http.ResponseWriter, r *http.Request) { + workspaceAgent := httpmw.WorkspaceAgent(r) + dbApps, err := api.Database.GetWorkspaceAppsByAgentID(r.Context(), workspaceAgent.ID) + if err != nil && !xerrors.Is(err, sql.ErrNoRows) { + httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace agent applications.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(r.Context(), rw, http.StatusOK, convertApps(dbApps)) +} + func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() workspaceAgent := httpmw.WorkspaceAgent(r) @@ -73,7 +86,7 @@ func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request) return } - httpapi.Write(ctx, rw, http.StatusOK, agent.Metadata{ + httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspaceAgentMetadata{ DERPMap: api.DERPMap, EnvironmentVariables: apiAgent.EnvironmentVariables, StartupScript: apiAgent.StartupScript, @@ -205,7 +218,7 @@ func (api *API) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) { _, _ = io.Copy(ptNetConn, wsNetConn) } -func (api *API) dialWorkspaceAgentTailnet(r *http.Request, agentID uuid.UUID) (*agent.Conn, error) { +func (api *API) dialWorkspaceAgentTailnet(r *http.Request, agentID uuid.UUID) (*codersdk.AgentConn, error) { clientConn, serverConn := net.Pipe() go func() { <-r.Context().Done() @@ -232,7 +245,7 @@ func (api *API) dialWorkspaceAgentTailnet(r *http.Request, agentID uuid.UUID) (* _ = conn.Close() } }() - return &agent.Conn{ + return &codersdk.AgentConn{ Conn: conn, }, nil } @@ -439,6 +452,12 @@ func convertApps(dbApps []database.WorkspaceApp) []codersdk.WorkspaceApp { Name: dbApp.Name, Command: dbApp.Command.String, Icon: dbApp.Icon, + Healthcheck: codersdk.Healthcheck{ + URL: dbApp.HealthcheckUrl, + Interval: dbApp.HealthcheckInterval, + Threshold: dbApp.HealthcheckThreshold, + }, + Health: codersdk.WorkspaceAppHealth(dbApp.Health), }) } return apps @@ -676,6 +695,94 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques } } +func (api *API) postWorkspaceAppHealth(rw http.ResponseWriter, r *http.Request) { + workspaceAgent := httpmw.WorkspaceAgent(r) + var req codersdk.PostWorkspaceAppHealthsRequest + if !httpapi.Read(r.Context(), rw, r, &req) { + return + } + + if req.Healths == nil || len(req.Healths) == 0 { + httpapi.Write(r.Context(), rw, http.StatusBadRequest, codersdk.Response{ + Message: "Health field is empty", + }) + return + } + + apps, err := api.Database.GetWorkspaceAppsByAgentID(r.Context(), workspaceAgent.ID) + if err != nil { + httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Error getting agent apps", + Detail: err.Error(), + }) + return + } + + var newApps []database.WorkspaceApp + for name, newHealth := range req.Healths { + old := func() *database.WorkspaceApp { + for _, app := range apps { + if app.Name == name { + return &app + } + } + + return nil + }() + if old == nil { + httpapi.Write(r.Context(), rw, http.StatusNotFound, codersdk.Response{ + Message: "Error setting workspace app health", + Detail: xerrors.Errorf("workspace app name %s not found", name).Error(), + }) + return + } + + if old.HealthcheckUrl == "" { + httpapi.Write(r.Context(), rw, http.StatusNotFound, codersdk.Response{ + Message: "Error setting workspace app health", + Detail: xerrors.Errorf("health checking is disabled for workspace app %s", name).Error(), + }) + return + } + + switch newHealth { + case codersdk.WorkspaceAppHealthInitializing: + case codersdk.WorkspaceAppHealthHealthy: + case codersdk.WorkspaceAppHealthUnhealthy: + default: + httpapi.Write(r.Context(), rw, http.StatusBadRequest, codersdk.Response{ + Message: "Error setting workspace app health", + Detail: xerrors.Errorf("workspace app health %s is not a valid value", newHealth).Error(), + }) + return + } + + // don't save if the value hasn't changed + if old.Health == database.WorkspaceAppHealth(newHealth) { + continue + } + old.Health = database.WorkspaceAppHealth(newHealth) + + newApps = append(newApps, *old) + } + + for _, app := range newApps { + err = api.Database.UpdateWorkspaceAppHealthByID(r.Context(), database.UpdateWorkspaceAppHealthByIDParams{ + ID: app.ID, + Health: app.Health, + }) + if err != nil { + httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Error setting workspace app health", + Detail: err.Error(), + }) + return + } + } + + httpapi.Write(r.Context(), rw, http.StatusOK, nil) +} + // wsNetConn wraps net.Conn created by websocket.NetConn(). Cancel func // is called if a read or write error is encountered. type wsNetConn struct { diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index c4514c1134427..d92501ad01fd0 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -324,7 +324,7 @@ func TestWorkspaceAgentPTY(t *testing.T) { // First attempt to resize the TTY. // The websocket will close if it fails! - data, err := json.Marshal(agent.ReconnectingPTYRequest{ + data, err := json.Marshal(codersdk.ReconnectingPTYRequest{ Height: 250, Width: 250, }) @@ -337,7 +337,7 @@ func TestWorkspaceAgentPTY(t *testing.T) { // the shell is simultaneously sending a prompt. time.Sleep(100 * time.Millisecond) - data, err = json.Marshal(agent.ReconnectingPTYRequest{ + data, err = json.Marshal(codersdk.ReconnectingPTYRequest{ Data: "echo test\r\n", }) require.NoError(t, err) @@ -363,3 +363,112 @@ func TestWorkspaceAgentPTY(t *testing.T) { expectLine(matchEchoCommand) expectLine(matchEchoOutput) } + +func TestWorkspaceAgentAppHealth(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }) + user := coderdtest.CreateFirstUser(t, client) + authToken := uuid.NewString() + apps := []*proto.App{ + { + Name: "code-server", + Command: "some-command", + Url: "http://localhost:3000", + Icon: "/code.svg", + }, + { + Name: "code-server-2", + Command: "some-command", + Url: "http://localhost:3000", + Icon: "/code.svg", + Healthcheck: &proto.Healthcheck{ + Url: "http://localhost:3000", + Interval: 5, + Threshold: 6, + }, + }, + } + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + Provision: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Resources: []*proto.Resource{{ + Name: "example", + Type: "aws_instance", + Agents: []*proto.Agent{{ + Id: uuid.NewString(), + Auth: &proto.Agent_Token{ + Token: authToken, + }, + Apps: apps, + }}, + }}, + }, + }, + }}, + }) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + agentClient := codersdk.New(client.URL) + agentClient.SessionToken = authToken + + apiApps, err := agentClient.WorkspaceAgentApps(ctx) + require.NoError(t, err) + require.EqualValues(t, codersdk.WorkspaceAppHealthDisabled, apiApps[0].Health) + require.EqualValues(t, codersdk.WorkspaceAppHealthInitializing, apiApps[1].Health) + err = agentClient.PostWorkspaceAgentAppHealth(ctx, codersdk.PostWorkspaceAppHealthsRequest{}) + require.Error(t, err) + // empty + err = agentClient.PostWorkspaceAgentAppHealth(ctx, codersdk.PostWorkspaceAppHealthsRequest{}) + require.Error(t, err) + // invalid name + err = agentClient.PostWorkspaceAgentAppHealth(ctx, codersdk.PostWorkspaceAppHealthsRequest{ + Healths: map[string]codersdk.WorkspaceAppHealth{ + "bad-name": codersdk.WorkspaceAppHealthDisabled, + }, + }) + require.Error(t, err) + // healcheck disabled + err = agentClient.PostWorkspaceAgentAppHealth(ctx, codersdk.PostWorkspaceAppHealthsRequest{ + Healths: map[string]codersdk.WorkspaceAppHealth{ + "code-server": codersdk.WorkspaceAppHealthInitializing, + }, + }) + require.Error(t, err) + // invalid value + err = agentClient.PostWorkspaceAgentAppHealth(ctx, codersdk.PostWorkspaceAppHealthsRequest{ + Healths: map[string]codersdk.WorkspaceAppHealth{ + "code-server-2": codersdk.WorkspaceAppHealth("bad-value"), + }, + }) + require.Error(t, err) + // update to healthy + err = agentClient.PostWorkspaceAgentAppHealth(ctx, codersdk.PostWorkspaceAppHealthsRequest{ + Healths: map[string]codersdk.WorkspaceAppHealth{ + "code-server-2": codersdk.WorkspaceAppHealthHealthy, + }, + }) + require.NoError(t, err) + apiApps, err = agentClient.WorkspaceAgentApps(ctx) + require.NoError(t, err) + require.EqualValues(t, codersdk.WorkspaceAppHealthHealthy, apiApps[1].Health) + // update to unhealthy + err = agentClient.PostWorkspaceAgentAppHealth(ctx, codersdk.PostWorkspaceAppHealthsRequest{ + Healths: map[string]codersdk.WorkspaceAppHealth{ + "code-server-2": codersdk.WorkspaceAppHealthUnhealthy, + }, + }) + require.NoError(t, err) + apiApps, err = agentClient.WorkspaceAgentApps(ctx) + require.NoError(t, err) + require.EqualValues(t, codersdk.WorkspaceAppHealthUnhealthy, apiApps[1].Health) +} diff --git a/coderd/workspaceresources_test.go b/coderd/workspaceresources_test.go index a017f4165102e..6fd0f97fb5e57 100644 --- a/coderd/workspaceresources_test.go +++ b/coderd/workspaceresources_test.go @@ -74,11 +74,24 @@ func TestWorkspaceResource(t *testing.T) { IncludeProvisionerDaemon: true, }) user := coderdtest.CreateFirstUser(t, client) - app := &proto.App{ - Name: "code-server", - Command: "some-command", - Url: "http://localhost:3000", - Icon: "/code.svg", + apps := []*proto.App{ + { + Name: "code-server", + Command: "some-command", + Url: "http://localhost:3000", + Icon: "/code.svg", + }, + { + Name: "code-server-2", + Command: "some-command", + Url: "http://localhost:3000", + Icon: "/code.svg", + Healthcheck: &proto.Healthcheck{ + Url: "http://localhost:3000", + Interval: 5, + Threshold: 6, + }, + }, } version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, @@ -91,7 +104,7 @@ func TestWorkspaceResource(t *testing.T) { Agents: []*proto.Agent{{ Id: "something", Auth: &proto.Agent_Token{}, - Apps: []*proto.App{app}, + Apps: apps, }}, }}, }, @@ -112,11 +125,25 @@ func TestWorkspaceResource(t *testing.T) { require.NoError(t, err) require.Len(t, resource.Agents, 1) agent := resource.Agents[0] - require.Len(t, agent.Apps, 1) + require.Len(t, agent.Apps, 2) got := agent.Apps[0] - require.Equal(t, app.Command, got.Command) - require.Equal(t, app.Icon, got.Icon) - require.Equal(t, app.Name, got.Name) + app := apps[0] + require.EqualValues(t, app.Command, got.Command) + require.EqualValues(t, app.Icon, got.Icon) + require.EqualValues(t, app.Name, got.Name) + require.EqualValues(t, codersdk.WorkspaceAppHealthDisabled, got.Health) + require.EqualValues(t, "", got.Healthcheck.URL) + require.EqualValues(t, 0, got.Healthcheck.Interval) + require.EqualValues(t, 0, got.Healthcheck.Threshold) + got = agent.Apps[1] + app = apps[1] + require.EqualValues(t, app.Command, got.Command) + require.EqualValues(t, app.Icon, got.Icon) + require.EqualValues(t, app.Name, got.Name) + require.EqualValues(t, codersdk.WorkspaceAppHealthInitializing, got.Health) + require.EqualValues(t, app.Healthcheck.Url, got.Healthcheck.URL) + require.EqualValues(t, app.Healthcheck.Interval, got.Healthcheck.Interval) + require.EqualValues(t, app.Healthcheck.Threshold, got.Healthcheck.Threshold) }) t.Run("Metadata", func(t *testing.T) { diff --git a/coderd/wsconncache/wsconncache.go b/coderd/wsconncache/wsconncache.go index 7d3b741a63b7e..252ef5897f195 100644 --- a/coderd/wsconncache/wsconncache.go +++ b/coderd/wsconncache/wsconncache.go @@ -12,7 +12,7 @@ import ( "golang.org/x/sync/singleflight" "golang.org/x/xerrors" - "github.com/coder/coder/agent" + "github.com/coder/coder/codersdk" ) // New creates a new workspace connection cache that closes @@ -32,11 +32,11 @@ func New(dialer Dialer, inactiveTimeout time.Duration) *Cache { } // Dialer creates a new agent connection by ID. -type Dialer func(r *http.Request, id uuid.UUID) (*agent.Conn, error) +type Dialer func(r *http.Request, id uuid.UUID) (*codersdk.AgentConn, error) // Conn wraps an agent connection with a reusable HTTP transport. type Conn struct { - *agent.Conn + *codersdk.AgentConn locks atomic.Uint64 timeoutMutex sync.Mutex @@ -59,7 +59,7 @@ func (c *Conn) CloseWithError(err error) error { if c.timeout != nil { c.timeout.Stop() } - return c.Conn.CloseWithError(err) + return c.AgentConn.CloseWithError(err) } type Cache struct { @@ -98,7 +98,7 @@ func (c *Cache) Acquire(r *http.Request, id uuid.UUID) (*Conn, func(), error) { transport := defaultTransport.Clone() transport.DialContext = agentConn.DialContext conn := &Conn{ - Conn: agentConn, + AgentConn: agentConn, timeoutCancel: timeoutCancelFunc, transport: transport, } diff --git a/coderd/wsconncache/wsconncache_test.go b/coderd/wsconncache/wsconncache_test.go index a9ea85a2492ac..003d3cddb8b7a 100644 --- a/coderd/wsconncache/wsconncache_test.go +++ b/coderd/wsconncache/wsconncache_test.go @@ -23,6 +23,7 @@ import ( "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/agent" "github.com/coder/coder/coderd/wsconncache" + "github.com/coder/coder/codersdk" "github.com/coder/coder/tailnet" "github.com/coder/coder/tailnet/tailnettest" ) @@ -35,8 +36,8 @@ func TestCache(t *testing.T) { t.Parallel() t.Run("Same", func(t *testing.T) { t.Parallel() - cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*agent.Conn, error) { - return setupAgent(t, agent.Metadata{}, 0), nil + cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*codersdk.AgentConn, error) { + return setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0), nil }, 0) defer func() { _ = cache.Close() @@ -50,9 +51,9 @@ func TestCache(t *testing.T) { t.Run("Expire", func(t *testing.T) { t.Parallel() called := atomic.NewInt32(0) - cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*agent.Conn, error) { + cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*codersdk.AgentConn, error) { called.Add(1) - return setupAgent(t, agent.Metadata{}, 0), nil + return setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0), nil }, time.Microsecond) defer func() { _ = cache.Close() @@ -69,8 +70,8 @@ func TestCache(t *testing.T) { }) t.Run("NoExpireWhenLocked", func(t *testing.T) { t.Parallel() - cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*agent.Conn, error) { - return setupAgent(t, agent.Metadata{}, 0), nil + cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*codersdk.AgentConn, error) { + return setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0), nil }, time.Microsecond) defer func() { _ = cache.Close() @@ -102,8 +103,8 @@ func TestCache(t *testing.T) { }() go server.Serve(random) - cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*agent.Conn, error) { - return setupAgent(t, agent.Metadata{}, 0), nil + cache := wsconncache.New(func(r *http.Request, id uuid.UUID) (*codersdk.AgentConn, error) { + return setupAgent(t, codersdk.WorkspaceAgentMetadata{}, 0), nil }, time.Microsecond) defer func() { _ = cache.Close() @@ -139,13 +140,13 @@ func TestCache(t *testing.T) { }) } -func setupAgent(t *testing.T, metadata agent.Metadata, ptyTimeout time.Duration) *agent.Conn { +func setupAgent(t *testing.T, metadata codersdk.WorkspaceAgentMetadata, ptyTimeout time.Duration) *codersdk.AgentConn { metadata.DERPMap = tailnettest.RunDERPAndSTUN(t) coordinator := tailnet.NewCoordinator() agentID := uuid.New() closer := agent.New(agent.Options{ - FetchMetadata: func(ctx context.Context) (agent.Metadata, error) { + FetchMetadata: func(ctx context.Context) (codersdk.WorkspaceAgentMetadata, error) { return metadata, nil }, CoordinatorDialer: func(ctx context.Context) (net.Conn, error) { @@ -180,7 +181,7 @@ func setupAgent(t *testing.T, metadata agent.Metadata, ptyTimeout time.Duration) return conn.UpdateNodes(node) }) conn.SetNodeCallback(sendNode) - return &agent.Conn{ + return &codersdk.AgentConn{ Conn: conn, } } diff --git a/agent/conn.go b/codersdk/agentconn.go similarity index 64% rename from agent/conn.go rename to codersdk/agentconn.go index b64e935af7ecc..3a5dab5158a70 100644 --- a/agent/conn.go +++ b/codersdk/agentconn.go @@ -1,4 +1,4 @@ -package agent +package codersdk import ( "context" @@ -18,23 +18,35 @@ import ( "github.com/coder/coder/tailnet" ) +var ( + // TailnetIP is a static IPv6 address with the Tailscale prefix that is used to route + // connections from clients to this node. A dynamic address is not required because a Tailnet + // client only dials a single agent at a time. + TailnetIP = netip.MustParseAddr("fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4") + TailnetSSHPort = 1 + TailnetReconnectingPTYPort = 2 + TailnetSpeedtestPort = 3 +) + // ReconnectingPTYRequest is sent from the client to the server // to pipe data to a PTY. +// @typescript-ignore ReconnectingPTYRequest type ReconnectingPTYRequest struct { Data string `json:"data"` Height uint16 `json:"height"` Width uint16 `json:"width"` } -type Conn struct { +// @typescript-ignore AgentConn +type AgentConn struct { *tailnet.Conn CloseFunc func() } -func (c *Conn) Ping() (time.Duration, error) { +func (c *AgentConn) Ping() (time.Duration, error) { errCh := make(chan error, 1) durCh := make(chan time.Duration, 1) - c.Conn.Ping(tailnetIP, tailcfg.PingICMP, func(pr *ipnstate.PingResult) { + c.Conn.Ping(TailnetIP, tailcfg.PingICMP, func(pr *ipnstate.PingResult) { if pr.Err != "" { errCh <- xerrors.New(pr.Err) return @@ -49,30 +61,31 @@ func (c *Conn) Ping() (time.Duration, error) { } } -func (c *Conn) CloseWithError(_ error) error { +func (c *AgentConn) CloseWithError(_ error) error { return c.Close() } -func (c *Conn) Close() error { +func (c *AgentConn) Close() error { if c.CloseFunc != nil { c.CloseFunc() } return c.Conn.Close() } -type reconnectingPTYInit struct { +// @typescript-ignore ReconnectingPTYInit +type ReconnectingPTYInit struct { ID string Height uint16 Width uint16 Command string } -func (c *Conn) ReconnectingPTY(id string, height, width uint16, command string) (net.Conn, error) { - conn, err := c.DialContextTCP(context.Background(), netip.AddrPortFrom(tailnetIP, uint16(tailnetReconnectingPTYPort))) +func (c *AgentConn) ReconnectingPTY(id string, height, width uint16, command string) (net.Conn, error) { + conn, err := c.DialContextTCP(context.Background(), netip.AddrPortFrom(TailnetIP, uint16(TailnetReconnectingPTYPort))) if err != nil { return nil, err } - data, err := json.Marshal(reconnectingPTYInit{ + data, err := json.Marshal(ReconnectingPTYInit{ ID: id, Height: height, Width: width, @@ -93,13 +106,13 @@ func (c *Conn) ReconnectingPTY(id string, height, width uint16, command string) return conn, nil } -func (c *Conn) SSH() (net.Conn, error) { - return c.DialContextTCP(context.Background(), netip.AddrPortFrom(tailnetIP, uint16(tailnetSSHPort))) +func (c *AgentConn) SSH() (net.Conn, error) { + return c.DialContextTCP(context.Background(), netip.AddrPortFrom(TailnetIP, uint16(TailnetSSHPort))) } // SSHClient calls SSH to create a client that uses a weak cipher // for high throughput. -func (c *Conn) SSHClient() (*ssh.Client, error) { +func (c *AgentConn) SSHClient() (*ssh.Client, error) { netConn, err := c.SSH() if err != nil { return nil, xerrors.Errorf("ssh: %w", err) @@ -116,8 +129,8 @@ func (c *Conn) SSHClient() (*ssh.Client, error) { return ssh.NewClient(sshConn, channels, requests), nil } -func (c *Conn) Speedtest(direction speedtest.Direction, duration time.Duration) ([]speedtest.Result, error) { - speedConn, err := c.DialContextTCP(context.Background(), netip.AddrPortFrom(tailnetIP, uint16(tailnetSpeedtestPort))) +func (c *AgentConn) Speedtest(direction speedtest.Direction, duration time.Duration) ([]speedtest.Result, error) { + speedConn, err := c.DialContextTCP(context.Background(), netip.AddrPortFrom(TailnetIP, uint16(TailnetSpeedtestPort))) if err != nil { return nil, xerrors.Errorf("dial speedtest: %w", err) } @@ -128,13 +141,13 @@ func (c *Conn) Speedtest(direction speedtest.Direction, duration time.Duration) return results, err } -func (c *Conn) DialContext(ctx context.Context, network string, addr string) (net.Conn, error) { +func (c *AgentConn) DialContext(ctx context.Context, network string, addr string) (net.Conn, error) { if network == "unix" { return nil, xerrors.New("network must be tcp or udp") } _, rawPort, _ := net.SplitHostPort(addr) port, _ := strconv.Atoi(rawPort) - ipp := netip.AddrPortFrom(tailnetIP, uint16(port)) + ipp := netip.AddrPortFrom(TailnetIP, uint16(port)) if network == "udp" { return c.Conn.DialContextUDP(ctx, ipp) } diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index 95832fc625e11..e876fdafd9940 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -21,20 +21,22 @@ import ( "cdr.dev/slog" - "github.com/coder/coder/agent" "github.com/coder/coder/tailnet" "github.com/coder/retry" ) +// @typescript-ignore GoogleInstanceIdentityToken type GoogleInstanceIdentityToken struct { JSONWebToken string `json:"json_web_token" validate:"required"` } +// @typescript-ignore AWSInstanceIdentityToken type AWSInstanceIdentityToken struct { Signature string `json:"signature" validate:"required"` Document string `json:"document" validate:"required"` } +// @typescript-ignore ReconnectingPTYRequest type AzureInstanceIdentityToken struct { Signature string `json:"signature" validate:"required"` Encoding string `json:"encoding" validate:"required"` @@ -42,20 +44,31 @@ type AzureInstanceIdentityToken struct { // WorkspaceAgentAuthenticateResponse is returned when an instance ID // has been exchanged for a session token. +// @typescript-ignore WorkspaceAgentAuthenticateResponse type WorkspaceAgentAuthenticateResponse struct { SessionToken string `json:"session_token"` } // WorkspaceAgentConnectionInfo returns required information for establishing // a connection with a workspace. +// @typescript-ignore WorkspaceAgentConnectionInfo type WorkspaceAgentConnectionInfo struct { DERPMap *tailcfg.DERPMap `json:"derp_map"` } +// @typescript-ignore PostWorkspaceAgentVersionRequest type PostWorkspaceAgentVersionRequest struct { Version string `json:"version"` } +// @typescript-ignore WorkspaceAgentMetadata +type WorkspaceAgentMetadata struct { + DERPMap *tailcfg.DERPMap `json:"derpmap"` + EnvironmentVariables map[string]string `json:"environment_variables"` + StartupScript string `json:"startup_script"` + Directory string `json:"directory"` +} + // AuthWorkspaceGoogleInstanceIdentity uses the Google Compute Engine Metadata API to // fetch a signed JWT, and exchange it for a session token for a workspace agent. // @@ -185,16 +198,16 @@ func (c *Client) AuthWorkspaceAzureInstanceIdentity(ctx context.Context) (Worksp } // WorkspaceAgentMetadata fetches metadata for the currently authenticated workspace agent. -func (c *Client) WorkspaceAgentMetadata(ctx context.Context) (agent.Metadata, error) { +func (c *Client) WorkspaceAgentMetadata(ctx context.Context) (WorkspaceAgentMetadata, error) { res, err := c.Request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/metadata", nil) if err != nil { - return agent.Metadata{}, err + return WorkspaceAgentMetadata{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return agent.Metadata{}, readBodyAsError(res) + return WorkspaceAgentMetadata{}, readBodyAsError(res) } - var agentMetadata agent.Metadata + var agentMetadata WorkspaceAgentMetadata return agentMetadata, json.NewDecoder(res.Body).Decode(&agentMetadata) } @@ -228,7 +241,7 @@ func (c *Client) ListenWorkspaceAgentTailnet(ctx context.Context) (net.Conn, err return websocket.NetConn(ctx, conn, websocket.MessageBinary), nil } -func (c *Client) DialWorkspaceAgentTailnet(ctx context.Context, logger slog.Logger, agentID uuid.UUID) (*agent.Conn, error) { +func (c *Client) DialWorkspaceAgentTailnet(ctx context.Context, logger slog.Logger, agentID uuid.UUID) (*AgentConn, error) { res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceagents/%s/connection", agentID), nil) if err != nil { return nil, err @@ -325,7 +338,7 @@ func (c *Client) DialWorkspaceAgentTailnet(ctx context.Context, logger slog.Logg _ = conn.Close() return nil, err } - return &agent.Conn{ + return &AgentConn{ Conn: conn, CloseFunc: func() { cancelFunc() @@ -348,6 +361,34 @@ func (c *Client) WorkspaceAgent(ctx context.Context, id uuid.UUID) (WorkspaceAge return workspaceAgent, json.NewDecoder(res.Body).Decode(&workspaceAgent) } +// MyWorkspaceAgent returns the requesting agent. +func (c *Client) WorkspaceAgentApps(ctx context.Context) ([]WorkspaceApp, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/apps", nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, readBodyAsError(res) + } + var workspaceApps []WorkspaceApp + return workspaceApps, json.NewDecoder(res.Body).Decode(&workspaceApps) +} + +// PostWorkspaceAgentAppHealth updates the workspace agent app health status. +func (c *Client) PostWorkspaceAgentAppHealth(ctx context.Context, req PostWorkspaceAppHealthsRequest) error { + res, err := c.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/me/app-health", req) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return readBodyAsError(res) + } + + return nil +} + func (c *Client) PostWorkspaceAgentVersion(ctx context.Context, version string) error { // Phone home and tell the mothership what version we're on. versionReq := PostWorkspaceAgentVersionRequest{Version: version} @@ -392,12 +433,22 @@ func (c *Client) WorkspaceAgentReconnectingPTY(ctx context.Context, agentID, rec return websocket.NetConn(ctx, conn, websocket.MessageBinary), nil } +// Stats records the Agent's network connection statistics for use in +// user-facing metrics and debugging. +// Each member value must be written and read with atomic. +// @typescript-ignore AgentStats +type AgentStats struct { + NumConns int64 `json:"num_comms"` + RxBytes int64 `json:"rx_bytes"` + TxBytes int64 `json:"tx_bytes"` +} + // AgentReportStats begins a stat streaming connection with the Coder server. // It is resilient to network failures and intermittent coderd issues. func (c *Client) AgentReportStats( ctx context.Context, log slog.Logger, - stats func() *agent.Stats, + stats func() *AgentStats, ) (io.Closer, error) { serverURL, err := c.URL.Parse("/api/v2/workspaceagents/me/report-stats") if err != nil { diff --git a/codersdk/workspaceapps.go b/codersdk/workspaceapps.go index d993a4dcf49ba..168e3c0d597c9 100644 --- a/codersdk/workspaceapps.go +++ b/codersdk/workspaceapps.go @@ -4,6 +4,15 @@ import ( "github.com/google/uuid" ) +type WorkspaceAppHealth string + +const ( + WorkspaceAppHealthDisabled WorkspaceAppHealth = "disabled" + WorkspaceAppHealthInitializing WorkspaceAppHealth = "initializing" + WorkspaceAppHealthHealthy WorkspaceAppHealth = "healthy" + WorkspaceAppHealthUnhealthy WorkspaceAppHealth = "unhealthy" +) + type WorkspaceApp struct { ID uuid.UUID `json:"id"` // Name is a unique identifier attached to an agent. @@ -12,4 +21,22 @@ type WorkspaceApp struct { // Icon is a relative path or external URL that specifies // an icon to be displayed in the dashboard. Icon string `json:"icon,omitempty"` + // Healthcheck specifies the configuration for checking app health. + Healthcheck Healthcheck `json:"healthcheck"` + Health WorkspaceAppHealth `json:"health"` +} + +type Healthcheck struct { + // URL specifies the url to check for the app health. + URL string `json:"url"` + // Interval specifies the seconds between each health check. + Interval int32 `json:"interval"` + // Threshold specifies the number of consecutive failed health checks before returning "unhealthy". + Threshold int32 `json:"threshold"` +} + +// @typescript-ignore PostWorkspaceAppHealthsRequest +type PostWorkspaceAppHealthsRequest struct { + // Healths is a map of the workspace app name and the health of the app. + Healths map[string]WorkspaceAppHealth } diff --git a/dogfood/main.tf b/dogfood/main.tf index d8aaa943a47b8..2032adc18212e 100644 --- a/dogfood/main.tf +++ b/dogfood/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.4.5" + version = "0.4.15" } docker = { source = "kreuzwerker/docker" diff --git a/examples/templates/aws-ecs-container/main.tf b/examples/templates/aws-ecs-container/main.tf index 63dd2a1fb0b08..783279a2213b1 100644 --- a/examples/templates/aws-ecs-container/main.tf +++ b/examples/templates/aws-ecs-container/main.tf @@ -6,7 +6,7 @@ terraform { } coder = { source = "coder/coder" - version = "0.4.11" + version = "0.4.15" } } } diff --git a/examples/templates/aws-linux/main.tf b/examples/templates/aws-linux/main.tf index 05c4cb42e8639..7d1e156d7fcd3 100644 --- a/examples/templates/aws-linux/main.tf +++ b/examples/templates/aws-linux/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.4.11" + version = "0.4.15" } } } diff --git a/examples/templates/aws-windows/main.tf b/examples/templates/aws-windows/main.tf index 965cb2573bd4a..2c995c1e87579 100644 --- a/examples/templates/aws-windows/main.tf +++ b/examples/templates/aws-windows/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.4.11" + version = "0.4.15" } } } diff --git a/examples/templates/azure-linux/main.tf b/examples/templates/azure-linux/main.tf index 85b86a4296675..33ed6d0170342 100644 --- a/examples/templates/azure-linux/main.tf +++ b/examples/templates/azure-linux/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.4.11" + version = "0.4.15" } azurerm = { source = "hashicorp/azurerm" diff --git a/examples/templates/do-linux/main.tf b/examples/templates/do-linux/main.tf index 0468c45c21e9a..526bd4f65bba9 100644 --- a/examples/templates/do-linux/main.tf +++ b/examples/templates/do-linux/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.4.11" + version = "0.4.15" } digitalocean = { source = "digitalocean/digitalocean" diff --git a/examples/templates/docker-code-server/main.tf b/examples/templates/docker-code-server/main.tf index 16fcf07a49095..8efcbfb48092d 100644 --- a/examples/templates/docker-code-server/main.tf +++ b/examples/templates/docker-code-server/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.4.11" + version = "0.4.15" } docker = { source = "kreuzwerker/docker" diff --git a/examples/templates/docker-image-builds/main.tf b/examples/templates/docker-image-builds/main.tf index fc54239e793c5..7ffb3991ca11a 100644 --- a/examples/templates/docker-image-builds/main.tf +++ b/examples/templates/docker-image-builds/main.tf @@ -3,7 +3,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.4.11" + version = "0.4.15" } docker = { source = "kreuzwerker/docker" diff --git a/examples/templates/docker-with-dotfiles/main.tf b/examples/templates/docker-with-dotfiles/main.tf index ceb2f21d801d4..594cdb72d39e4 100644 --- a/examples/templates/docker-with-dotfiles/main.tf +++ b/examples/templates/docker-with-dotfiles/main.tf @@ -9,7 +9,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.4.11" + version = "0.4.15" } docker = { source = "kreuzwerker/docker" diff --git a/examples/templates/docker/main.tf b/examples/templates/docker/main.tf index 68d2f39a73833..36ce85da7f5f3 100644 --- a/examples/templates/docker/main.tf +++ b/examples/templates/docker/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.4.11" + version = "0.4.15" } docker = { source = "kreuzwerker/docker" @@ -47,6 +47,11 @@ resource "coder_app" "code-server" { name = "code-server" url = "http://localhost:13337/?folder=/home/coder" icon = "/icon/code.svg" + healthcheck { + url = "http://localhost:13337/healthz" + interval = 5 + threshold = 6 + } } diff --git a/examples/templates/gcp-linux/main.tf b/examples/templates/gcp-linux/main.tf index 647fb6ac61ec9..533866cd44723 100644 --- a/examples/templates/gcp-linux/main.tf +++ b/examples/templates/gcp-linux/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.4.11" + version = "0.4.15" } google = { source = "hashicorp/google" diff --git a/examples/templates/gcp-vm-container/main.tf b/examples/templates/gcp-vm-container/main.tf index 17cf48884f035..8f7bccaf81149 100644 --- a/examples/templates/gcp-vm-container/main.tf +++ b/examples/templates/gcp-vm-container/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.4.11" + version = "0.4.15" } google = { source = "hashicorp/google" diff --git a/examples/templates/gcp-windows/main.tf b/examples/templates/gcp-windows/main.tf index eaba792b94c87..25e1e90bd9f9c 100644 --- a/examples/templates/gcp-windows/main.tf +++ b/examples/templates/gcp-windows/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.4.11" + version = "0.4.15" } google = { source = "hashicorp/google" diff --git a/examples/templates/kubernetes/main.tf b/examples/templates/kubernetes/main.tf index f2380b436cb5c..edcd4cce19f55 100644 --- a/examples/templates/kubernetes/main.tf +++ b/examples/templates/kubernetes/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.4.11" + version = "0.4.15" } kubernetes = { source = "hashicorp/kubernetes" diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index 264cdad139899..22685c566120a 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -1,6 +1,8 @@ package terraform import ( + "encoding/json" + "fmt" "strings" "github.com/awalterschulze/gographviz" @@ -25,12 +27,20 @@ type agentAttributes struct { // A mapping of attributes on the "coder_app" resource. type agentAppAttributes struct { - AgentID string `mapstructure:"agent_id"` - Name string `mapstructure:"name"` - Icon string `mapstructure:"icon"` - URL string `mapstructure:"url"` - Command string `mapstructure:"command"` - RelativePath bool `mapstructure:"relative_path"` + AgentID string `mapstructure:"agent_id"` + Name string `mapstructure:"name"` + Icon string `mapstructure:"icon"` + URL string `mapstructure:"url"` + Command string `mapstructure:"command"` + RelativePath bool `mapstructure:"relative_path"` + Healthcheck []appHealthcheckAttributes `mapstructure:"healthcheck"` +} + +// A mapping of attributes on the "healthcheck" resource. +type appHealthcheckAttributes struct { + URL string `mapstructure:"url"` + Interval int32 `mapstructure:"interval"` + Threshold int32 `mapstructure:"threshold"` } // A mapping of attributes on the "coder_metadata" resource. @@ -212,12 +222,22 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res var attrs agentAppAttributes err = mapstructure.Decode(resource.AttributeValues, &attrs) if err != nil { + d, _ := json.MarshalIndent(resource.AttributeValues, "", " ") + fmt.Print(string(d)) return nil, xerrors.Errorf("decode app attributes: %w", err) } if attrs.Name == "" { // Default to the resource name if none is set! attrs.Name = resource.Name } + var healthcheck *proto.Healthcheck + if len(attrs.Healthcheck) != 0 { + healthcheck = &proto.Healthcheck{ + Url: attrs.Healthcheck[0].URL, + Interval: attrs.Healthcheck[0].Interval, + Threshold: attrs.Healthcheck[0].Threshold, + } + } for _, agents := range resourceAgents { for _, agent := range agents { // Find agents with the matching ID and associate them! @@ -230,6 +250,7 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res Url: attrs.URL, Icon: attrs.Icon, RelativePath: attrs.RelativePath, + Healthcheck: healthcheck, }) } } diff --git a/provisioner/terraform/resources_test.go b/provisioner/terraform/resources_test.go index 5e2cdbc7fc588..7330a215eac17 100644 --- a/provisioner/terraform/resources_test.go +++ b/provisioner/terraform/resources_test.go @@ -112,6 +112,11 @@ func TestConvertResources(t *testing.T) { Name: "app1", }, { Name: "app2", + Healthcheck: &proto.Healthcheck{ + Url: "http://localhost:13337/healthz", + Interval: 5, + Threshold: 6, + }, }}, Auth: &proto.Agent_Token{}, }}, diff --git a/provisioner/terraform/testdata/calling-module/calling-module.tf b/provisioner/terraform/testdata/calling-module/calling-module.tf index 14303795cff4c..894e082e30aab 100644 --- a/provisioner/terraform/testdata/calling-module/calling-module.tf +++ b/provisioner/terraform/testdata/calling-module/calling-module.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.4.11" + version = "0.4.15" } } } diff --git a/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json b/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json index 0518f8dbced64..fe61412e402c2 100644 --- a/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json +++ b/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json @@ -16,11 +16,11 @@ "auth": "token", "dir": null, "env": null, - "id": "f05ddf9e-106a-4669-bba8-5e2289bd891d", + "id": "f5435556-71b4-4e9c-a961-474ef4c70836", "init_script": "", "os": "linux", "startup_script": null, - "token": "ed4655b9-e917-44af-8706-a1215384a35f" + "token": "cbe1cec2-8c52-4411-ab1b-c7e9aa4e93ea" }, "sensitive_values": {} } @@ -44,7 +44,7 @@ "outputs": { "script": "" }, - "random": "7640853885488752810" + "random": "2977741887145450154" }, "sensitive_values": { "inputs": {}, @@ -59,7 +59,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "6481148597794195898", + "id": "3098344175322958112", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tf b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tf index 75347019d2247..fb9c1fb0ffaa4 100644 --- a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tf +++ b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.4.11" + version = "0.4.15" } } } diff --git a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json index 95c62fe5cde09..1986782431efb 100644 --- a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json +++ b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json @@ -16,11 +16,11 @@ "auth": "token", "dir": null, "env": null, - "id": "fcd8018c-7e4a-4e92-855b-e02319ab051e", + "id": "846b2cd1-1dcc-4b26-ad71-8508c8d71738", "init_script": "", "os": "linux", "startup_script": null, - "token": "ad906408-0eb0-4844-83f7-0f5070427e1c" + "token": "3a3e4e25-6be2-4b51-a369-957fdb243a4f" }, "sensitive_values": {} }, @@ -32,7 +32,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "2672857180605476162", + "id": "8441562949971496089", "triggers": null }, "sensitive_values": {}, @@ -49,7 +49,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "264584188140644760", + "id": "4737933879128730392", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tf b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tf index db0787b2dd550..d5b50dcd864b9 100644 --- a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tf +++ b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.4.11" + version = "0.4.15" } } } diff --git a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json index 58152817465d9..c9c411da2b9fd 100644 --- a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json +++ b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json @@ -16,11 +16,11 @@ "auth": "token", "dir": null, "env": null, - "id": "e3df7d56-17ce-4d8a-9d4e-30ea41cc8a93", + "id": "2efd4acf-bb30-4713-98b5-21fef293c995", "init_script": "", "os": "linux", "startup_script": null, - "token": "1717f79d-2c72-440e-a5c6-e4b8c3fef084" + "token": "7db84d6e-c079-4b4a-99e0-e2414a70df84" }, "sensitive_values": {} }, @@ -32,7 +32,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "2957375211969224115", + "id": "6618109150570768254", "triggers": null }, "sensitive_values": {}, @@ -48,7 +48,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "6924176854496195292", + "id": "4505836003282545145", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/instance-id/instance-id.tf b/provisioner/terraform/testdata/instance-id/instance-id.tf index a56988dcc1f81..3e92a8d7799a6 100644 --- a/provisioner/terraform/testdata/instance-id/instance-id.tf +++ b/provisioner/terraform/testdata/instance-id/instance-id.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.4.11" + version = "0.4.15" } } } diff --git a/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json b/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json index c288ecbe3d770..c726acf85432d 100644 --- a/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json +++ b/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json @@ -16,11 +16,11 @@ "auth": "google-instance-identity", "dir": null, "env": null, - "id": "9a37096a-7f01-42cd-93d8-9f4572c94489", + "id": "e2d2e12e-1975-4bca-8a96-67d6b303b25b", "init_script": "", "os": "linux", "startup_script": null, - "token": "7784ea1f-7fe5-463f-af8d-255c32d12992" + "token": "87ba2736-3519-4368-b9ee-4132bd042fe3" }, "sensitive_values": {} }, @@ -32,8 +32,8 @@ "provider_name": "registry.terraform.io/coder/coder", "schema_version": 0, "values": { - "agent_id": "9a37096a-7f01-42cd-93d8-9f4572c94489", - "id": "8ed448e2-51d7-4cc7-9e26-a3a77f252b1d", + "agent_id": "e2d2e12e-1975-4bca-8a96-67d6b303b25b", + "id": "979121e7-2a41-432a-aa90-8b0d2d802b50", "instance_id": "example" }, "sensitive_values": {}, @@ -49,7 +49,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "771742387122791362", + "id": "3316746911978433294", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tf b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tf index 2aea125c0fec9..5186fc26a09b2 100644 --- a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tf +++ b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.4.11" + version = "0.4.15" } } } diff --git a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json index 3755a14d44abd..56b5e1cc708c3 100644 --- a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json +++ b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json @@ -16,11 +16,11 @@ "auth": "token", "dir": null, "env": null, - "id": "0c3c20d8-8a1d-4fc9-bc73-ed45ddad9a9d", + "id": "032d71aa-570d-4d0a-bce8-57b9d884b694", "init_script": "", "os": "linux", "startup_script": null, - "token": "48b3f4c4-4bb9-477c-8d32-d1e14188e5f8" + "token": "7a0df6bf-313d-4f73-ba2c-6532d72cb808" }, "sensitive_values": {} }, @@ -36,11 +36,11 @@ "auth": "token", "dir": null, "env": null, - "id": "08e8ebc8-4660-47f0-acb5-6ca46747919d", + "id": "019ae4b9-ae5c-4837-be16-dae99b911acf", "init_script": "", "os": "darwin", "startup_script": null, - "token": "827a1f01-a2d7-4794-ab73-8fd8442010d5" + "token": "9f4adbf4-9113-42f4-bb84-d1621262b1e2" }, "sensitive_values": {} }, @@ -56,11 +56,11 @@ "auth": "token", "dir": null, "env": null, - "id": "50f52bd4-a52b-4c73-bf99-fe956913bca4", + "id": "8f2c3b12-e112-405e-9fbf-fe540ed3fe21", "init_script": "", "os": "windows", "startup_script": null, - "token": "159d6407-a913-4e05-8ba7-786d47a7e34b" + "token": "1a6ddbc7-77a9-43c2-9e60-c84d3ecf512a" }, "sensitive_values": {} }, @@ -72,7 +72,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "2529387636030139440", + "id": "6351611769218065391", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf index 456d00ec6abc1..02e42868839c8 100644 --- a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf +++ b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.4.11" + version = "0.4.15" } } } @@ -18,6 +18,11 @@ resource "coder_app" "app1" { resource "coder_app" "app2" { agent_id = coder_agent.dev1.id + healthcheck { + url = "http://localhost:13337/healthz" + interval = 5 + threshold = 6 + } } resource "null_resource" "dev" { diff --git a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json index d728eb2c88c43..6b117d913769a 100644 --- a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json +++ b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json @@ -30,12 +30,15 @@ "schema_version": 0, "values": { "command": null, + "healthcheck": [], "icon": null, "name": null, "relative_path": null, "url": null }, - "sensitive_values": {} + "sensitive_values": { + "healthcheck": [] + } }, { "address": "coder_app.app2", @@ -46,12 +49,23 @@ "schema_version": 0, "values": { "command": null, + "healthcheck": [ + { + "interval": 5, + "threshold": 6, + "url": "http://localhost:13337/healthz" + } + ], "icon": null, "name": null, "relative_path": null, "url": null }, - "sensitive_values": {} + "sensitive_values": { + "healthcheck": [ + {} + ] + } }, { "address": "null_resource.dev", @@ -94,7 +108,9 @@ "token": true }, "before_sensitive": false, - "after_sensitive": {} + "after_sensitive": { + "token": true + } } }, { @@ -110,6 +126,7 @@ "before": null, "after": { "command": null, + "healthcheck": [], "icon": null, "name": null, "relative_path": null, @@ -117,10 +134,13 @@ }, "after_unknown": { "agent_id": true, + "healthcheck": [], "id": true }, "before_sensitive": false, - "after_sensitive": {} + "after_sensitive": { + "healthcheck": [] + } } }, { @@ -136,6 +156,13 @@ "before": null, "after": { "command": null, + "healthcheck": [ + { + "interval": 5, + "threshold": 6, + "url": "http://localhost:13337/healthz" + } + ], "icon": null, "name": null, "relative_path": null, @@ -143,10 +170,17 @@ }, "after_unknown": { "agent_id": true, + "healthcheck": [ + {} + ], "id": true }, "before_sensitive": false, - "after_sensitive": {} + "after_sensitive": { + "healthcheck": [ + {} + ] + } } }, { @@ -176,7 +210,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.4.11" + "version_constraint": "0.4.14" }, "null": { "name": "null", @@ -229,7 +263,20 @@ "coder_agent.dev1.id", "coder_agent.dev1" ] - } + }, + "healthcheck": [ + { + "interval": { + "constant_value": 5 + }, + "threshold": { + "constant_value": 6 + }, + "url": { + "constant_value": "http://localhost:13337/healthz" + } + } + ] }, "schema_version": 0 }, diff --git a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfstate.json b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfstate.json index ca16a470ca1bb..c703dd490e878 100644 --- a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfstate.json +++ b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfstate.json @@ -16,11 +16,11 @@ "auth": "token", "dir": null, "env": null, - "id": "3d4ee1d5-6413-4dc7-baec-2fa9dbd870ba", + "id": "685dba1f-09de-40c0-8fc0-4d8ca00ef946", "init_script": "", "os": "linux", "startup_script": null, - "token": "32e082d7-af02-42f1-a5bd-f6adc34220a1" + "token": "2c73d680-ef4c-4bc1-80f0-f6916e4e5255" }, "sensitive_values": {} }, @@ -32,15 +32,18 @@ "provider_name": "registry.terraform.io/coder/coder", "schema_version": 0, "values": { - "agent_id": "3d4ee1d5-6413-4dc7-baec-2fa9dbd870ba", + "agent_id": "685dba1f-09de-40c0-8fc0-4d8ca00ef946", "command": null, + "healthcheck": [], "icon": null, - "id": "90e045f9-19f1-4d8a-8021-be61c44ee54f", + "id": "46f8d3cd-bcf7-4792-8d54-66e01e63018a", "name": null, "relative_path": null, "url": null }, - "sensitive_values": {}, + "sensitive_values": { + "healthcheck": [] + }, "depends_on": [ "coder_agent.dev1" ] @@ -53,15 +56,26 @@ "provider_name": "registry.terraform.io/coder/coder", "schema_version": 0, "values": { - "agent_id": "3d4ee1d5-6413-4dc7-baec-2fa9dbd870ba", + "agent_id": "685dba1f-09de-40c0-8fc0-4d8ca00ef946", "command": null, + "healthcheck": [ + { + "interval": 5, + "threshold": 6, + "url": "http://localhost:13337/healthz" + } + ], "icon": null, - "id": "873026f8-3050-4b0b-bebf-41e13e5949bb", + "id": "e4556c74-2f67-4266-b1e8-7ee61d754583", "name": null, "relative_path": null, "url": null }, - "sensitive_values": {}, + "sensitive_values": { + "healthcheck": [ + {} + ] + }, "depends_on": [ "coder_agent.dev1" ] @@ -74,7 +88,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "4447693752005094678", + "id": "2997000197756647168", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf index bed06efe7520e..110f07099db70 100644 --- a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf +++ b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.4.11" + version = "0.4.15" } } } diff --git a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json index bc017d9e13ca9..2873d610f87ba 100644 --- a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json +++ b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json @@ -16,11 +16,11 @@ "auth": "token", "dir": null, "env": null, - "id": "09aac2a4-9d8e-43ef-83cb-34657db199f4", + "id": "50a0466c-d983-422f-8bed-9dd0bf705a9a", "init_script": "", "os": "linux", "startup_script": null, - "token": "a0f6b8af-8edc-447f-b6d2-67a60ecd2a77" + "token": "aa714059-3579-49d1-a0e2-3519dbe43688" }, "sensitive_values": {} }, @@ -34,7 +34,7 @@ "values": { "hide": true, "icon": "/icon/server.svg", - "id": "a7f9cf03-de78-4d17-bcbb-21dc34c2d86a", + "id": "64a47d31-28d0-4a50-8e09-a3e705278305", "item": [ { "is_null": false, @@ -61,7 +61,7 @@ "value": "squirrel" } ], - "resource_id": "6209384655473556868" + "resource_id": "4887255791781048166" }, "sensitive_values": { "item": [ @@ -83,7 +83,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "6209384655473556868", + "id": "4887255791781048166", "triggers": null }, "sensitive_values": {} diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index b977e00a42e36..e6134519976f3 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -850,11 +850,12 @@ type App struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Command string `protobuf:"bytes,2,opt,name=command,proto3" json:"command,omitempty"` - Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"` - Icon string `protobuf:"bytes,4,opt,name=icon,proto3" json:"icon,omitempty"` - RelativePath bool `protobuf:"varint,5,opt,name=relative_path,json=relativePath,proto3" json:"relative_path,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Command string `protobuf:"bytes,2,opt,name=command,proto3" json:"command,omitempty"` + Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"` + Icon string `protobuf:"bytes,4,opt,name=icon,proto3" json:"icon,omitempty"` + RelativePath bool `protobuf:"varint,5,opt,name=relative_path,json=relativePath,proto3" json:"relative_path,omitempty"` + Healthcheck *Healthcheck `protobuf:"bytes,6,opt,name=healthcheck,proto3" json:"healthcheck,omitempty"` } func (x *App) Reset() { @@ -924,6 +925,77 @@ func (x *App) GetRelativePath() bool { return false } +func (x *App) GetHealthcheck() *Healthcheck { + if x != nil { + return x.Healthcheck + } + return nil +} + +// Healthcheck represents configuration for checking for app readiness. +type Healthcheck struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + Interval int32 `protobuf:"varint,2,opt,name=interval,proto3" json:"interval,omitempty"` + Threshold int32 `protobuf:"varint,3,opt,name=threshold,proto3" json:"threshold,omitempty"` +} + +func (x *Healthcheck) Reset() { + *x = Healthcheck{} + if protoimpl.UnsafeEnabled { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Healthcheck) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Healthcheck) ProtoMessage() {} + +func (x *Healthcheck) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Healthcheck.ProtoReflect.Descriptor instead. +func (*Healthcheck) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{9} +} + +func (x *Healthcheck) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *Healthcheck) GetInterval() int32 { + if x != nil { + return x.Interval + } + return 0 +} + +func (x *Healthcheck) GetThreshold() int32 { + if x != nil { + return x.Threshold + } + return 0 +} + // Resource represents created infrastructure. type Resource struct { state protoimpl.MessageState @@ -941,7 +1013,7 @@ type Resource struct { func (x *Resource) Reset() { *x = Resource{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[9] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -954,7 +1026,7 @@ func (x *Resource) String() string { func (*Resource) ProtoMessage() {} func (x *Resource) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[9] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -967,7 +1039,7 @@ func (x *Resource) ProtoReflect() protoreflect.Message { // Deprecated: Use Resource.ProtoReflect.Descriptor instead. func (*Resource) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{9} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10} } func (x *Resource) GetName() string { @@ -1022,7 +1094,7 @@ type Parse struct { func (x *Parse) Reset() { *x = Parse{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[10] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1035,7 +1107,7 @@ func (x *Parse) String() string { func (*Parse) ProtoMessage() {} func (x *Parse) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[10] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1048,7 +1120,7 @@ func (x *Parse) ProtoReflect() protoreflect.Message { // Deprecated: Use Parse.ProtoReflect.Descriptor instead. func (*Parse) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11} } // Provision consumes source-code from a directory to produce resources. @@ -1061,7 +1133,7 @@ type Provision struct { func (x *Provision) Reset() { *x = Provision{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[11] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1074,7 +1146,7 @@ func (x *Provision) String() string { func (*Provision) ProtoMessage() {} func (x *Provision) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[11] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1087,7 +1159,7 @@ func (x *Provision) ProtoReflect() protoreflect.Message { // Deprecated: Use Provision.ProtoReflect.Descriptor instead. func (*Provision) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{12} } type Resource_Metadata struct { @@ -1104,7 +1176,7 @@ type Resource_Metadata struct { func (x *Resource_Metadata) Reset() { *x = Resource_Metadata{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[13] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1117,7 +1189,7 @@ func (x *Resource_Metadata) String() string { func (*Resource_Metadata) ProtoMessage() {} func (x *Resource_Metadata) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[13] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1130,7 +1202,7 @@ func (x *Resource_Metadata) ProtoReflect() protoreflect.Message { // Deprecated: Use Resource_Metadata.ProtoReflect.Descriptor instead. func (*Resource_Metadata) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{9, 0} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10, 0} } func (x *Resource_Metadata) GetKey() string { @@ -1172,7 +1244,7 @@ type Parse_Request struct { func (x *Parse_Request) Reset() { *x = Parse_Request{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1185,7 +1257,7 @@ func (x *Parse_Request) String() string { func (*Parse_Request) ProtoMessage() {} func (x *Parse_Request) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1198,7 +1270,7 @@ func (x *Parse_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use Parse_Request.ProtoReflect.Descriptor instead. func (*Parse_Request) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10, 0} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11, 0} } func (x *Parse_Request) GetDirectory() string { @@ -1219,7 +1291,7 @@ type Parse_Complete struct { func (x *Parse_Complete) Reset() { *x = Parse_Complete{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1232,7 +1304,7 @@ func (x *Parse_Complete) String() string { func (*Parse_Complete) ProtoMessage() {} func (x *Parse_Complete) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1245,7 +1317,7 @@ func (x *Parse_Complete) ProtoReflect() protoreflect.Message { // Deprecated: Use Parse_Complete.ProtoReflect.Descriptor instead. func (*Parse_Complete) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10, 1} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11, 1} } func (x *Parse_Complete) GetParameterSchemas() []*ParameterSchema { @@ -1270,7 +1342,7 @@ type Parse_Response struct { func (x *Parse_Response) Reset() { *x = Parse_Response{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[16] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1283,7 +1355,7 @@ func (x *Parse_Response) String() string { func (*Parse_Response) ProtoMessage() {} func (x *Parse_Response) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[16] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1296,7 +1368,7 @@ func (x *Parse_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use Parse_Response.ProtoReflect.Descriptor instead. func (*Parse_Response) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10, 2} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11, 2} } func (m *Parse_Response) GetType() isParse_Response_Type { @@ -1353,7 +1425,7 @@ type Provision_Metadata struct { func (x *Provision_Metadata) Reset() { *x = Provision_Metadata{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1366,7 +1438,7 @@ func (x *Provision_Metadata) String() string { func (*Provision_Metadata) ProtoMessage() {} func (x *Provision_Metadata) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1379,7 +1451,7 @@ func (x *Provision_Metadata) ProtoReflect() protoreflect.Message { // Deprecated: Use Provision_Metadata.ProtoReflect.Descriptor instead. func (*Provision_Metadata) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11, 0} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{12, 0} } func (x *Provision_Metadata) GetCoderUrl() string { @@ -1446,7 +1518,7 @@ type Provision_Start struct { func (x *Provision_Start) Reset() { *x = Provision_Start{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1459,7 +1531,7 @@ func (x *Provision_Start) String() string { func (*Provision_Start) ProtoMessage() {} func (x *Provision_Start) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1472,7 +1544,7 @@ func (x *Provision_Start) ProtoReflect() protoreflect.Message { // Deprecated: Use Provision_Start.ProtoReflect.Descriptor instead. func (*Provision_Start) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11, 1} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{12, 1} } func (x *Provision_Start) GetDirectory() string { @@ -1519,7 +1591,7 @@ type Provision_Cancel struct { func (x *Provision_Cancel) Reset() { *x = Provision_Cancel{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1532,7 +1604,7 @@ func (x *Provision_Cancel) String() string { func (*Provision_Cancel) ProtoMessage() {} func (x *Provision_Cancel) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1545,7 +1617,7 @@ func (x *Provision_Cancel) ProtoReflect() protoreflect.Message { // Deprecated: Use Provision_Cancel.ProtoReflect.Descriptor instead. func (*Provision_Cancel) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11, 2} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{12, 2} } type Provision_Request struct { @@ -1563,7 +1635,7 @@ type Provision_Request struct { func (x *Provision_Request) Reset() { *x = Provision_Request{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1576,7 +1648,7 @@ func (x *Provision_Request) String() string { func (*Provision_Request) ProtoMessage() {} func (x *Provision_Request) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1589,7 +1661,7 @@ func (x *Provision_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use Provision_Request.ProtoReflect.Descriptor instead. func (*Provision_Request) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11, 3} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{12, 3} } func (m *Provision_Request) GetType() isProvision_Request_Type { @@ -1642,7 +1714,7 @@ type Provision_Complete struct { func (x *Provision_Complete) Reset() { *x = Provision_Complete{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[21] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1655,7 +1727,7 @@ func (x *Provision_Complete) String() string { func (*Provision_Complete) ProtoMessage() {} func (x *Provision_Complete) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[21] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1668,7 +1740,7 @@ func (x *Provision_Complete) ProtoReflect() protoreflect.Message { // Deprecated: Use Provision_Complete.ProtoReflect.Descriptor instead. func (*Provision_Complete) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11, 4} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{12, 4} } func (x *Provision_Complete) GetState() []byte { @@ -1707,7 +1779,7 @@ type Provision_Response struct { func (x *Provision_Response) Reset() { *x = Provision_Response{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[22] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1720,7 +1792,7 @@ func (x *Provision_Response) String() string { func (*Provision_Response) ProtoMessage() {} func (x *Provision_Response) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[22] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1733,7 +1805,7 @@ func (x *Provision_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use Provision_Response.ProtoReflect.Descriptor instead. func (*Provision_Response) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11, 5} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{12, 5} } func (m *Provision_Response) GetType() isProvision_Response_Type { @@ -1880,131 +1952,140 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x0a, 0x08, 0x45, 0x6e, 0x76, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x06, 0x0a, 0x04, 0x61, 0x75, 0x74, 0x68, 0x22, 0x7e, - 0x0a, 0x03, 0x41, 0x70, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, - 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, - 0x61, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x6c, - 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, 0x22, 0xad, - 0x02, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, - 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, - 0x79, 0x70, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x12, - 0x3a, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x68, - 0x69, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x68, 0x69, 0x64, 0x65, 0x12, - 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, - 0x63, 0x6f, 0x6e, 0x1a, 0x69, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, - 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, - 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, - 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, - 0x69, 0x74, 0x69, 0x76, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x73, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, 0x73, 0x4e, 0x75, 0x6c, 0x6c, 0x22, 0xfc, - 0x01, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x1a, 0x27, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, - 0x79, 0x1a, 0x55, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x49, 0x0a, - 0x11, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, - 0x61, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, - 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, - 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x1a, 0x73, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x39, 0x0a, 0x08, 0x63, 0x6f, - 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, - 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, 0x6d, - 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xae, 0x07, - 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0xd1, 0x02, 0x0a, 0x08, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x53, 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, - 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, - 0x77, 0x6e, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2c, 0x0a, - 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, - 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x65, - 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x1a, - 0xd9, 0x01, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, - 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, - 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x46, 0x0a, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, - 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0f, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, - 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, - 0x74, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x72, 0x79, 0x5f, 0x72, 0x75, 0x6e, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x06, 0x64, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x1a, 0x08, 0x0a, 0x06, 0x43, - 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x1a, 0x80, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x34, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x06, 0x0a, 0x04, 0x61, 0x75, 0x74, 0x68, 0x22, 0xba, + 0x01, 0x0a, 0x03, 0x41, 0x70, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, + 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, + 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, + 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, + 0x3a, 0x0a, 0x0b, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x0b, + 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x22, 0x59, 0x0a, 0x0b, 0x48, + 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, + 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x68, 0x72, 0x65, + 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x74, 0x68, 0x72, + 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x22, 0xad, 0x02, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, + 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x3a, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x69, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x04, 0x68, 0x69, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x1a, 0x69, 0x0a, 0x08, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, + 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x12, 0x17, 0x0a, + 0x07, 0x69, 0x73, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, + 0x69, 0x73, 0x4e, 0x75, 0x6c, 0x6c, 0x22, 0xfc, 0x01, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, + 0x1a, 0x27, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, + 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x1a, 0x55, 0x0a, 0x08, 0x43, 0x6f, 0x6d, + 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x49, 0x0a, 0x11, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, + 0x65, 0x72, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x48, 0x00, - 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x37, 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, - 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, - 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x48, 0x00, 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, - 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x1a, 0x6b, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, - 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x73, 0x1a, 0x77, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, - 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x3d, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, - 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, - 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x3f, - 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, - 0x41, 0x43, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x01, - 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, - 0x52, 0x4e, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, - 0x37, 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, - 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, - 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, - 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x02, 0x32, 0xa3, 0x01, 0x0a, 0x0b, 0x50, 0x72, 0x6f, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, - 0x65, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, - 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x50, 0x0a, 0x09, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x10, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, + 0x1a, 0x73, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x03, + 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, 0x03, 0x6c, + 0x6f, 0x67, 0x12, 0x39, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, + 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x06, 0x0a, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xae, 0x07, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x1a, 0xd1, 0x02, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x53, 0x0a, + 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x13, 0x77, + 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x77, 0x6f, 0x72, + 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, + 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, + 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2c, 0x0a, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, + 0x72, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, + 0x65, 0x72, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x1a, 0xd9, 0x01, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x72, + 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, + 0x46, 0x0a, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, + 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, + 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x2d, - 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x65, 0x72, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x72, + 0x79, 0x5f, 0x72, 0x75, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x64, 0x72, 0x79, + 0x52, 0x75, 0x6e, 0x1a, 0x08, 0x0a, 0x06, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x1a, 0x80, 0x01, + 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x05, 0x73, 0x74, 0x61, + 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x48, 0x00, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, + 0x37, 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x48, 0x00, + 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x1a, 0x6b, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, + 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x1a, 0x77, 0x0a, + 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, + 0x3d, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x06, + 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x3f, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, + 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, + 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, + 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, + 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, 0x37, 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, + 0x0a, 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f, + 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x02, + 0x32, 0xa3, 0x01, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x12, 0x42, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x30, 0x01, 0x12, 0x50, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x2d, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x73, 0x64, 0x6b, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2020,7 +2101,7 @@ func file_provisionersdk_proto_provisioner_proto_rawDescGZIP() []byte { } var file_provisionersdk_proto_provisioner_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 23) +var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 24) var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{ (LogLevel)(0), // 0: provisioner.LogLevel (WorkspaceTransition)(0), // 1: provisioner.WorkspaceTransition @@ -2036,20 +2117,21 @@ var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{ (*InstanceIdentityAuth)(nil), // 11: provisioner.InstanceIdentityAuth (*Agent)(nil), // 12: provisioner.Agent (*App)(nil), // 13: provisioner.App - (*Resource)(nil), // 14: provisioner.Resource - (*Parse)(nil), // 15: provisioner.Parse - (*Provision)(nil), // 16: provisioner.Provision - nil, // 17: provisioner.Agent.EnvEntry - (*Resource_Metadata)(nil), // 18: provisioner.Resource.Metadata - (*Parse_Request)(nil), // 19: provisioner.Parse.Request - (*Parse_Complete)(nil), // 20: provisioner.Parse.Complete - (*Parse_Response)(nil), // 21: provisioner.Parse.Response - (*Provision_Metadata)(nil), // 22: provisioner.Provision.Metadata - (*Provision_Start)(nil), // 23: provisioner.Provision.Start - (*Provision_Cancel)(nil), // 24: provisioner.Provision.Cancel - (*Provision_Request)(nil), // 25: provisioner.Provision.Request - (*Provision_Complete)(nil), // 26: provisioner.Provision.Complete - (*Provision_Response)(nil), // 27: provisioner.Provision.Response + (*Healthcheck)(nil), // 14: provisioner.Healthcheck + (*Resource)(nil), // 15: provisioner.Resource + (*Parse)(nil), // 16: provisioner.Parse + (*Provision)(nil), // 17: provisioner.Provision + nil, // 18: provisioner.Agent.EnvEntry + (*Resource_Metadata)(nil), // 19: provisioner.Resource.Metadata + (*Parse_Request)(nil), // 20: provisioner.Parse.Request + (*Parse_Complete)(nil), // 21: provisioner.Parse.Complete + (*Parse_Response)(nil), // 22: provisioner.Parse.Response + (*Provision_Metadata)(nil), // 23: provisioner.Provision.Metadata + (*Provision_Start)(nil), // 24: provisioner.Provision.Start + (*Provision_Cancel)(nil), // 25: provisioner.Provision.Cancel + (*Provision_Request)(nil), // 26: provisioner.Provision.Request + (*Provision_Complete)(nil), // 27: provisioner.Provision.Complete + (*Provision_Response)(nil), // 28: provisioner.Provision.Response } var file_provisionersdk_proto_provisioner_proto_depIdxs = []int32{ 2, // 0: provisioner.ParameterSource.scheme:type_name -> provisioner.ParameterSource.Scheme @@ -2059,30 +2141,31 @@ var file_provisionersdk_proto_provisioner_proto_depIdxs = []int32{ 7, // 4: provisioner.ParameterSchema.default_destination:type_name -> provisioner.ParameterDestination 4, // 5: provisioner.ParameterSchema.validation_type_system:type_name -> provisioner.ParameterSchema.TypeSystem 0, // 6: provisioner.Log.level:type_name -> provisioner.LogLevel - 17, // 7: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry + 18, // 7: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry 13, // 8: provisioner.Agent.apps:type_name -> provisioner.App - 12, // 9: provisioner.Resource.agents:type_name -> provisioner.Agent - 18, // 10: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata - 9, // 11: provisioner.Parse.Complete.parameter_schemas:type_name -> provisioner.ParameterSchema - 10, // 12: provisioner.Parse.Response.log:type_name -> provisioner.Log - 20, // 13: provisioner.Parse.Response.complete:type_name -> provisioner.Parse.Complete - 1, // 14: provisioner.Provision.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition - 8, // 15: provisioner.Provision.Start.parameter_values:type_name -> provisioner.ParameterValue - 22, // 16: provisioner.Provision.Start.metadata:type_name -> provisioner.Provision.Metadata - 23, // 17: provisioner.Provision.Request.start:type_name -> provisioner.Provision.Start - 24, // 18: provisioner.Provision.Request.cancel:type_name -> provisioner.Provision.Cancel - 14, // 19: provisioner.Provision.Complete.resources:type_name -> provisioner.Resource - 10, // 20: provisioner.Provision.Response.log:type_name -> provisioner.Log - 26, // 21: provisioner.Provision.Response.complete:type_name -> provisioner.Provision.Complete - 19, // 22: provisioner.Provisioner.Parse:input_type -> provisioner.Parse.Request - 25, // 23: provisioner.Provisioner.Provision:input_type -> provisioner.Provision.Request - 21, // 24: provisioner.Provisioner.Parse:output_type -> provisioner.Parse.Response - 27, // 25: provisioner.Provisioner.Provision:output_type -> provisioner.Provision.Response - 24, // [24:26] is the sub-list for method output_type - 22, // [22:24] is the sub-list for method input_type - 22, // [22:22] is the sub-list for extension type_name - 22, // [22:22] is the sub-list for extension extendee - 0, // [0:22] is the sub-list for field type_name + 14, // 9: provisioner.App.healthcheck:type_name -> provisioner.Healthcheck + 12, // 10: provisioner.Resource.agents:type_name -> provisioner.Agent + 19, // 11: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata + 9, // 12: provisioner.Parse.Complete.parameter_schemas:type_name -> provisioner.ParameterSchema + 10, // 13: provisioner.Parse.Response.log:type_name -> provisioner.Log + 21, // 14: provisioner.Parse.Response.complete:type_name -> provisioner.Parse.Complete + 1, // 15: provisioner.Provision.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition + 8, // 16: provisioner.Provision.Start.parameter_values:type_name -> provisioner.ParameterValue + 23, // 17: provisioner.Provision.Start.metadata:type_name -> provisioner.Provision.Metadata + 24, // 18: provisioner.Provision.Request.start:type_name -> provisioner.Provision.Start + 25, // 19: provisioner.Provision.Request.cancel:type_name -> provisioner.Provision.Cancel + 15, // 20: provisioner.Provision.Complete.resources:type_name -> provisioner.Resource + 10, // 21: provisioner.Provision.Response.log:type_name -> provisioner.Log + 27, // 22: provisioner.Provision.Response.complete:type_name -> provisioner.Provision.Complete + 20, // 23: provisioner.Provisioner.Parse:input_type -> provisioner.Parse.Request + 26, // 24: provisioner.Provisioner.Provision:input_type -> provisioner.Provision.Request + 22, // 25: provisioner.Provisioner.Parse:output_type -> provisioner.Parse.Response + 28, // 26: provisioner.Provisioner.Provision:output_type -> provisioner.Provision.Response + 25, // [25:27] is the sub-list for method output_type + 23, // [23:25] is the sub-list for method input_type + 23, // [23:23] is the sub-list for extension type_name + 23, // [23:23] is the sub-list for extension extendee + 0, // [0:23] is the sub-list for field type_name } func init() { file_provisionersdk_proto_provisioner_proto_init() } @@ -2200,7 +2283,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Resource); i { + switch v := v.(*Healthcheck); i { case 0: return &v.state case 1: @@ -2212,7 +2295,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Parse); i { + switch v := v.(*Resource); i { case 0: return &v.state case 1: @@ -2224,6 +2307,18 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Parse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_provisionersdk_proto_provisioner_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Provision); i { case 0: return &v.state @@ -2235,7 +2330,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Resource_Metadata); i { case 0: return &v.state @@ -2247,7 +2342,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Parse_Request); i { case 0: return &v.state @@ -2259,7 +2354,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Parse_Complete); i { case 0: return &v.state @@ -2271,7 +2366,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Parse_Response); i { case 0: return &v.state @@ -2283,7 +2378,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Provision_Metadata); i { case 0: return &v.state @@ -2295,7 +2390,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Provision_Start); i { case 0: return &v.state @@ -2307,7 +2402,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Provision_Cancel); i { case 0: return &v.state @@ -2319,7 +2414,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Provision_Request); i { case 0: return &v.state @@ -2331,7 +2426,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Provision_Complete); i { case 0: return &v.state @@ -2343,7 +2438,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Provision_Response); i { case 0: return &v.state @@ -2360,15 +2455,15 @@ func file_provisionersdk_proto_provisioner_proto_init() { (*Agent_Token)(nil), (*Agent_InstanceId)(nil), } - file_provisionersdk_proto_provisioner_proto_msgTypes[16].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[17].OneofWrappers = []interface{}{ (*Parse_Response_Log)(nil), (*Parse_Response_Complete)(nil), } - file_provisionersdk_proto_provisioner_proto_msgTypes[20].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[21].OneofWrappers = []interface{}{ (*Provision_Request_Start)(nil), (*Provision_Request_Cancel)(nil), } - file_provisionersdk_proto_provisioner_proto_msgTypes[22].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[23].OneofWrappers = []interface{}{ (*Provision_Response_Log)(nil), (*Provision_Response_Complete)(nil), } @@ -2378,7 +2473,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_provisionersdk_proto_provisioner_proto_rawDesc, NumEnums: 5, - NumMessages: 23, + NumMessages: 24, NumExtensions: 0, NumServices: 1, }, diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index 57931f4524069..af30e32f10524 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -94,6 +94,14 @@ message App { string url = 3; string icon = 4; bool relative_path = 5; + Healthcheck healthcheck = 6; +} + +// Healthcheck represents configuration for checking for app readiness. +message Healthcheck { + string url = 1; + int32 interval = 2; + int32 threshold = 3; } // Resource represents created infrastructure. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 192b33166ca3b..c541b830dcbbc 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -12,12 +12,6 @@ export interface APIKey { readonly lifetime_seconds: number } -// From codersdk/workspaceagents.go -export interface AWSInstanceIdentityToken { - readonly signature: string - readonly document: string -} - // From codersdk/licenses.go export interface AddLicenseRequest { readonly license: string @@ -250,9 +244,11 @@ export interface GitSSHKey { readonly public_key: string } -// From codersdk/workspaceagents.go -export interface GoogleInstanceIdentityToken { - readonly json_web_token: string +// From codersdk/workspaceapps.go +export interface Healthcheck { + readonly url: string + readonly interval: number + readonly threshold: number } // From codersdk/licenses.go @@ -331,11 +327,6 @@ export interface ParameterSchema { readonly validation_contains?: string[] } -// From codersdk/workspaceagents.go -export interface PostWorkspaceAgentVersionRequest { - readonly version: string -} - // From codersdk/provisionerdaemons.go export interface ProvisionerDaemon { readonly id: string @@ -576,18 +567,6 @@ export interface WorkspaceAgent { readonly latency?: Record } -// From codersdk/workspaceagents.go -export interface WorkspaceAgentAuthenticateResponse { - readonly session_token: string -} - -// From codersdk/workspaceagents.go -export interface WorkspaceAgentConnectionInfo { - // Named type "tailscale.com/tailcfg.DERPMap" unknown, using "any" - // eslint-disable-next-line @typescript-eslint/no-explicit-any - readonly derp_map?: any -} - // From codersdk/workspaceresources.go export interface WorkspaceAgentInstanceMetadata { readonly jail_orchestrator: string @@ -616,6 +595,8 @@ export interface WorkspaceApp { readonly name: string readonly command?: string readonly icon?: string + readonly healthcheck: Healthcheck + readonly health: WorkspaceAppHealth } // From codersdk/workspacebuilds.go @@ -738,5 +719,8 @@ export type UserStatus = "active" | "suspended" // From codersdk/workspaceresources.go export type WorkspaceAgentStatus = "connected" | "connecting" | "disconnected" +// From codersdk/workspaceapps.go +export type WorkspaceAppHealth = "disabled" | "healthy" | "initializing" | "unhealthy" + // From codersdk/workspacebuilds.go export type WorkspaceTransition = "delete" | "start" | "stop" diff --git a/site/src/components/AppLink/AppLink.stories.tsx b/site/src/components/AppLink/AppLink.stories.tsx index b008905ef6c9d..eb7fd8bbb7d14 100644 --- a/site/src/components/AppLink/AppLink.stories.tsx +++ b/site/src/components/AppLink/AppLink.stories.tsx @@ -15,6 +15,7 @@ WithIcon.args = { workspaceName: MockWorkspace.name, appName: "code-server", appIcon: "/icon/code.svg", + health: "healthy", } export const WithoutIcon = Template.bind({}) @@ -22,4 +23,29 @@ WithoutIcon.args = { userName: "developer", workspaceName: MockWorkspace.name, appName: "code-server", + health: "healthy", +} + +export const HealthDisabled = Template.bind({}) +HealthDisabled.args = { + userName: "developer", + workspaceName: MockWorkspace.name, + appName: "code-server", + health: "disabled", +} + +export const HealthInitializing = Template.bind({}) +HealthInitializing.args = { + userName: "developer", + workspaceName: MockWorkspace.name, + appName: "code-server", + health: "initializing", +} + +export const HealthUnhealthy = Template.bind({}) +HealthUnhealthy.args = { + userName: "developer", + workspaceName: MockWorkspace.name, + appName: "code-server", + health: "unhealthy", } diff --git a/site/src/components/AppLink/AppLink.tsx b/site/src/components/AppLink/AppLink.tsx index 7d4d901c4bd96..1d09404720f7e 100644 --- a/site/src/components/AppLink/AppLink.tsx +++ b/site/src/components/AppLink/AppLink.tsx @@ -1,7 +1,9 @@ import Button from "@material-ui/core/Button" +import CircularProgress from "@material-ui/core/CircularProgress" import Link from "@material-ui/core/Link" import { makeStyles } from "@material-ui/core/styles" import ComputerIcon from "@material-ui/icons/Computer" +import ErrorOutlineIcon from "@material-ui/icons/ErrorOutline" import { FC, PropsWithChildren } from "react" import * as TypesGen from "../../api/typesGenerated" import { generateRandomString } from "../../util/random" @@ -17,6 +19,7 @@ export interface AppLinkProps { appName: TypesGen.WorkspaceApp["name"] appIcon?: TypesGen.WorkspaceApp["icon"] appCommand?: TypesGen.WorkspaceApp["command"] + health: TypesGen.WorkspaceApp["health"] } export const AppLink: FC> = ({ @@ -26,6 +29,7 @@ export const AppLink: FC> = ({ appName, appIcon, appCommand, + health, }) => { const styles = useStyles() @@ -38,37 +42,57 @@ export const AppLink: FC> = ({ )}` } + let canClick = true + let icon = appIcon ? {`${appName} : + if (health === "initializing") { + canClick = false + icon = + } + if (health === "unhealthy") { + canClick = false + icon = + } + return ( { - event.preventDefault() - window.open( - href, - Language.appTitle(appName, generateRandomString(12)), - "width=900,height=600", - ) - }} + className={canClick ? styles.link : styles.disabledLink} + onClick={ + canClick + ? (event) => { + event.preventDefault() + window.open( + href, + Language.appTitle(appName, generateRandomString(12)), + "width=900,height=600", + ) + } + : undefined + } > - ) } -const useStyles = makeStyles(() => ({ +const useStyles = makeStyles((theme) => ({ link: { textDecoration: "none !important", }, + disabledLink: { + pointerEvents: "none", + textDecoration: "none !important", + }, + button: { whiteSpace: "nowrap", }, + + unhealthyIcon: { + color: theme.palette.warning.light, + }, })) diff --git a/site/src/components/Resources/Resources.tsx b/site/src/components/Resources/Resources.tsx index d935f2096d1bc..402b5a202144f 100644 --- a/site/src/components/Resources/Resources.tsx +++ b/site/src/components/Resources/Resources.tsx @@ -171,6 +171,7 @@ export const Resources: FC> = ({ userName={workspace.owner_name} workspaceName={workspace.name} agentName={agent.name} + health={app.health} /> ))} diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 34b06a219656a..c5392745ed5f8 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -324,6 +324,12 @@ export const MockWorkspaceApp: TypesGen.WorkspaceApp = { id: "test-app", name: "test-app", icon: "", + health: "disabled", + healthcheck: { + url: "", + interval: 0, + threshold: 0, + }, } export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = {