feat: add health check monitoring to workspace apps (#4114)

This commit is contained in:
Garrett Delfosse 2022-09-23 15:51:04 -04:00 committed by GitHub
parent f160830226
commit 4c8be34d81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 1592 additions and 509 deletions

View File

@ -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
@ -443,7 +436,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 {
@ -478,7 +471,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)
}
@ -634,7 +627,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
@ -775,7 +768,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) {

View File

@ -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
}

184
agent/apphealth.go Normal file
View File

@ -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
}

177
agent/apphealth_test.go Normal file
View File

@ -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()
}
}
}

View File

@ -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)

View File

@ -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()

View File

@ -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 (

View File

@ -414,8 +414,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)

View File

@ -57,10 +57,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!

View File

@ -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()

View File

@ -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',
@ -344,7 +351,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 (

View File

@ -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;

View File

@ -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';

View File

@ -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 (
@ -576,14 +597,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 {

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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)

View File

@ -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
}
@ -442,6 +455,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
@ -667,6 +686,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 {

View File

@ -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)
}

View File

@ -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) {

View File

@ -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,
}

View File

@ -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,
}
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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
}

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.4.5"
version = "0.4.15"
}
docker = {
source = "kreuzwerker/docker"

View File

@ -6,7 +6,7 @@ terraform {
}
coder = {
source = "coder/coder"
version = "0.4.11"
version = "0.4.15"
}
}
}

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.4.11"
version = "0.4.15"
}
}
}

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.4.11"
version = "0.4.15"
}
}
}

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.4.11"
version = "0.4.15"
}
azurerm = {
source = "hashicorp/azurerm"

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.4.11"
version = "0.4.15"
}
digitalocean = {
source = "digitalocean/digitalocean"

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.4.11"
version = "0.4.15"
}
docker = {
source = "kreuzwerker/docker"

View File

@ -3,7 +3,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.4.11"
version = "0.4.15"
}
docker = {
source = "kreuzwerker/docker"

View File

@ -9,7 +9,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.4.11"
version = "0.4.15"
}
docker = {
source = "kreuzwerker/docker"

View File

@ -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
}
}

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.4.11"
version = "0.4.15"
}
google = {
source = "hashicorp/google"

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.4.11"
version = "0.4.15"
}
google = {
source = "hashicorp/google"

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.4.11"
version = "0.4.15"
}
google = {
source = "hashicorp/google"

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.4.11"
version = "0.4.15"
}
kubernetes = {
source = "hashicorp/kubernetes"

View File

@ -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,
})
}
}

View File

@ -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{},
}},

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.4.11"
version = "0.4.15"
}
}
}

View File

@ -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": {},

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.4.11"
version = "0.4.15"
}
}
}

View File

@ -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": {},

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.4.11"
version = "0.4.15"
}
}
}

View File

@ -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": {},

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.4.11"
version = "0.4.15"
}
}
}

View File

@ -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": {},

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.4.11"
version = "0.4.15"
}
}
}

View File

@ -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": {},

View File

@ -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" {

View File

@ -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
},

View File

@ -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": {},

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.4.11"
version = "0.4.15"
}
}
}

View File

@ -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": {}

View File

@ -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,
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, 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,
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,
},

View File

@ -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.

View File

@ -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
@ -277,9 +271,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
@ -358,11 +354,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
@ -581,18 +572,6 @@ export interface WorkspaceAgent {
readonly latency?: Record<string, DERPRegion>
}
// 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
@ -621,6 +600,8 @@ export interface WorkspaceApp {
readonly name: string
readonly command?: string
readonly icon?: string
readonly healthcheck: Healthcheck
readonly health: WorkspaceAppHealth
}
// From codersdk/workspacebuilds.go
@ -743,5 +724,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"

View File

@ -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",
}

View File

@ -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<PropsWithChildren<AppLinkProps>> = ({
@ -26,6 +29,7 @@ export const AppLink: FC<PropsWithChildren<AppLinkProps>> = ({
appName,
appIcon,
appCommand,
health,
}) => {
const styles = useStyles()
@ -38,37 +42,57 @@ export const AppLink: FC<PropsWithChildren<AppLinkProps>> = ({
)}`
}
let canClick = true
let icon = appIcon ? <img alt={`${appName} Icon`} src={appIcon} /> : <ComputerIcon />
if (health === "initializing") {
canClick = false
icon = <CircularProgress size={16} />
}
if (health === "unhealthy") {
canClick = false
icon = <ErrorOutlineIcon className={styles.unhealthyIcon} />
}
return (
<Link
href={href}
target="_blank"
className={styles.link}
onClick={(event) => {
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
}
>
<Button
size="small"
startIcon={appIcon ? <img alt={`${appName} Icon`} src={appIcon} /> : <ComputerIcon />}
className={styles.button}
>
<Button size="small" startIcon={icon} className={styles.button} disabled={!canClick}>
{appName}
</Button>
</Link>
)
}
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,
},
}))

View File

@ -171,6 +171,7 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
userName={workspace.owner_name}
workspaceName={workspace.name}
agentName={agent.name}
health={app.health}
/>
))}
</>

View File

@ -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 = {