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 = { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy