diff --git a/agent/agent.go b/agent/agent.go index f73d004ed65b7..356338b14bfa7 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -2,12 +2,14 @@ package agent import ( "bufio" + "bytes" "context" "crypto/rand" "crypto/rsa" "encoding/binary" "encoding/json" "errors" + "flag" "fmt" "io" "net" @@ -33,6 +35,7 @@ import ( "go.uber.org/atomic" gossh "golang.org/x/crypto/ssh" "golang.org/x/exp/slices" + "golang.org/x/sync/singleflight" "golang.org/x/xerrors" "tailscale.com/net/speedtest" "tailscale.com/tailcfg" @@ -83,12 +86,13 @@ type Options struct { } type Client interface { - Metadata(ctx context.Context) (agentsdk.Metadata, error) + Manifest(ctx context.Context) (agentsdk.Manifest, error) Listen(ctx context.Context) (net.Conn, error) ReportStats(ctx context.Context, log slog.Logger, statsChan <-chan *agentsdk.Stats, setInterval func(time.Duration)) (io.Closer, error) PostLifecycle(ctx context.Context, state agentsdk.PostLifecycleRequest) error PostAppHealth(ctx context.Context, req agentsdk.PostAppHealthsRequest) error PostStartup(ctx context.Context, req agentsdk.PostStartupRequest) error + PostMetadata(ctx context.Context, key string, req agentsdk.PostMetadataRequest) error PatchStartupLogs(ctx context.Context, req agentsdk.PatchStartupLogs) error } @@ -156,8 +160,8 @@ type agent struct { closed chan struct{} envVars map[string]string - // metadata is atomic because values can change after reconnection. - metadata atomic.Value + // manifest is atomic because values can change after reconnection. + manifest atomic.Pointer[agentsdk.Manifest] sessionToken atomic.Pointer[string] sshServer *ssh.Server sshMaxTimeout time.Duration @@ -183,6 +187,7 @@ type agent struct { // failure, you'll want the agent to reconnect. func (a *agent) runLoop(ctx context.Context) { go a.reportLifecycleLoop(ctx) + go a.reportMetadataLoop(ctx) for retrier := retry.New(100*time.Millisecond, 10*time.Second); retrier.Wait(ctx); { a.logger.Info(ctx, "connecting to coderd") @@ -205,6 +210,168 @@ func (a *agent) runLoop(ctx context.Context) { } } +func (a *agent) collectMetadata(ctx context.Context, md codersdk.WorkspaceAgentMetadataDescription) *codersdk.WorkspaceAgentMetadataResult { + var out bytes.Buffer + result := &codersdk.WorkspaceAgentMetadataResult{ + // CollectedAt is set here for testing purposes and overrode by + // the server to the time the server received the result to protect + // against clock skew. + // + // In the future, the server may accept the timestamp from the agent + // if it is certain the clocks are in sync. + CollectedAt: time.Now(), + } + cmd, err := a.createCommand(ctx, md.Script, nil) + if err != nil { + result.Error = err.Error() + return result + } + + cmd.Stdout = &out + cmd.Stderr = &out + + // The error isn't mutually exclusive with useful output. + err = cmd.Run() + + const bufLimit = 10 << 10 + if out.Len() > bufLimit { + err = errors.Join( + err, + xerrors.Errorf("output truncated from %v to %v bytes", out.Len(), bufLimit), + ) + out.Truncate(bufLimit) + } + + if err != nil { + result.Error = err.Error() + } + result.Value = out.String() + return result +} + +func adjustIntervalForTests(i int64) time.Duration { + // In tests we want to set shorter intervals because engineers are + // impatient. + base := time.Second + if flag.Lookup("test.v") != nil { + base = time.Millisecond * 100 + } + return time.Duration(i) * base +} + +type metadataResultAndKey struct { + result *codersdk.WorkspaceAgentMetadataResult + key string +} + +func (a *agent) reportMetadataLoop(ctx context.Context) { + baseInterval := adjustIntervalForTests(1) + + const metadataLimit = 128 + + var ( + baseTicker = time.NewTicker(baseInterval) + lastCollectedAts = make(map[string]time.Time) + metadataResults = make(chan metadataResultAndKey, metadataLimit) + ) + defer baseTicker.Stop() + + var flight singleflight.Group + + for { + select { + case <-ctx.Done(): + return + case mr := <-metadataResults: + lastCollectedAts[mr.key] = mr.result.CollectedAt + err := a.client.PostMetadata(ctx, mr.key, *mr.result) + if err != nil { + a.logger.Error(ctx, "report metadata", slog.Error(err)) + } + case <-baseTicker.C: + } + + if len(metadataResults) > 0 { + // The inner collection loop expects the channel is empty before spinning up + // all the collection goroutines. + a.logger.Debug( + ctx, "metadata collection backpressured", + slog.F("queue_len", len(metadataResults)), + ) + continue + } + + manifest := a.manifest.Load() + if manifest == nil { + continue + } + + if len(manifest.Metadata) > metadataLimit { + a.logger.Error( + ctx, "metadata limit exceeded", + slog.F("limit", metadataLimit), slog.F("got", len(manifest.Metadata)), + ) + continue + } + + // If the manifest changes (e.g. on agent reconnect) we need to + // purge old cache values to prevent lastCollectedAt from growing + // boundlessly. + for key := range lastCollectedAts { + if slices.IndexFunc(manifest.Metadata, func(md codersdk.WorkspaceAgentMetadataDescription) bool { + return md.Key == key + }) < 0 { + delete(lastCollectedAts, key) + } + } + + // Spawn a goroutine for each metadata collection, and use a + // channel to synchronize the results and avoid both messy + // mutex logic and overloading the API. + for _, md := range manifest.Metadata { + collectedAt, ok := lastCollectedAts[md.Key] + if ok { + // If the interval is zero, we assume the user just wants + // a single collection at startup, not a spinning loop. + if md.Interval == 0 { + continue + } + // The last collected value isn't quite stale yet, so we skip it. + if collectedAt.Add( + adjustIntervalForTests(md.Interval), + ).After(time.Now()) { + continue + } + } + + md := md + // We send the result to the channel in the goroutine to avoid + // sending the same result multiple times. So, we don't care about + // the return values. + flight.DoChan(md.Key, func() (interface{}, error) { + timeout := md.Timeout + if timeout == 0 { + timeout = md.Interval + } + ctx, cancel := context.WithTimeout(ctx, + time.Duration(timeout)*time.Second, + ) + defer cancel() + + select { + case <-ctx.Done(): + return 0, nil + case metadataResults <- metadataResultAndKey{ + key: md.Key, + result: a.collectMetadata(ctx, md), + }: + } + return 0, nil + }) + } + } +} + // reportLifecycleLoop reports the current lifecycle state once. // Only the latest state is reported, intermediate states may be // lost if the agent can't communicate with the API. @@ -279,40 +446,40 @@ func (a *agent) run(ctx context.Context) error { } a.sessionToken.Store(&sessionToken) - metadata, err := a.client.Metadata(ctx) + manifest, err := a.client.Manifest(ctx) if err != nil { return xerrors.Errorf("fetch metadata: %w", err) } - a.logger.Info(ctx, "fetched metadata", slog.F("metadata", metadata)) + a.logger.Info(ctx, "fetched manifest", slog.F("manifest", manifest)) // Expand the directory and send it back to coderd so external // applications that rely on the directory can use it. // // An example is VS Code Remote, which must know the directory // before initializing a connection. - metadata.Directory, err = expandDirectory(metadata.Directory) + manifest.Directory, err = expandDirectory(manifest.Directory) if err != nil { return xerrors.Errorf("expand directory: %w", err) } err = a.client.PostStartup(ctx, agentsdk.PostStartupRequest{ Version: buildinfo.Version(), - ExpandedDirectory: metadata.Directory, + ExpandedDirectory: manifest.Directory, }) if err != nil { return xerrors.Errorf("update workspace agent version: %w", err) } - oldMetadata := a.metadata.Swap(metadata) + oldManifest := a.manifest.Swap(&manifest) // The startup script should only execute on the first run! - if oldMetadata == nil { + if oldManifest == nil { a.setLifecycle(ctx, codersdk.WorkspaceAgentLifecycleStarting) // Perform overrides early so that Git auth can work even if users // connect to a workspace that is not yet ready. We don't run this // concurrently with the startup script to avoid conflicts between // them. - if metadata.GitAuthConfigs > 0 { + if manifest.GitAuthConfigs > 0 { // If this fails, we should consider surfacing the error in the // startup log and setting the lifecycle state to be "start_error" // (after startup script completion), but for now we'll just log it. @@ -327,7 +494,7 @@ func (a *agent) run(ctx context.Context) error { scriptStart := time.Now() err = a.trackConnGoroutine(func() { defer close(scriptDone) - scriptDone <- a.runStartupScript(ctx, metadata.StartupScript) + scriptDone <- a.runStartupScript(ctx, manifest.StartupScript) }) if err != nil { return xerrors.Errorf("track startup script: %w", err) @@ -336,8 +503,8 @@ func (a *agent) run(ctx context.Context) error { var timeout <-chan time.Time // If timeout is zero, an older version of the coder // provider was used. Otherwise a timeout is always > 0. - if metadata.StartupScriptTimeout > 0 { - t := time.NewTimer(metadata.StartupScriptTimeout) + if manifest.StartupScriptTimeout > 0 { + t := time.NewTimer(manifest.StartupScriptTimeout) defer t.Stop() timeout = t.C } @@ -354,7 +521,7 @@ func (a *agent) run(ctx context.Context) error { return } // Only log if there was a startup script. - if metadata.StartupScript != "" { + if manifest.StartupScript != "" { execTime := time.Since(scriptStart) if err != nil { a.logger.Warn(ctx, "startup script failed", slog.F("execution_time", execTime), slog.Error(err)) @@ -371,13 +538,13 @@ func (a *agent) run(ctx context.Context) error { appReporterCtx, appReporterCtxCancel := context.WithCancel(ctx) defer appReporterCtxCancel() go NewWorkspaceAppHealthReporter( - a.logger, metadata.Apps, a.client.PostAppHealth)(appReporterCtx) + a.logger, manifest.Apps, a.client.PostAppHealth)(appReporterCtx) a.closeMutex.Lock() network := a.network a.closeMutex.Unlock() if network == nil { - network, err = a.createTailnet(ctx, metadata.DERPMap) + network, err = a.createTailnet(ctx, manifest.DERPMap) if err != nil { return xerrors.Errorf("create tailnet: %w", err) } @@ -396,7 +563,7 @@ func (a *agent) run(ctx context.Context) error { a.startReportingConnectionStats(ctx) } else { // Update the DERP map! - network.SetDERPMap(metadata.DERPMap) + network.SetDERPMap(manifest.DERPMap) } a.logger.Debug(ctx, "running tailnet connection coordinator") @@ -926,9 +1093,9 @@ func (a *agent) init(ctx context.Context) { } // createCommand processes raw command input with OpenSSH-like behavior. -// If the rawCommand provided is empty, it will default to the users shell. +// If the script provided is empty, it will default to the users shell. // This injects environment variables specified by the user at launch too. -func (a *agent) createCommand(ctx context.Context, rawCommand string, env []string) (*exec.Cmd, error) { +func (a *agent) createCommand(ctx context.Context, script string, env []string) (*exec.Cmd, error) { currentUser, err := user.Current() if err != nil { return nil, xerrors.Errorf("get current user: %w", err) @@ -940,14 +1107,10 @@ func (a *agent) createCommand(ctx context.Context, rawCommand string, env []stri return nil, xerrors.Errorf("get user shell: %w", err) } - rawMetadata := a.metadata.Load() - if rawMetadata == nil { + manifest := a.manifest.Load() + if manifest == nil { return nil, xerrors.Errorf("no metadata was provided") } - metadata, valid := rawMetadata.(agentsdk.Metadata) - if !valid { - return nil, xerrors.Errorf("metadata is the wrong type: %T", metadata) - } // OpenSSH executes all commands with the users current shell. // We replicate that behavior for IDE support. @@ -955,11 +1118,11 @@ func (a *agent) createCommand(ctx context.Context, rawCommand string, env []stri if runtime.GOOS == "windows" { caller = "/c" } - args := []string{caller, rawCommand} + args := []string{caller, script} // gliderlabs/ssh returns a command slice of zero // when a shell is requested. - if len(rawCommand) == 0 { + if len(script) == 0 { args = []string{} if runtime.GOOS != "windows" { // On Linux and macOS, we should start a login @@ -969,7 +1132,7 @@ func (a *agent) createCommand(ctx context.Context, rawCommand string, env []stri } cmd := exec.CommandContext(ctx, shell, args...) - cmd.Dir = metadata.Directory + cmd.Dir = manifest.Directory // If the metadata directory doesn't exist, we run the command // in the users home directory. @@ -1010,14 +1173,14 @@ func (a *agent) createCommand(ctx context.Context, rawCommand string, env []stri // This adds the ports dialog to code-server that enables // proxying a port dynamically. - cmd.Env = append(cmd.Env, fmt.Sprintf("VSCODE_PROXY_URI=%s", metadata.VSCodePortProxyURI)) + cmd.Env = append(cmd.Env, fmt.Sprintf("VSCODE_PROXY_URI=%s", manifest.VSCodePortProxyURI)) // Hide Coder message on code-server's "Getting Started" page cmd.Env = append(cmd.Env, "CS_DISABLE_GETTING_STARTED_OVERRIDE=true") // Load environment variables passed via the agent. // These should override all variables we manually specify. - for envKey, value := range metadata.EnvironmentVariables { + for envKey, value := range manifest.EnvironmentVariables { // Expanding environment variables allows for customization // of the $PATH, among other variables. Customers can prepend // or append to the $PATH, so allowing expand is required! @@ -1080,9 +1243,9 @@ func (a *agent) handleSSHSession(session ssh.Session) (retErr error) { session.DisablePTYEmulation() if !isQuietLogin(session.RawCommand()) { - metadata, ok := a.metadata.Load().(agentsdk.Metadata) - if ok { - err = showMOTD(session, metadata.MOTDFile) + manifest := a.manifest.Load() + if manifest != nil { + err = showMOTD(session, manifest.MOTDFile) if err != nil { a.logger.Error(ctx, "show MOTD", slog.Error(err)) } @@ -1512,19 +1675,19 @@ func (a *agent) Close() error { a.setLifecycle(ctx, codersdk.WorkspaceAgentLifecycleShuttingDown) lifecycleState := codersdk.WorkspaceAgentLifecycleOff - if metadata, ok := a.metadata.Load().(agentsdk.Metadata); ok && metadata.ShutdownScript != "" { + if manifest := a.manifest.Load(); manifest != nil && manifest.ShutdownScript != "" { scriptDone := make(chan error, 1) scriptStart := time.Now() go func() { defer close(scriptDone) - scriptDone <- a.runShutdownScript(ctx, metadata.ShutdownScript) + scriptDone <- a.runShutdownScript(ctx, manifest.ShutdownScript) }() var timeout <-chan time.Time // If timeout is zero, an older version of the coder // provider was used. Otherwise a timeout is always > 0. - if metadata.ShutdownScriptTimeout > 0 { - t := time.NewTimer(metadata.ShutdownScriptTimeout) + if manifest.ShutdownScriptTimeout > 0 { + t := time.NewTimer(manifest.ShutdownScriptTimeout) defer t.Stop() timeout = t.C } diff --git a/agent/agent_test.go b/agent/agent_test.go index a147d27815f6a..ec76aa1b0b6b9 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -33,6 +33,7 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/goleak" "golang.org/x/crypto/ssh" + "golang.org/x/exp/maps" "golang.org/x/xerrors" "tailscale.com/net/speedtest" "tailscale.com/tailcfg" @@ -61,7 +62,7 @@ func TestAgent_Stats_SSH(t *testing.T) { defer cancel() //nolint:dogsled - conn, _, stats, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) + conn, _, stats, _, _ := setupAgent(t, agentsdk.Manifest{}, 0) sshClient, err := conn.SSHClient(ctx) require.NoError(t, err) @@ -94,7 +95,7 @@ func TestAgent_Stats_ReconnectingPTY(t *testing.T) { defer cancel() //nolint:dogsled - conn, _, stats, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) + conn, _, stats, _, _ := setupAgent(t, agentsdk.Manifest{}, 0) ptyConn, err := conn.ReconnectingPTY(ctx, uuid.New(), 128, 128, "/bin/bash") require.NoError(t, err) @@ -124,7 +125,7 @@ func TestAgent_Stats_Magic(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() //nolint:dogsled - conn, _, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) + conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0) sshClient, err := conn.SSHClient(ctx) require.NoError(t, err) defer sshClient.Close() @@ -151,7 +152,7 @@ func TestAgent_Stats_Magic(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() //nolint:dogsled - conn, _, stats, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) + conn, _, stats, _, _ := setupAgent(t, agentsdk.Manifest{}, 0) sshClient, err := conn.SSHClient(ctx) require.NoError(t, err) defer sshClient.Close() @@ -186,7 +187,7 @@ func TestAgent_Stats_Magic(t *testing.T) { func TestAgent_SessionExec(t *testing.T) { t.Parallel() - session := setupSSHSession(t, agentsdk.Metadata{}) + session := setupSSHSession(t, agentsdk.Manifest{}) command := "echo test" if runtime.GOOS == "windows" { @@ -199,7 +200,7 @@ func TestAgent_SessionExec(t *testing.T) { func TestAgent_GitSSH(t *testing.T) { t.Parallel() - session := setupSSHSession(t, agentsdk.Metadata{}) + session := setupSSHSession(t, agentsdk.Manifest{}) command := "sh -c 'echo $GIT_SSH_COMMAND'" if runtime.GOOS == "windows" { command = "cmd.exe /c echo %GIT_SSH_COMMAND%" @@ -219,7 +220,7 @@ func TestAgent_SessionTTYShell(t *testing.T) { // it seems like it could be either. t.Skip("ConPTY appears to be inconsistent on Windows.") } - session := setupSSHSession(t, agentsdk.Metadata{}) + session := setupSSHSession(t, agentsdk.Manifest{}) command := "sh" if runtime.GOOS == "windows" { command = "cmd.exe" @@ -242,7 +243,7 @@ func TestAgent_SessionTTYShell(t *testing.T) { func TestAgent_SessionTTYExitCode(t *testing.T) { t.Parallel() - session := setupSSHSession(t, agentsdk.Metadata{}) + session := setupSSHSession(t, agentsdk.Manifest{}) command := "areallynotrealcommand" err := session.RequestPty("xterm", 128, 128, ssh.TerminalModes{}) require.NoError(t, err) @@ -281,7 +282,7 @@ func TestAgent_Session_TTY_MOTD(t *testing.T) { // Set HOME so we can ensure no ~/.hushlogin is present. t.Setenv("HOME", tmpdir) - session := setupSSHSession(t, agentsdk.Metadata{ + session := setupSSHSession(t, agentsdk.Manifest{ MOTDFile: name, }) err = session.RequestPty("xterm", 128, 128, ssh.TerminalModes{}) @@ -327,7 +328,7 @@ func TestAgent_Session_TTY_Hushlogin(t *testing.T) { // Set HOME so we can ensure ~/.hushlogin is present. t.Setenv("HOME", tmpdir) - session := setupSSHSession(t, agentsdk.Metadata{ + session := setupSSHSession(t, agentsdk.Manifest{ MOTDFile: name, }) err = session.RequestPty("xterm", 128, 128, ssh.TerminalModes{}) @@ -357,7 +358,7 @@ func TestAgent_Session_TTY_FastCommandHasOutput(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() //nolint:dogsled - conn, _, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) + conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0) sshClient, err := conn.SSHClient(ctx) require.NoError(t, err) defer sshClient.Close() @@ -407,7 +408,7 @@ func TestAgent_Session_TTY_HugeOutputIsNotLost(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() //nolint:dogsled - conn, _, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) + conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0) sshClient, err := conn.SSHClient(ctx) require.NoError(t, err) defer sshClient.Close() @@ -706,7 +707,7 @@ func TestAgent_SFTP(t *testing.T) { home = "/" + strings.ReplaceAll(home, "\\", "/") } //nolint:dogsled - conn, _, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) + conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0) sshClient, err := conn.SSHClient(ctx) require.NoError(t, err) defer sshClient.Close() @@ -738,7 +739,7 @@ func TestAgent_SCP(t *testing.T) { defer cancel() //nolint:dogsled - conn, _, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) + conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0) sshClient, err := conn.SSHClient(ctx) require.NoError(t, err) defer sshClient.Close() @@ -757,7 +758,7 @@ func TestAgent_EnvironmentVariables(t *testing.T) { t.Parallel() key := "EXAMPLE" value := "value" - session := setupSSHSession(t, agentsdk.Metadata{ + session := setupSSHSession(t, agentsdk.Manifest{ EnvironmentVariables: map[string]string{ key: value, }, @@ -774,7 +775,7 @@ func TestAgent_EnvironmentVariables(t *testing.T) { func TestAgent_EnvironmentVariableExpansion(t *testing.T) { t.Parallel() key := "EXAMPLE" - session := setupSSHSession(t, agentsdk.Metadata{ + session := setupSSHSession(t, agentsdk.Manifest{ EnvironmentVariables: map[string]string{ key: "$SOMETHINGNOTSET", }, @@ -801,7 +802,7 @@ func TestAgent_CoderEnvVars(t *testing.T) { t.Run(key, func(t *testing.T) { t.Parallel() - session := setupSSHSession(t, agentsdk.Metadata{}) + session := setupSSHSession(t, agentsdk.Manifest{}) command := "sh -c 'echo $" + key + "'" if runtime.GOOS == "windows" { command = "cmd.exe /c echo %" + key + "%" @@ -824,7 +825,7 @@ func TestAgent_SSHConnectionEnvVars(t *testing.T) { t.Run(key, func(t *testing.T) { t.Parallel() - session := setupSSHSession(t, agentsdk.Metadata{}) + session := setupSSHSession(t, agentsdk.Manifest{}) command := "sh -c 'echo $" + key + "'" if runtime.GOOS == "windows" { command = "cmd.exe /c echo %" + key + "%" @@ -848,7 +849,7 @@ func TestAgent_StartupScript(t *testing.T) { client := &client{ t: t, agentID: uuid.New(), - metadata: agentsdk.Metadata{ + manifest: agentsdk.Manifest{ StartupScript: command, DERPMap: &tailcfg.DERPMap{}, }, @@ -879,7 +880,7 @@ func TestAgent_StartupScript(t *testing.T) { client := &client{ t: t, agentID: uuid.New(), - metadata: agentsdk.Metadata{ + manifest: agentsdk.Manifest{ StartupScript: command, DERPMap: &tailcfg.DERPMap{}, }, @@ -912,13 +913,125 @@ func TestAgent_StartupScript(t *testing.T) { }) } +func TestAgent_Metadata(t *testing.T) { + t.Parallel() + + t.Run("Once", func(t *testing.T) { + t.Parallel() + script := "echo -n hello" + if runtime.GOOS == "windows" { + script = "powershell " + script + } + //nolint:dogsled + _, client, _, _, _ := setupAgent(t, agentsdk.Manifest{ + Metadata: []codersdk.WorkspaceAgentMetadataDescription{ + { + Key: "greeting", + Interval: 0, + Script: script, + }, + }, + }, 0) + + var gotMd map[string]agentsdk.PostMetadataRequest + require.Eventually(t, func() bool { + gotMd = client.getMetadata() + return len(gotMd) == 1 + }, testutil.WaitShort, testutil.IntervalMedium) + + collectedAt := gotMd["greeting"].CollectedAt + + require.Never(t, func() bool { + gotMd = client.getMetadata() + if len(gotMd) != 1 { + panic("unexpected number of metadata") + } + return !gotMd["greeting"].CollectedAt.Equal(collectedAt) + }, testutil.WaitShort, testutil.IntervalMedium) + }) + + t.Run("Many", func(t *testing.T) { + if runtime.GOOS == "windows" { + // Shell scripting in Windows is a pain, and we have already tested + // that the OS logic works in the simpler "Once" test above. + t.Skip() + } + t.Parallel() + + dir := t.TempDir() + + const reportInterval = 2 + const intervalUnit = 100 * time.Millisecond + var ( + greetingPath = filepath.Join(dir, "greeting") + script = "echo hello | tee -a " + greetingPath + ) + _, client, _, _, _ := setupAgent(t, agentsdk.Manifest{ + Metadata: []codersdk.WorkspaceAgentMetadataDescription{ + { + Key: "greeting", + Interval: reportInterval, + Script: script, + }, + { + Key: "bad", + Interval: reportInterval, + Script: "exit 1", + }, + }, + }, 0) + + require.Eventually(t, func() bool { + return len(client.getMetadata()) == 2 + }, testutil.WaitShort, testutil.IntervalMedium) + + for start := time.Now(); time.Since(start) < testutil.WaitMedium; time.Sleep(testutil.IntervalMedium) { + md := client.getMetadata() + if len(md) != 2 { + panic("unexpected number of metadata entries") + } + + require.Equal(t, "hello\n", md["greeting"].Value) + require.Equal(t, "exit status 1", md["bad"].Error) + + greetingByt, err := os.ReadFile(greetingPath) + require.NoError(t, err) + + var ( + numGreetings = bytes.Count(greetingByt, []byte("hello")) + idealNumGreetings = time.Since(start) / (reportInterval * intervalUnit) + // We allow a 50% error margin because the report loop may backlog + // in CI and other toasters. In production, there is no hard + // guarantee on timing either, and the frontend gives similar + // wiggle room to the staleness of the value. + upperBound = int(idealNumGreetings) + 1 + lowerBound = (int(idealNumGreetings) / 2) + ) + + if idealNumGreetings < 50 { + // There is an insufficient sample size. + continue + } + + t.Logf("numGreetings: %d, idealNumGreetings: %d", numGreetings, idealNumGreetings) + // The report loop may slow down on load, but it should never, ever + // speed up. + if numGreetings > upperBound { + t.Fatalf("too many greetings: %d > %d in %v", numGreetings, upperBound, time.Since(start)) + } else if numGreetings < lowerBound { + t.Fatalf("too few greetings: %d < %d", numGreetings, lowerBound) + } + } + }) +} + func TestAgent_Lifecycle(t *testing.T) { t.Parallel() t.Run("StartTimeout", func(t *testing.T) { t.Parallel() - _, client, _, _, _ := setupAgent(t, agentsdk.Metadata{ + _, client, _, _, _ := setupAgent(t, agentsdk.Manifest{ StartupScript: "sleep 5", StartupScriptTimeout: time.Nanosecond, }, 0) @@ -947,7 +1060,7 @@ func TestAgent_Lifecycle(t *testing.T) { t.Run("StartError", func(t *testing.T) { t.Parallel() - _, client, _, _, _ := setupAgent(t, agentsdk.Metadata{ + _, client, _, _, _ := setupAgent(t, agentsdk.Manifest{ StartupScript: "false", StartupScriptTimeout: 30 * time.Second, }, 0) @@ -976,7 +1089,7 @@ func TestAgent_Lifecycle(t *testing.T) { t.Run("Ready", func(t *testing.T) { t.Parallel() - _, client, _, _, _ := setupAgent(t, agentsdk.Metadata{ + _, client, _, _, _ := setupAgent(t, agentsdk.Manifest{ StartupScript: "true", StartupScriptTimeout: 30 * time.Second, }, 0) @@ -1005,7 +1118,7 @@ func TestAgent_Lifecycle(t *testing.T) { t.Run("ShuttingDown", func(t *testing.T) { t.Parallel() - _, client, _, _, closer := setupAgent(t, agentsdk.Metadata{ + _, client, _, _, closer := setupAgent(t, agentsdk.Manifest{ ShutdownScript: "sleep 5", StartupScriptTimeout: 30 * time.Second, }, 0) @@ -1043,7 +1156,7 @@ func TestAgent_Lifecycle(t *testing.T) { t.Run("ShutdownTimeout", func(t *testing.T) { t.Parallel() - _, client, _, _, closer := setupAgent(t, agentsdk.Metadata{ + _, client, _, _, closer := setupAgent(t, agentsdk.Manifest{ ShutdownScript: "sleep 5", ShutdownScriptTimeout: time.Nanosecond, }, 0) @@ -1090,7 +1203,7 @@ func TestAgent_Lifecycle(t *testing.T) { t.Run("ShutdownError", func(t *testing.T) { t.Parallel() - _, client, _, _, closer := setupAgent(t, agentsdk.Metadata{ + _, client, _, _, closer := setupAgent(t, agentsdk.Manifest{ ShutdownScript: "false", ShutdownScriptTimeout: 30 * time.Second, }, 0) @@ -1141,7 +1254,7 @@ func TestAgent_Lifecycle(t *testing.T) { client := &client{ t: t, agentID: uuid.New(), - metadata: agentsdk.Metadata{ + manifest: agentsdk.Manifest{ DERPMap: tailnettest.RunDERPAndSTUN(t), StartupScript: "echo 1", ShutdownScript: "echo " + expected, @@ -1194,7 +1307,7 @@ func TestAgent_Startup(t *testing.T) { t.Run("EmptyDirectory", func(t *testing.T) { t.Parallel() - _, client, _, _, _ := setupAgent(t, agentsdk.Metadata{ + _, client, _, _, _ := setupAgent(t, agentsdk.Manifest{ StartupScript: "true", StartupScriptTimeout: 30 * time.Second, Directory: "", @@ -1208,7 +1321,7 @@ func TestAgent_Startup(t *testing.T) { t.Run("HomeDirectory", func(t *testing.T) { t.Parallel() - _, client, _, _, _ := setupAgent(t, agentsdk.Metadata{ + _, client, _, _, _ := setupAgent(t, agentsdk.Manifest{ StartupScript: "true", StartupScriptTimeout: 30 * time.Second, Directory: "~", @@ -1224,7 +1337,7 @@ func TestAgent_Startup(t *testing.T) { t.Run("HomeEnvironmentVariable", func(t *testing.T) { t.Parallel() - _, client, _, _, _ := setupAgent(t, agentsdk.Metadata{ + _, client, _, _, _ := setupAgent(t, agentsdk.Manifest{ StartupScript: "true", StartupScriptTimeout: 30 * time.Second, Directory: "$HOME", @@ -1251,7 +1364,7 @@ func TestAgent_ReconnectingPTY(t *testing.T) { defer cancel() //nolint:dogsled - conn, _, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) + conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0) id := uuid.New() netConn, err := conn.ReconnectingPTY(ctx, id, 100, 100, "/bin/bash") require.NoError(t, err) @@ -1353,7 +1466,7 @@ func TestAgent_Dial(t *testing.T) { }() //nolint:dogsled - conn, _, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) + conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0) require.True(t, conn.AwaitReachable(context.Background())) conn1, err := conn.DialContext(context.Background(), l.Addr().Network(), l.Addr().String()) require.NoError(t, err) @@ -1375,7 +1488,7 @@ func TestAgent_Speedtest(t *testing.T) { defer cancel() derpMap := tailnettest.RunDERPAndSTUN(t) //nolint:dogsled - conn, _, _, _, _ := setupAgent(t, agentsdk.Metadata{ + conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{ DERPMap: derpMap, }, 0) defer conn.Close() @@ -1397,7 +1510,7 @@ func TestAgent_Reconnect(t *testing.T) { client := &client{ t: t, agentID: agentID, - metadata: agentsdk.Metadata{ + manifest: agentsdk.Manifest{ DERPMap: derpMap, }, statsChan: statsCh, @@ -1432,7 +1545,7 @@ func TestAgent_WriteVSCodeConfigs(t *testing.T) { client := &client{ t: t, agentID: uuid.New(), - metadata: agentsdk.Metadata{ + manifest: agentsdk.Manifest{ GitAuthConfigs: 1, DERPMap: &tailcfg.DERPMap{}, }, @@ -1461,7 +1574,7 @@ func TestAgent_WriteVSCodeConfigs(t *testing.T) { func setupSSHCommand(t *testing.T, beforeArgs []string, afterArgs []string) *exec.Cmd { //nolint:dogsled - agentConn, _, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) + agentConn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0) listener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) waitGroup := sync.WaitGroup{} @@ -1504,7 +1617,7 @@ func setupSSHCommand(t *testing.T, beforeArgs []string, afterArgs []string) *exe return exec.Command("ssh", args...) } -func setupSSHSession(t *testing.T, options agentsdk.Metadata) *ssh.Session { +func setupSSHSession(t *testing.T, options agentsdk.Manifest) *ssh.Session { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() //nolint:dogsled @@ -1528,7 +1641,7 @@ func (c closeFunc) Close() error { return c() } -func setupAgent(t *testing.T, metadata agentsdk.Metadata, ptyTimeout time.Duration) ( +func setupAgent(t *testing.T, metadata agentsdk.Manifest, ptyTimeout time.Duration) ( *codersdk.WorkspaceAgentConn, *client, <-chan *agentsdk.Stats, @@ -1548,7 +1661,7 @@ func setupAgent(t *testing.T, metadata agentsdk.Metadata, ptyTimeout time.Durati c := &client{ t: t, agentID: agentID, - metadata: metadata, + manifest: metadata, statsChan: statsCh, coordinator: coordinator, } @@ -1631,7 +1744,8 @@ func assertWritePayload(t *testing.T, w io.Writer, payload []byte) { type client struct { t *testing.T agentID uuid.UUID - metadata agentsdk.Metadata + manifest agentsdk.Manifest + metadata map[string]agentsdk.PostMetadataRequest statsChan chan *agentsdk.Stats coordinator tailnet.Coordinator lastWorkspaceAgent func() @@ -1643,8 +1757,8 @@ type client struct { logs []agentsdk.StartupLog } -func (c *client) Metadata(_ context.Context) (agentsdk.Metadata, error) { - return c.metadata, nil +func (c *client) Manifest(_ context.Context) (agentsdk.Manifest, error) { + return c.manifest, nil } func (c *client) Listen(_ context.Context) (net.Conn, error) { @@ -1718,6 +1832,22 @@ func (c *client) getStartup() agentsdk.PostStartupRequest { return c.startup } +func (c *client) getMetadata() map[string]agentsdk.PostMetadataRequest { + c.mu.Lock() + defer c.mu.Unlock() + return maps.Clone(c.metadata) +} + +func (c *client) PostMetadata(_ context.Context, key string, req agentsdk.PostMetadataRequest) error { + c.mu.Lock() + defer c.mu.Unlock() + if c.metadata == nil { + c.metadata = make(map[string]agentsdk.PostMetadataRequest) + } + c.metadata[key] = req + return nil +} + func (c *client) PostStartup(_ context.Context, startup agentsdk.PostStartupRequest) error { c.mu.Lock() defer c.mu.Unlock() diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 9561e0b511a6d..e18f37d5348d4 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -4263,7 +4263,7 @@ const docTemplate = `{ } } }, - "/workspaceagents/me/metadata": { + "/workspaceagents/me/manifest": { "get": { "security": [ { @@ -4276,18 +4276,62 @@ const docTemplate = `{ "tags": [ "Agents" ], - "summary": "Get authorized workspace agent metadata", - "operationId": "get-authorized-workspace-agent-metadata", + "summary": "Get authorized workspace agent manifest", + "operationId": "get-authorized-workspace-agent-manifest", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/agentsdk.Metadata" + "$ref": "#/definitions/agentsdk.Manifest" } } } } }, + "/workspaceagents/me/metadata/{key}": { + "post": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "tags": [ + "Agents" + ], + "summary": "Submit workspace agent metadata", + "operationId": "submit-workspace-agent-metadata", + "parameters": [ + { + "description": "Workspace agent metadata request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentsdk.PostMetadataRequest" + } + }, + { + "type": "string", + "format": "string", + "description": "metadata key", + "name": "key", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "Success" + } + }, + "x-apidocgen": { + "skip": true + } + } + }, "/workspaceagents/me/report-lifecycle": { "post": { "security": [ @@ -4663,6 +4707,38 @@ const docTemplate = `{ } } }, + "/workspaceagents/{workspaceagent}/watch-metadata": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": [ + "Agents" + ], + "summary": "Watch for workspace agent metadata updates", + "operationId": "watch-for-workspace-agent-metadata-updates", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Workspace agent ID", + "name": "workspaceagent", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success" + } + }, + "x-apidocgen": { + "skip": true + } + } + }, "/workspacebuilds/{workspacebuild}": { "get": { "security": [ @@ -5397,7 +5473,7 @@ const docTemplate = `{ } } }, - "agentsdk.Metadata": { + "agentsdk.Manifest": { "type": "object", "properties": { "apps": { @@ -5422,6 +5498,12 @@ const docTemplate = `{ "description": "GitAuthConfigs stores the number of Git configurations\nthe Coder deployment has. If this number is \u003e0, we\nset up special configuration in the workspace.", "type": "integer" }, + "metadata": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.WorkspaceAgentMetadataDescription" + } + }, "motd_file": { "type": "string" }, @@ -5473,6 +5555,25 @@ const docTemplate = `{ } } }, + "agentsdk.PostMetadataRequest": { + "type": "object", + "properties": { + "age": { + "description": "Age is the number of seconds since the metadata was collected.\nIt is provided in addition to CollectedAt to protect against clock skew.", + "type": "integer" + }, + "collected_at": { + "type": "string", + "format": "date-time" + }, + "error": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, "agentsdk.PostStartupRequest": { "type": "object", "properties": { @@ -8915,6 +9016,26 @@ const docTemplate = `{ } } }, + "codersdk.WorkspaceAgentMetadataDescription": { + "type": "object", + "properties": { + "display_name": { + "type": "string" + }, + "interval": { + "type": "integer" + }, + "key": { + "type": "string" + }, + "script": { + "type": "string" + }, + "timeout": { + "type": "integer" + } + } + }, "codersdk.WorkspaceAgentStartupLog": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index fd9c8a7e8a3ff..da7128998340f 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -3747,7 +3747,7 @@ } } }, - "/workspaceagents/me/metadata": { + "/workspaceagents/me/manifest": { "get": { "security": [ { @@ -3756,18 +3756,58 @@ ], "produces": ["application/json"], "tags": ["Agents"], - "summary": "Get authorized workspace agent metadata", - "operationId": "get-authorized-workspace-agent-metadata", + "summary": "Get authorized workspace agent manifest", + "operationId": "get-authorized-workspace-agent-manifest", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/agentsdk.Metadata" + "$ref": "#/definitions/agentsdk.Manifest" } } } } }, + "/workspaceagents/me/metadata/{key}": { + "post": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "tags": ["Agents"], + "summary": "Submit workspace agent metadata", + "operationId": "submit-workspace-agent-metadata", + "parameters": [ + { + "description": "Workspace agent metadata request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentsdk.PostMetadataRequest" + } + }, + { + "type": "string", + "format": "string", + "description": "metadata key", + "name": "key", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "Success" + } + }, + "x-apidocgen": { + "skip": true + } + } + }, "/workspaceagents/me/report-lifecycle": { "post": { "security": [ @@ -4101,6 +4141,36 @@ } } }, + "/workspaceagents/{workspaceagent}/watch-metadata": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": ["Agents"], + "summary": "Watch for workspace agent metadata updates", + "operationId": "watch-for-workspace-agent-metadata-updates", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Workspace agent ID", + "name": "workspaceagent", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success" + } + }, + "x-apidocgen": { + "skip": true + } + } + }, "/workspacebuilds/{workspacebuild}": { "get": { "security": [ @@ -4758,7 +4828,7 @@ } } }, - "agentsdk.Metadata": { + "agentsdk.Manifest": { "type": "object", "properties": { "apps": { @@ -4783,6 +4853,12 @@ "description": "GitAuthConfigs stores the number of Git configurations\nthe Coder deployment has. If this number is \u003e0, we\nset up special configuration in the workspace.", "type": "integer" }, + "metadata": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.WorkspaceAgentMetadataDescription" + } + }, "motd_file": { "type": "string" }, @@ -4834,6 +4910,25 @@ } } }, + "agentsdk.PostMetadataRequest": { + "type": "object", + "properties": { + "age": { + "description": "Age is the number of seconds since the metadata was collected.\nIt is provided in addition to CollectedAt to protect against clock skew.", + "type": "integer" + }, + "collected_at": { + "type": "string", + "format": "date-time" + }, + "error": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, "agentsdk.PostStartupRequest": { "type": "object", "properties": { @@ -8043,6 +8138,26 @@ } } }, + "codersdk.WorkspaceAgentMetadataDescription": { + "type": "object", + "properties": { + "display_name": { + "type": "string" + }, + "interval": { + "type": "integer" + }, + "key": { + "type": "string" + }, + "script": { + "type": "string" + }, + "timeout": { + "type": "integer" + } + } + }, "codersdk.WorkspaceAgentStartupLog": { "type": "object", "properties": { diff --git a/coderd/coderd.go b/coderd/coderd.go index 8fd68ba47e82a..9573d9b3cf889 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -608,7 +608,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("/metadata", api.workspaceAgentMetadata) + r.Get("/manifest", api.workspaceAgentManifest) + // This route is deprecated and will be removed in a future release. + // New agents will use /me/manifest instead. + r.Get("/metadata", api.workspaceAgentManifest) r.Post("/startup", api.postWorkspaceAgentStartup) r.Patch("/startup-logs", api.patchWorkspaceAgentStartupLogs) r.Post("/app-health", api.postWorkspaceAppHealth) @@ -617,6 +620,7 @@ func New(options *Options) *API { r.Get("/coordinate", api.workspaceAgentCoordinate) r.Post("/report-stats", api.workspaceAgentReportStats) r.Post("/report-lifecycle", api.workspaceAgentReportLifecycle) + r.Post("/metadata/{key}", api.workspaceAgentPostMetadata) }) // No middleware on the PTY endpoint since it uses workspace // application auth and tickets. @@ -628,6 +632,8 @@ func New(options *Options) *API { httpmw.ExtractWorkspaceParam(options.Database), ) r.Get("/", api.workspaceAgent) + r.Get("/watch-metadata", api.watchWorkspaceAgentMetadata) + r.Get("/pty", api.workspaceAgentPTY) r.Get("/startup-logs", api.workspaceAgentStartupLogs) r.Get("/listening-ports", api.workspaceAgentListeningPorts) r.Get("/connection", api.workspaceAgentConnection) diff --git a/coderd/coderdtest/swaggerparser.go b/coderd/coderdtest/swaggerparser.go index dda80d2f40800..be70b0379d6b0 100644 --- a/coderd/coderdtest/swaggerparser.go +++ b/coderd/coderdtest/swaggerparser.go @@ -160,6 +160,11 @@ func VerifySwaggerDefinitions(t *testing.T, router chi.Router, swaggerComments [ t.Run(method+" "+route, func(t *testing.T) { t.Parallel() + // This route is for compatibility purposes and is not documented. + if route == "/workspaceagents/me/metadata" { + return + } + c := findSwaggerCommentByMethodAndRoute(swaggerComments, method, route) assert.NotNil(t, c, "Missing @Router annotation") if c == nil { diff --git a/coderd/database/dbauthz/querier.go b/coderd/database/dbauthz/querier.go index a55ed6ba95d8f..13699e28f7cd8 100644 --- a/coderd/database/dbauthz/querier.go +++ b/coderd/database/dbauthz/querier.go @@ -1564,6 +1564,44 @@ func (q *querier) InsertWorkspaceAgentStat(ctx context.Context, arg database.Ins return q.db.InsertWorkspaceAgentStat(ctx, arg) } +func (q *querier) InsertWorkspaceAgentMetadata(ctx context.Context, arg database.InsertWorkspaceAgentMetadataParams) error { + // We don't check for workspace ownership here since the agent metadata may + // be associated with an orphaned agent used by a dry run build. + if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceSystem); err != nil { + return err + } + + return q.db.InsertWorkspaceAgentMetadata(ctx, arg) +} + +func (q *querier) UpdateWorkspaceAgentMetadata(ctx context.Context, arg database.UpdateWorkspaceAgentMetadataParams) error { + workspace, err := q.db.GetWorkspaceByAgentID(ctx, arg.WorkspaceAgentID) + if err != nil { + return err + } + + err = q.authorizeContext(ctx, rbac.ActionUpdate, workspace) + if err != nil { + return err + } + + return q.db.UpdateWorkspaceAgentMetadata(ctx, arg) +} + +func (q *querier) GetWorkspaceAgentMetadata(ctx context.Context, workspaceAgentID uuid.UUID) ([]database.WorkspaceAgentMetadatum, error) { + workspace, err := q.db.GetWorkspaceByAgentID(ctx, workspaceAgentID) + if err != nil { + return nil, err + } + + err = q.authorizeContext(ctx, rbac.ActionRead, workspace) + if err != nil { + return nil, err + } + + return q.db.GetWorkspaceAgentMetadata(ctx, workspaceAgentID) +} + func (q *querier) UpdateWorkspaceAppHealthByID(ctx context.Context, arg database.UpdateWorkspaceAppHealthByIDParams) error { // TODO: This is a workspace agent operation. Should users be able to query this? workspace, err := q.db.GetWorkspaceByWorkspaceAppID(ctx, arg.ID) diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go index 351707748b3c9..5822b15a727eb 100644 --- a/coderd/database/dbfake/databasefake.go +++ b/coderd/database/dbfake/databasefake.go @@ -124,6 +124,7 @@ type data struct { templateVersionVariables []database.TemplateVersionVariable templates []database.Template workspaceAgents []database.WorkspaceAgent + workspaceAgentMetadata []database.WorkspaceAgentMetadatum workspaceAgentLogs []database.WorkspaceAgentStartupLog workspaceApps []database.WorkspaceApp workspaceBuilds []database.WorkspaceBuild @@ -2741,6 +2742,60 @@ func (q *fakeQuerier) InsertAPIKey(_ context.Context, arg database.InsertAPIKeyP return key, nil } +func (q *fakeQuerier) UpdateWorkspaceAgentMetadata(_ context.Context, arg database.UpdateWorkspaceAgentMetadataParams) error { + q.mutex.Lock() + defer q.mutex.Unlock() + + //nolint:gosimple + updated := database.WorkspaceAgentMetadatum{ + WorkspaceAgentID: arg.WorkspaceAgentID, + Key: arg.Key, + Value: arg.Value, + Error: arg.Error, + CollectedAt: arg.CollectedAt, + } + + for i, m := range q.workspaceAgentMetadata { + if m.WorkspaceAgentID == arg.WorkspaceAgentID && m.Key == arg.Key { + q.workspaceAgentMetadata[i] = updated + return nil + } + } + + return nil +} + +func (q *fakeQuerier) InsertWorkspaceAgentMetadata(_ context.Context, arg database.InsertWorkspaceAgentMetadataParams) error { + q.mutex.Lock() + defer q.mutex.Unlock() + + //nolint:gosimple + metadatum := database.WorkspaceAgentMetadatum{ + WorkspaceAgentID: arg.WorkspaceAgentID, + Script: arg.Script, + DisplayName: arg.DisplayName, + Key: arg.Key, + Timeout: arg.Timeout, + Interval: arg.Interval, + } + + q.workspaceAgentMetadata = append(q.workspaceAgentMetadata, metadatum) + return nil +} + +func (q *fakeQuerier) GetWorkspaceAgentMetadata(_ context.Context, workspaceAgentID uuid.UUID) ([]database.WorkspaceAgentMetadatum, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + metadata := make([]database.WorkspaceAgentMetadatum, 0) + for _, m := range q.workspaceAgentMetadata { + if m.WorkspaceAgentID == workspaceAgentID { + metadata = append(metadata, m) + } + } + return metadata, nil +} + func (q *fakeQuerier) InsertFile(_ context.Context, arg database.InsertFileParams) (database.File, error) { if err := validateDatabaseType(arg); err != nil { return database.File{}, err diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 2f99cf2a073e3..ae2343f249156 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -475,6 +475,18 @@ CREATE TABLE users ( last_seen_at timestamp without time zone DEFAULT '0001-01-01 00:00:00'::timestamp without time zone NOT NULL ); +CREATE UNLOGGED TABLE workspace_agent_metadata ( + workspace_agent_id uuid NOT NULL, + display_name character varying(127) NOT NULL, + key character varying(127) NOT NULL, + script character varying(65535) NOT NULL, + value character varying(65535) DEFAULT ''::character varying NOT NULL, + error character varying(65535) DEFAULT ''::character varying NOT NULL, + timeout bigint NOT NULL, + "interval" bigint NOT NULL, + collected_at timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL +); + CREATE TABLE workspace_agent_startup_logs ( agent_id uuid NOT NULL, created_at timestamp with time zone NOT NULL, @@ -756,6 +768,9 @@ ALTER TABLE ONLY user_links ALTER TABLE ONLY users ADD CONSTRAINT users_pkey PRIMARY KEY (id); +ALTER TABLE ONLY workspace_agent_metadata + ADD CONSTRAINT workspace_agent_metadata_pkey PRIMARY KEY (workspace_agent_id, key); + ALTER TABLE ONLY workspace_agent_startup_logs ADD CONSTRAINT workspace_agent_startup_logs_pkey PRIMARY KEY (id); @@ -894,6 +909,9 @@ ALTER TABLE ONLY templates ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; +ALTER TABLE ONLY workspace_agent_metadata + ADD CONSTRAINT workspace_agent_metadata_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ALTER TABLE ONLY workspace_agent_startup_logs ADD CONSTRAINT workspace_agent_startup_logs_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; diff --git a/coderd/database/migrations/000111_workspace_agent_metadata.down.sql b/coderd/database/migrations/000111_workspace_agent_metadata.down.sql new file mode 100644 index 0000000000000..e59fb238447b6 --- /dev/null +++ b/coderd/database/migrations/000111_workspace_agent_metadata.down.sql @@ -0,0 +1 @@ +DROP TABLE workspace_agent_metadata; diff --git a/coderd/database/migrations/000111_workspace_agent_metadata.up.sql b/coderd/database/migrations/000111_workspace_agent_metadata.up.sql new file mode 100644 index 0000000000000..426b4d69a1d5e --- /dev/null +++ b/coderd/database/migrations/000111_workspace_agent_metadata.up.sql @@ -0,0 +1,16 @@ +-- This table is UNLOGGED because it is very update-heavy and the the data +-- is not valuable enough to justify the overhead of WAL logging. This should +-- give us a ~70% improvement in write throughput. +CREATE UNLOGGED TABLE workspace_agent_metadata ( + workspace_agent_id uuid NOT NULL, + display_name varchar(127) NOT NULL, + key varchar(127) NOT NULL, + script varchar(65535) NOT NULL, + value varchar(65535) NOT NULL DEFAULT '', + error varchar(65535) NOT NULL DEFAULT '', + timeout bigint NOT NULL, + interval bigint NOT NULL, + collected_at timestamp with time zone NOT NULL DEFAULT '0001-01-01 00:00:00+00', + PRIMARY KEY (workspace_agent_id, key), + FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE +); diff --git a/coderd/database/migrations/testdata/fixtures/000111_workspace_agent_metadata.up.sql b/coderd/database/migrations/testdata/fixtures/000111_workspace_agent_metadata.up.sql new file mode 100644 index 0000000000000..cfa7476742309 --- /dev/null +++ b/coderd/database/migrations/testdata/fixtures/000111_workspace_agent_metadata.up.sql @@ -0,0 +1,18 @@ +INSERT INTO + workspace_agent_metadata ( + workspace_agent_id, + display_name, + key, + script, + timeout, + interval + ) +VALUES + ( + '45e89705-e09d-4850-bcec-f9a937f5d78d', + 'a h e m', + 'ahem', + 'rm -rf', + 3, + 1 + ); diff --git a/coderd/database/models.go b/coderd/database/models.go index 0a929ae07005f..639de9475a5c7 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1575,6 +1575,18 @@ type WorkspaceAgent struct { StartupLogsOverflowed bool `db:"startup_logs_overflowed" json:"startup_logs_overflowed"` } +type WorkspaceAgentMetadatum struct { + WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` + DisplayName string `db:"display_name" json:"display_name"` + Key string `db:"key" json:"key"` + Script string `db:"script" json:"script"` + Value string `db:"value" json:"value"` + Error string `db:"error" json:"error"` + Timeout int64 `db:"timeout" json:"timeout"` + Interval int64 `db:"interval" json:"interval"` + CollectedAt time.Time `db:"collected_at" json:"collected_at"` +} + type WorkspaceAgentStartupLog struct { AgentID uuid.UUID `db:"agent_id" json:"agent_id"` CreatedAt time.Time `db:"created_at" json:"created_at"` diff --git a/coderd/database/querier.go b/coderd/database/querier.go index bdd370acca17f..cdf1f14fcd7bd 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -126,6 +126,7 @@ type sqlcQuerier interface { GetWorkspaceAgentByAuthToken(ctx context.Context, authToken uuid.UUID) (WorkspaceAgent, error) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error) GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (WorkspaceAgent, error) + GetWorkspaceAgentMetadata(ctx context.Context, workspaceAgentID uuid.UUID) ([]WorkspaceAgentMetadatum, error) GetWorkspaceAgentStartupLogsAfter(ctx context.Context, arg GetWorkspaceAgentStartupLogsAfterParams) ([]WorkspaceAgentStartupLog, error) GetWorkspaceAgentStats(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsRow, error) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error) @@ -185,6 +186,7 @@ type sqlcQuerier interface { InsertUserLink(ctx context.Context, arg InsertUserLinkParams) (UserLink, error) InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (Workspace, error) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error) + InsertWorkspaceAgentMetadata(ctx context.Context, arg InsertWorkspaceAgentMetadataParams) error InsertWorkspaceAgentStartupLogs(ctx context.Context, arg InsertWorkspaceAgentStartupLogsParams) ([]WorkspaceAgentStartupLog, error) InsertWorkspaceAgentStat(ctx context.Context, arg InsertWorkspaceAgentStatParams) (WorkspaceAgentStat, error) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspaceAppParams) (WorkspaceApp, error) @@ -229,6 +231,7 @@ type sqlcQuerier interface { UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (Workspace, error) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context, arg UpdateWorkspaceAgentLifecycleStateByIDParams) error + UpdateWorkspaceAgentMetadata(ctx context.Context, arg UpdateWorkspaceAgentMetadataParams) error UpdateWorkspaceAgentStartupByID(ctx context.Context, arg UpdateWorkspaceAgentStartupByIDParams) error UpdateWorkspaceAgentStartupLogOverflowByID(ctx context.Context, arg UpdateWorkspaceAgentStartupLogOverflowByIDParams) error UpdateWorkspaceAppHealthByID(ctx context.Context, arg UpdateWorkspaceAppHealthByIDParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 49e6a95489196..45dbbe75c584b 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5297,6 +5297,48 @@ func (q *sqlQuerier) GetWorkspaceAgentByInstanceID(ctx context.Context, authInst return i, err } +const getWorkspaceAgentMetadata = `-- name: GetWorkspaceAgentMetadata :many +SELECT + workspace_agent_id, display_name, key, script, value, error, timeout, interval, collected_at +FROM + workspace_agent_metadata +WHERE + workspace_agent_id = $1 +` + +func (q *sqlQuerier) GetWorkspaceAgentMetadata(ctx context.Context, workspaceAgentID uuid.UUID) ([]WorkspaceAgentMetadatum, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceAgentMetadata, workspaceAgentID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgentMetadatum + for rows.Next() { + var i WorkspaceAgentMetadatum + if err := rows.Scan( + &i.WorkspaceAgentID, + &i.DisplayName, + &i.Key, + &i.Script, + &i.Value, + &i.Error, + &i.Timeout, + &i.Interval, + &i.CollectedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getWorkspaceAgentStartupLogsAfter = `-- name: GetWorkspaceAgentStartupLogsAfter :many SELECT agent_id, created_at, output, id @@ -5651,6 +5693,41 @@ func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspa return i, err } +const insertWorkspaceAgentMetadata = `-- name: InsertWorkspaceAgentMetadata :exec +INSERT INTO + workspace_agent_metadata ( + workspace_agent_id, + display_name, + key, + script, + timeout, + interval + ) +VALUES + ($1, $2, $3, $4, $5, $6) +` + +type InsertWorkspaceAgentMetadataParams struct { + WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` + DisplayName string `db:"display_name" json:"display_name"` + Key string `db:"key" json:"key"` + Script string `db:"script" json:"script"` + Timeout int64 `db:"timeout" json:"timeout"` + Interval int64 `db:"interval" json:"interval"` +} + +func (q *sqlQuerier) InsertWorkspaceAgentMetadata(ctx context.Context, arg InsertWorkspaceAgentMetadataParams) error { + _, err := q.db.ExecContext(ctx, insertWorkspaceAgentMetadata, + arg.WorkspaceAgentID, + arg.DisplayName, + arg.Key, + arg.Script, + arg.Timeout, + arg.Interval, + ) + return err +} + const insertWorkspaceAgentStartupLogs = `-- name: InsertWorkspaceAgentStartupLogs :many WITH new_length AS ( UPDATE workspace_agents SET @@ -5758,6 +5835,37 @@ func (q *sqlQuerier) UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context, return err } +const updateWorkspaceAgentMetadata = `-- name: UpdateWorkspaceAgentMetadata :exec +UPDATE + workspace_agent_metadata +SET + value = $3, + error = $4, + collected_at = $5 +WHERE + workspace_agent_id = $1 + AND key = $2 +` + +type UpdateWorkspaceAgentMetadataParams struct { + WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` + Key string `db:"key" json:"key"` + Value string `db:"value" json:"value"` + Error string `db:"error" json:"error"` + CollectedAt time.Time `db:"collected_at" json:"collected_at"` +} + +func (q *sqlQuerier) UpdateWorkspaceAgentMetadata(ctx context.Context, arg UpdateWorkspaceAgentMetadataParams) error { + _, err := q.db.ExecContext(ctx, updateWorkspaceAgentMetadata, + arg.WorkspaceAgentID, + arg.Key, + arg.Value, + arg.Error, + arg.CollectedAt, + ) + return err +} + const updateWorkspaceAgentStartupByID = `-- name: UpdateWorkspaceAgentStartupByID :exec UPDATE workspace_agents diff --git a/coderd/database/queries/workspaceagents.sql b/coderd/database/queries/workspaceagents.sql index 143be63c07267..36c0ab31e13a0 100644 --- a/coderd/database/queries/workspaceagents.sql +++ b/coderd/database/queries/workspaceagents.sql @@ -94,6 +94,38 @@ SET WHERE id = $1; +-- name: InsertWorkspaceAgentMetadata :exec +INSERT INTO + workspace_agent_metadata ( + workspace_agent_id, + display_name, + key, + script, + timeout, + interval + ) +VALUES + ($1, $2, $3, $4, $5, $6); + +-- name: UpdateWorkspaceAgentMetadata :exec +UPDATE + workspace_agent_metadata +SET + value = $3, + error = $4, + collected_at = $5 +WHERE + workspace_agent_id = $1 + AND key = $2; + +-- name: GetWorkspaceAgentMetadata :many +SELECT + * +FROM + workspace_agent_metadata +WHERE + workspace_agent_id = $1; + -- name: UpdateWorkspaceAgentStartupLogOverflowByID :exec UPDATE workspace_agents diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 5f989f784d8e4..0450e7aba6621 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1277,6 +1277,21 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. } snapshot.WorkspaceAgents = append(snapshot.WorkspaceAgents, telemetry.ConvertWorkspaceAgent(dbAgent)) + for _, md := range prAgent.Metadata { + p := database.InsertWorkspaceAgentMetadataParams{ + WorkspaceAgentID: agentID, + DisplayName: md.DisplayName, + Script: md.Script, + Key: md.Key, + Timeout: md.Timeout, + Interval: md.Interval, + } + err := db.InsertWorkspaceAgentMetadata(ctx, p) + if err != nil { + return xerrors.Errorf("insert agent metadata: %w, params: %+v", err, p) + } + } + for _, app := range prAgent.Apps { slug := app.Slug if slug == "" { diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 74e2818a6bbe1..49885bb9e5a21 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -21,6 +21,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/google/uuid" "go.opentelemetry.io/otel/trace" + "golang.org/x/exp/slices" "golang.org/x/mod/semver" "golang.org/x/xerrors" "nhooyr.io/websocket" @@ -76,14 +77,14 @@ func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusOK, apiAgent) } -// @Summary Get authorized workspace agent metadata -// @ID get-authorized-workspace-agent-metadata +// @Summary Get authorized workspace agent manifest +// @ID get-authorized-workspace-agent-manifest // @Security CoderSessionToken // @Produce json // @Tags Agents -// @Success 200 {object} agentsdk.Metadata -// @Router /workspaceagents/me/metadata [get] -func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request) { +// @Success 200 {object} agentsdk.Manifest +// @Router /workspaceagents/me/manifest [get] +func (api *API) workspaceAgentManifest(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() workspaceAgent := httpmw.WorkspaceAgent(r) apiAgent, err := convertWorkspaceAgent( @@ -105,6 +106,16 @@ func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request) }) return } + + metadata, err := api.Database.GetWorkspaceAgentMetadata(ctx, workspaceAgent.ID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace agent metadata.", + Detail: err.Error(), + }) + return + } + resource, err := api.Database.GetWorkspaceResourceByID(ctx, workspaceAgent.ResourceID) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ @@ -149,7 +160,7 @@ func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request) vscodeProxyURI += fmt.Sprintf(":%s", api.AccessURL.Port()) } - httpapi.Write(ctx, rw, http.StatusOK, agentsdk.Metadata{ + httpapi.Write(ctx, rw, http.StatusOK, agentsdk.Manifest{ Apps: convertApps(dbApps), DERPMap: api.DERPMap, GitAuthConfigs: len(api.GitAuthConfigs), @@ -161,6 +172,7 @@ func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request) StartupScriptTimeout: time.Duration(apiAgent.StartupScriptTimeoutSeconds) * time.Second, ShutdownScript: apiAgent.ShutdownScript, ShutdownScriptTimeout: time.Duration(apiAgent.ShutdownScriptTimeoutSeconds) * time.Second, + Metadata: convertWorkspaceAgentMetadataDesc(metadata), }) } @@ -1133,6 +1145,20 @@ func convertApps(dbApps []database.WorkspaceApp) []codersdk.WorkspaceApp { return apps } +func convertWorkspaceAgentMetadataDesc(mds []database.WorkspaceAgentMetadatum) []codersdk.WorkspaceAgentMetadataDescription { + metadata := make([]codersdk.WorkspaceAgentMetadataDescription, 0) + for _, datum := range mds { + metadata = append(metadata, codersdk.WorkspaceAgentMetadataDescription{ + DisplayName: datum.DisplayName, + Key: datum.Key, + Script: datum.Script, + Interval: datum.Interval, + Timeout: datum.Timeout, + }) + } + return metadata +} + func convertWorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator tailnet.Coordinator, dbAgent database.WorkspaceAgent, apps []codersdk.WorkspaceApp, agentInactiveDisconnectTimeout time.Duration, agentFallbackTroubleshootingURL string) (codersdk.WorkspaceAgent, error) { var envs map[string]string if dbAgent.EnvironmentVariables.Valid { @@ -1298,6 +1324,219 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques }) } +// @Summary Submit workspace agent metadata +// @ID submit-workspace-agent-metadata +// @Security CoderSessionToken +// @Accept json +// @Tags Agents +// @Param request body agentsdk.PostMetadataRequest true "Workspace agent metadata request" +// @Param key path string true "metadata key" format(string) +// @Success 204 "Success" +// @Router /workspaceagents/me/metadata/{key} [post] +// @x-apidocgen {"skip": true} +func (api *API) workspaceAgentPostMetadata(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var req agentsdk.PostMetadataRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + workspaceAgent := httpmw.WorkspaceAgent(r) + + workspace, err := api.Database.GetWorkspaceByAgentID(ctx, workspaceAgent.ID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Failed to get workspace.", + Detail: err.Error(), + }) + return + } + + key := chi.URLParam(r, "key") + + const ( + maxValueLen = 32 << 10 + maxErrorLen = maxValueLen + ) + + metadataError := req.Error + + // We overwrite the error if the provided payload is too long. + if len(req.Value) > maxValueLen { + metadataError = fmt.Sprintf("value of %d bytes exceeded %d bytes", len(req.Value), maxValueLen) + req.Value = req.Value[:maxValueLen] + } + + if len(req.Error) > maxErrorLen { + metadataError = fmt.Sprintf("error of %d bytes exceeded %d bytes", len(req.Error), maxErrorLen) + req.Error = req.Error[:maxErrorLen] + } + + datum := database.UpdateWorkspaceAgentMetadataParams{ + WorkspaceAgentID: workspaceAgent.ID, + // We don't want a misconfigured agent to fill the database. + Key: key, + Value: req.Value, + Error: metadataError, + // We ignore the CollectedAt from the agent to avoid bugs caused by + // clock skew. + CollectedAt: time.Now(), + } + + err = api.Database.UpdateWorkspaceAgentMetadata(ctx, datum) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + api.Logger.Debug( + ctx, "accepted metadata report", + slog.F("agent", workspaceAgent.ID), + slog.F("workspace", workspace.ID), + slog.F("collected_at", datum.CollectedAt), + slog.F("key", datum.Key), + ) + + err = api.Pubsub.Publish(watchWorkspaceAgentMetadataChannel(workspaceAgent.ID), []byte(datum.Key)) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + httpapi.Write(ctx, rw, http.StatusNoContent, nil) +} + +// @Summary Watch for workspace agent metadata updates +// @ID watch-for-workspace-agent-metadata-updates +// @Security CoderSessionToken +// @Tags Agents +// @Success 200 "Success" +// @Param workspaceagent path string true "Workspace agent ID" format(uuid) +// @Router /workspaceagents/{workspaceagent}/watch-metadata [get] +// @x-apidocgen {"skip": true} +func (api *API) watchWorkspaceAgentMetadata(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + workspaceAgent = httpmw.WorkspaceAgentParam(r) + ) + + sendEvent, senderClosed, err := httpapi.ServerSentEventSender(rw, r) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error setting up server-sent events.", + Detail: err.Error(), + }) + return + } + // Prevent handler from returning until the sender is closed. + defer func() { + <-senderClosed + }() + + // We don't want this intentionally long request to skew our tracing + // reports. + ctx = trace.ContextWithSpan(ctx, tracing.NoopSpan) + + const refreshInterval = time.Second * 5 + refreshTicker := time.NewTicker(refreshInterval) + defer refreshTicker.Stop() + + var ( + lastDBMetaMu sync.Mutex + lastDBMeta []database.WorkspaceAgentMetadatum + ) + + sendMetadata := func(pull bool) { + lastDBMetaMu.Lock() + defer lastDBMetaMu.Unlock() + + var err error + if pull { + // We always use the original Request context because it contains + // the RBAC actor. + lastDBMeta, err = api.Database.GetWorkspaceAgentMetadata(ctx, workspaceAgent.ID) + if err != nil { + _ = sendEvent(ctx, codersdk.ServerSentEvent{ + Type: codersdk.ServerSentEventTypeError, + Data: codersdk.Response{ + Message: "Internal error getting metadata.", + Detail: err.Error(), + }, + }) + return + } + slices.SortFunc(lastDBMeta, func(i, j database.WorkspaceAgentMetadatum) bool { + return i.Key < j.Key + }) + + // Avoid sending refresh if the client is about to get a + // fresh update. + refreshTicker.Reset(refreshInterval) + } + + _ = sendEvent(ctx, codersdk.ServerSentEvent{ + Type: codersdk.ServerSentEventTypeData, + Data: convertWorkspaceAgentMetadata(lastDBMeta), + }) + } + + // Send initial metadata. + sendMetadata(true) + + // Send metadata on updates. + cancelSub, err := api.Pubsub.Subscribe(watchWorkspaceAgentMetadataChannel(workspaceAgent.ID), func(_ context.Context, _ []byte) { + sendMetadata(true) + }) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + defer cancelSub() + + for { + select { + case <-senderClosed: + return + case <-refreshTicker.C: + break + } + + // Avoid spamming the DB with reads we know there are no updates. We want + // to continue sending updates to the frontend so that "Result.Age" + // is always accurate. This way, the frontend doesn't need to + // sync its own clock with the backend. + sendMetadata(false) + } +} + +func convertWorkspaceAgentMetadata(db []database.WorkspaceAgentMetadatum) []codersdk.WorkspaceAgentMetadata { + // An empty array is easier for clients to handle than a null. + result := []codersdk.WorkspaceAgentMetadata{} + for _, datum := range db { + result = append(result, codersdk.WorkspaceAgentMetadata{ + Result: codersdk.WorkspaceAgentMetadataResult{ + Value: datum.Value, + Error: datum.Error, + CollectedAt: datum.CollectedAt, + Age: int64(time.Since(datum.CollectedAt).Seconds()), + }, + Description: codersdk.WorkspaceAgentMetadataDescription{ + DisplayName: datum.DisplayName, + Key: datum.Key, + Script: datum.Script, + Interval: datum.Interval, + Timeout: datum.Timeout, + }, + }) + } + return result +} + +func watchWorkspaceAgentMetadataChannel(id uuid.UUID) string { + return "workspace_agent_metadata:" + id.String() +} + // @Summary Submit workspace agent lifecycle state // @ID submit-workspace-agent-lifecycle-state // @Security CoderSessionToken diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index fe37b075427d8..1da40dc7372f9 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -831,10 +831,10 @@ func TestWorkspaceAgentAppHealth(t *testing.T) { agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) - metadata, err := agentClient.Metadata(ctx) + manifest, err := agentClient.Manifest(ctx) require.NoError(t, err) - require.EqualValues(t, codersdk.WorkspaceAppHealthDisabled, metadata.Apps[0].Health) - require.EqualValues(t, codersdk.WorkspaceAppHealthInitializing, metadata.Apps[1].Health) + require.EqualValues(t, codersdk.WorkspaceAppHealthDisabled, manifest.Apps[0].Health) + require.EqualValues(t, codersdk.WorkspaceAppHealthInitializing, manifest.Apps[1].Health) err = agentClient.PostAppHealth(ctx, agentsdk.PostAppHealthsRequest{}) require.Error(t, err) // empty @@ -843,37 +843,37 @@ func TestWorkspaceAgentAppHealth(t *testing.T) { // healthcheck disabled err = agentClient.PostAppHealth(ctx, agentsdk.PostAppHealthsRequest{ Healths: map[uuid.UUID]codersdk.WorkspaceAppHealth{ - metadata.Apps[0].ID: codersdk.WorkspaceAppHealthInitializing, + manifest.Apps[0].ID: codersdk.WorkspaceAppHealthInitializing, }, }) require.Error(t, err) // invalid value err = agentClient.PostAppHealth(ctx, agentsdk.PostAppHealthsRequest{ Healths: map[uuid.UUID]codersdk.WorkspaceAppHealth{ - metadata.Apps[1].ID: codersdk.WorkspaceAppHealth("bad-value"), + manifest.Apps[1].ID: codersdk.WorkspaceAppHealth("bad-value"), }, }) require.Error(t, err) // update to healthy err = agentClient.PostAppHealth(ctx, agentsdk.PostAppHealthsRequest{ Healths: map[uuid.UUID]codersdk.WorkspaceAppHealth{ - metadata.Apps[1].ID: codersdk.WorkspaceAppHealthHealthy, + manifest.Apps[1].ID: codersdk.WorkspaceAppHealthHealthy, }, }) require.NoError(t, err) - metadata, err = agentClient.Metadata(ctx) + manifest, err = agentClient.Manifest(ctx) require.NoError(t, err) - require.EqualValues(t, codersdk.WorkspaceAppHealthHealthy, metadata.Apps[1].Health) + require.EqualValues(t, codersdk.WorkspaceAppHealthHealthy, manifest.Apps[1].Health) // update to unhealthy err = agentClient.PostAppHealth(ctx, agentsdk.PostAppHealthsRequest{ Healths: map[uuid.UUID]codersdk.WorkspaceAppHealth{ - metadata.Apps[1].ID: codersdk.WorkspaceAppHealthUnhealthy, + manifest.Apps[1].ID: codersdk.WorkspaceAppHealthUnhealthy, }, }) require.NoError(t, err) - metadata, err = agentClient.Metadata(ctx) + manifest, err = agentClient.Manifest(ctx) require.NoError(t, err) - require.EqualValues(t, codersdk.WorkspaceAppHealthUnhealthy, metadata.Apps[1].Health) + require.EqualValues(t, codersdk.WorkspaceAppHealthUnhealthy, manifest.Apps[1].Health) } // nolint:bodyclose @@ -1262,3 +1262,155 @@ func TestWorkspaceAgent_LifecycleState(t *testing.T) { } }) } + +func TestWorkspaceAgent_Metadata(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }) + user := coderdtest.CreateFirstUser(t, client) + authToken := uuid.NewString() + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: echo.ProvisionComplete, + ProvisionApply: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Resources: []*proto.Resource{{ + Name: "example", + Type: "aws_instance", + Agents: []*proto.Agent{{ + Metadata: []*proto.Agent_Metadata{ + { + DisplayName: "First Meta", + Key: "foo1", + Script: "echo hi", + Interval: 10, + Timeout: 3, + }, + { + DisplayName: "Second Meta", + Key: "foo2", + Script: "echo howdy", + Interval: 10, + Timeout: 3, + }, + { + DisplayName: "TooLong", + Key: "foo3", + Script: "echo howdy", + Interval: 10, + Timeout: 3, + }, + }, + Id: uuid.NewString(), + Auth: &proto.Agent_Token{ + Token: authToken, + }, + }}, + }}, + }, + }, + }}, + }) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + + for _, res := range workspace.LatestBuild.Resources { + for _, a := range res.Agents { + require.Equal(t, codersdk.WorkspaceAgentLifecycleCreated, a.LifecycleState) + } + } + + agentClient := agentsdk.New(client.URL) + agentClient.SetSessionToken(authToken) + + ctx := testutil.Context(t, testutil.WaitMedium) + + manifest, err := agentClient.Manifest(ctx) + require.NoError(t, err) + + // Verify manifest API response. + require.Equal(t, "First Meta", manifest.Metadata[0].DisplayName) + require.Equal(t, "foo1", manifest.Metadata[0].Key) + require.Equal(t, "echo hi", manifest.Metadata[0].Script) + require.EqualValues(t, 10, manifest.Metadata[0].Interval) + require.EqualValues(t, 3, manifest.Metadata[0].Timeout) + + post := func(key string, mr codersdk.WorkspaceAgentMetadataResult) { + err := agentClient.PostMetadata(ctx, key, mr) + require.NoError(t, err, "post metadata", t) + } + + workspace, err = client.Workspace(ctx, workspace.ID) + require.NoError(t, err, "get workspace") + + agentID := workspace.LatestBuild.Resources[0].Agents[0].ID + + var update []codersdk.WorkspaceAgentMetadata + + check := func(want codersdk.WorkspaceAgentMetadataResult, got codersdk.WorkspaceAgentMetadata) { + require.WithinDuration(t, want.CollectedAt, got.Result.CollectedAt, time.Second) + require.WithinDuration( + t, time.Now(), got.Result.CollectedAt.Add(time.Duration(got.Result.Age)*time.Second), time.Millisecond*500, + ) + require.Equal(t, want.Value, got.Result.Value) + require.Equal(t, want.Error, got.Result.Error) + } + + wantMetadata1 := codersdk.WorkspaceAgentMetadataResult{ + CollectedAt: time.Now(), + Value: "bar", + } + + // Initial post must come before the Watch is established. + post("foo1", wantMetadata1) + + updates, errors := client.WatchWorkspaceAgentMetadata(ctx, agentID) + + recvUpdate := func() []codersdk.WorkspaceAgentMetadata { + select { + case err := <-errors: + t.Fatalf("error watching metadata: %v", err) + return nil + case update := <-updates: + return update + } + } + + update = recvUpdate() + require.Len(t, update, 3) + check(wantMetadata1, update[0]) + // The second metadata result is not yet posted. + require.Zero(t, update[1].Result.CollectedAt) + + wantMetadata2 := wantMetadata1 + post("foo2", wantMetadata2) + update = recvUpdate() + require.Len(t, update, 3) + check(wantMetadata1, update[0]) + check(wantMetadata2, update[1]) + + wantMetadata1.Error = "error" + post("foo1", wantMetadata1) + update = recvUpdate() + require.Len(t, update, 3) + check(wantMetadata1, update[0]) + + const maxValueLen = 32 << 10 + tooLongValueMetadata := wantMetadata1 + tooLongValueMetadata.Value = strings.Repeat("a", maxValueLen*2) + tooLongValueMetadata.Error = "" + tooLongValueMetadata.CollectedAt = time.Now() + post("foo3", tooLongValueMetadata) + got := recvUpdate()[2] + require.Len(t, got.Result.Value, maxValueLen) + require.NotEmpty(t, got.Result.Error) + + unknownKeyMetadata := wantMetadata1 + err = agentClient.PostMetadata(ctx, "unknown", unknownKeyMetadata) + require.NoError(t, err) +} diff --git a/coderd/workspaceapps_test.go b/coderd/workspaceapps_test.go index 24e4529a82664..dff7033d89848 100644 --- a/coderd/workspaceapps_test.go +++ b/coderd/workspaceapps_test.go @@ -273,7 +273,7 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) if appHost != "" { - metadata, err := agentClient.Metadata(context.Background()) + manifest, err := agentClient.Manifest(context.Background()) require.NoError(t, err) proxyURL := fmt.Sprintf( "http://{{port}}--%s--%s--%s%s", @@ -285,7 +285,7 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U if client.URL.Port() != "" { proxyURL += fmt.Sprintf(":%s", client.URL.Port()) } - require.Equal(t, proxyURL, metadata.VSCodePortProxyURI) + require.Equal(t, proxyURL, manifest.VSCodePortProxyURI) } agentCloser := agent.New(agent.Options{ Client: agentClient, diff --git a/coderd/wsconncache/wsconncache_test.go b/coderd/wsconncache/wsconncache_test.go index da5df152f06a0..24f0f241a123d 100644 --- a/coderd/wsconncache/wsconncache_test.go +++ b/coderd/wsconncache/wsconncache_test.go @@ -41,7 +41,7 @@ func TestCache(t *testing.T) { t.Run("Same", func(t *testing.T) { t.Parallel() cache := wsconncache.New(func(id uuid.UUID) (*codersdk.WorkspaceAgentConn, error) { - return setupAgent(t, agentsdk.Metadata{}, 0), nil + return setupAgent(t, agentsdk.Manifest{}, 0), nil }, 0) defer func() { _ = cache.Close() @@ -57,7 +57,7 @@ func TestCache(t *testing.T) { called := atomic.NewInt32(0) cache := wsconncache.New(func(id uuid.UUID) (*codersdk.WorkspaceAgentConn, error) { called.Add(1) - return setupAgent(t, agentsdk.Metadata{}, 0), nil + return setupAgent(t, agentsdk.Manifest{}, 0), nil }, time.Microsecond) defer func() { _ = cache.Close() @@ -75,7 +75,7 @@ func TestCache(t *testing.T) { t.Run("NoExpireWhenLocked", func(t *testing.T) { t.Parallel() cache := wsconncache.New(func(id uuid.UUID) (*codersdk.WorkspaceAgentConn, error) { - return setupAgent(t, agentsdk.Metadata{}, 0), nil + return setupAgent(t, agentsdk.Manifest{}, 0), nil }, time.Microsecond) defer func() { _ = cache.Close() @@ -108,7 +108,7 @@ func TestCache(t *testing.T) { go server.Serve(random) cache := wsconncache.New(func(id uuid.UUID) (*codersdk.WorkspaceAgentConn, error) { - return setupAgent(t, agentsdk.Metadata{}, 0), nil + return setupAgent(t, agentsdk.Manifest{}, 0), nil }, time.Microsecond) defer func() { _ = cache.Close() @@ -154,10 +154,10 @@ func TestCache(t *testing.T) { }) } -func setupAgent(t *testing.T, metadata agentsdk.Metadata, ptyTimeout time.Duration) *codersdk.WorkspaceAgentConn { +func setupAgent(t *testing.T, manifest agentsdk.Manifest, ptyTimeout time.Duration) *codersdk.WorkspaceAgentConn { t.Helper() - metadata.DERPMap = tailnettest.RunDERPAndSTUN(t) + manifest.DERPMap = tailnettest.RunDERPAndSTUN(t) coordinator := tailnet.NewCoordinator() t.Cleanup(func() { @@ -168,7 +168,7 @@ func setupAgent(t *testing.T, metadata agentsdk.Metadata, ptyTimeout time.Durati Client: &client{ t: t, agentID: agentID, - metadata: metadata, + manifest: manifest, coordinator: coordinator, }, Logger: slogtest.Make(t, nil).Named("agent").Leveled(slog.LevelInfo), @@ -179,7 +179,7 @@ func setupAgent(t *testing.T, metadata agentsdk.Metadata, ptyTimeout time.Durati }) conn, err := tailnet.NewConn(&tailnet.Options{ Addresses: []netip.Prefix{netip.PrefixFrom(tailnet.IP(), 128)}, - DERPMap: metadata.DERPMap, + DERPMap: manifest.DERPMap, Logger: slogtest.Make(t, nil).Named("tailnet").Leveled(slog.LevelDebug), }) require.NoError(t, err) @@ -211,12 +211,12 @@ func setupAgent(t *testing.T, metadata agentsdk.Metadata, ptyTimeout time.Durati type client struct { t *testing.T agentID uuid.UUID - metadata agentsdk.Metadata + manifest agentsdk.Manifest coordinator tailnet.Coordinator } -func (c *client) Metadata(_ context.Context) (agentsdk.Metadata, error) { - return c.metadata, nil +func (c *client) Manifest(_ context.Context) (agentsdk.Manifest, error) { + return c.manifest, nil } func (c *client) Listen(_ context.Context) (net.Conn, error) { @@ -246,6 +246,10 @@ func (*client) PostAppHealth(_ context.Context, _ agentsdk.PostAppHealthsRequest return nil } +func (*client) PostMetadata(_ context.Context, _ string, _ agentsdk.PostMetadataRequest) error { + return nil +} + func (*client) PostStartup(_ context.Context, _ agentsdk.PostStartupRequest) error { return nil } diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index 1796baea60ac6..db6c839c5a994 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -65,37 +65,56 @@ func (c *Client) GitSSHKey(ctx context.Context) (GitSSHKey, error) { return gitSSHKey, json.NewDecoder(res.Body).Decode(&gitSSHKey) } -type Metadata struct { +// In the future, we may want to support sending back multiple values for +// performance. +type PostMetadataRequest = codersdk.WorkspaceAgentMetadataResult + +func (c *Client) PostMetadata(ctx context.Context, key string, req PostMetadataRequest) error { + res, err := c.SDK.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/me/metadata/"+key, req) + if err != nil { + return xerrors.Errorf("execute request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusNoContent { + return codersdk.ReadBodyAsError(res) + } + + return nil +} + +type Manifest struct { // GitAuthConfigs stores the number of Git configurations // the Coder deployment has. If this number is >0, we // set up special configuration in the workspace. - GitAuthConfigs int `json:"git_auth_configs"` - VSCodePortProxyURI string `json:"vscode_port_proxy_uri"` - Apps []codersdk.WorkspaceApp `json:"apps"` - DERPMap *tailcfg.DERPMap `json:"derpmap"` - EnvironmentVariables map[string]string `json:"environment_variables"` - StartupScript string `json:"startup_script"` - StartupScriptTimeout time.Duration `json:"startup_script_timeout"` - Directory string `json:"directory"` - MOTDFile string `json:"motd_file"` - ShutdownScript string `json:"shutdown_script"` - ShutdownScriptTimeout time.Duration `json:"shutdown_script_timeout"` -} - -// Metadata fetches metadata for the currently authenticated workspace agent. -func (c *Client) Metadata(ctx context.Context) (Metadata, error) { - res, err := c.SDK.Request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/metadata", nil) - if err != nil { - return Metadata{}, err + GitAuthConfigs int `json:"git_auth_configs"` + VSCodePortProxyURI string `json:"vscode_port_proxy_uri"` + Apps []codersdk.WorkspaceApp `json:"apps"` + DERPMap *tailcfg.DERPMap `json:"derpmap"` + EnvironmentVariables map[string]string `json:"environment_variables"` + StartupScript string `json:"startup_script"` + StartupScriptTimeout time.Duration `json:"startup_script_timeout"` + Directory string `json:"directory"` + MOTDFile string `json:"motd_file"` + ShutdownScript string `json:"shutdown_script"` + ShutdownScriptTimeout time.Duration `json:"shutdown_script_timeout"` + Metadata []codersdk.WorkspaceAgentMetadataDescription `json:"metadata"` +} + +// Manifest fetches manifest for the currently authenticated workspace agent. +func (c *Client) Manifest(ctx context.Context) (Manifest, error) { + res, err := c.SDK.Request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/manifest", nil) + if err != nil { + return Manifest{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return Metadata{}, codersdk.ReadBodyAsError(res) + return Manifest{}, codersdk.ReadBodyAsError(res) } - var agentMeta Metadata + var agentMeta Manifest err = json.NewDecoder(res.Body).Decode(&agentMeta) if err != nil { - return Metadata{}, err + return Manifest{}, err } accessingPort := c.SDK.URL.Port() if accessingPort == "" { @@ -106,14 +125,14 @@ func (c *Client) Metadata(ctx context.Context) (Metadata, error) { } accessPort, err := strconv.Atoi(accessingPort) if err != nil { - return Metadata{}, xerrors.Errorf("convert accessing port %q: %w", accessingPort, err) + return Manifest{}, xerrors.Errorf("convert accessing port %q: %w", accessingPort, err) } // Agents can provide an arbitrary access URL that may be different // that the globally configured one. This breaks the built-in DERP, // which would continue to reference the global access URL. // // This converts all built-in DERPs to use the access URL that the - // metadata request was performed with. + // manifest request was performed with. for _, region := range agentMeta.DERPMap.Regions { if !region.EmbeddedRelay { continue diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index cf2d1687f3dc5..706ea1ec6a257 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -19,6 +19,7 @@ import ( "tailscale.com/tailcfg" "cdr.dev/slog" + "github.com/coder/coder/coderd/tracing" "github.com/coder/coder/tailnet" "github.com/coder/retry" ) @@ -75,6 +76,31 @@ var WorkspaceAgentLifecycleOrder = []WorkspaceAgentLifecycle{ WorkspaceAgentLifecycleOff, } +type WorkspaceAgentMetadataResult struct { + CollectedAt time.Time `json:"collected_at" format:"date-time"` + // Age is the number of seconds since the metadata was collected. + // It is provided in addition to CollectedAt to protect against clock skew. + Age int64 `json:"age"` + Value string `json:"value"` + Error string `json:"error"` +} + +// WorkspaceAgentMetadataDescription is a description of dynamic metadata the agent should report +// back to coderd. It is provided via the `metadata` list in the `coder_agent` +// block. +type WorkspaceAgentMetadataDescription struct { + DisplayName string `json:"display_name"` + Key string `json:"key"` + Script string `json:"script"` + Interval int64 `json:"interval"` + Timeout int64 `json:"timeout"` +} + +type WorkspaceAgentMetadata struct { + Result WorkspaceAgentMetadataResult `json:"result"` + Description WorkspaceAgentMetadataDescription `json:"description"` +} + type WorkspaceAgent struct { ID uuid.UUID `json:"id" format:"uuid"` CreatedAt time.Time `json:"created_at" format:"date-time"` @@ -258,6 +284,75 @@ func (c *Client) DialWorkspaceAgent(ctx context.Context, agentID uuid.UUID, opti return agentConn, nil } +// WatchWorkspaceAgentMetadata watches the metadata of a workspace agent. +// The returned channel will be closed when the context is canceled. Exactly +// one error will be sent on the error channel. The metadata channel is never closed. +func (c *Client) WatchWorkspaceAgentMetadata(ctx context.Context, id uuid.UUID) (<-chan []WorkspaceAgentMetadata, <-chan error) { + ctx, span := tracing.StartSpan(ctx) + defer span.End() + + metadataChan := make(chan []WorkspaceAgentMetadata, 256) + + watch := func() error { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceagents/%s/watch-metadata", id), nil) + if err != nil { + return err + } + if res.StatusCode != http.StatusOK { + return ReadBodyAsError(res) + } + + nextEvent := ServerSentEventReader(ctx, res.Body) + defer res.Body.Close() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + break + } + + sse, err := nextEvent() + if err != nil { + return err + } + + b, ok := sse.Data.([]byte) + if !ok { + return xerrors.Errorf("unexpected data type: %T", sse.Data) + } + + switch sse.Type { + case ServerSentEventTypeData: + var met []WorkspaceAgentMetadata + err = json.Unmarshal(b, &met) + if err != nil { + return xerrors.Errorf("unmarshal metadata: %w", err) + } + metadataChan <- met + case ServerSentEventTypeError: + var r Response + err = json.Unmarshal(b, &r) + if err != nil { + return xerrors.Errorf("unmarshal error: %w", err) + } + return xerrors.Errorf("%+v", r) + default: + return xerrors.Errorf("unexpected event type: %s", sse.Type) + } + } + } + + errorChan := make(chan error, 1) + go func() { + defer close(errorChan) + errorChan <- watch() + }() + + return metadataChan, errorChan +} + // WorkspaceAgent returns an agent by ID. func (c *Client) WorkspaceAgent(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error) { res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceagents/%s", id), nil) diff --git a/codersdk/workspaceagents_test.go b/codersdk/workspaceagents_test.go index a5b20d886acdc..6373160c299e1 100644 --- a/codersdk/workspaceagents_test.go +++ b/codersdk/workspaceagents_test.go @@ -25,7 +25,7 @@ func TestWorkspaceAgentMetadata(t *testing.T) { // This test ensures that the DERP map returned properly // mutates built-in DERPs with the client access URL. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - httpapi.Write(context.Background(), w, http.StatusOK, agentsdk.Metadata{ + httpapi.Write(context.Background(), w, http.StatusOK, agentsdk.Manifest{ DERPMap: &tailcfg.DERPMap{ Regions: map[int]*tailcfg.DERPRegion{ 1: { @@ -43,9 +43,9 @@ func TestWorkspaceAgentMetadata(t *testing.T) { parsed, err := url.Parse(srv.URL) require.NoError(t, err) client := agentsdk.New(parsed) - metadata, err := client.Metadata(context.Background()) + manifest, err := client.Manifest(context.Background()) require.NoError(t, err) - region := metadata.DERPMap.Regions[1] + region := manifest.DERPMap.Regions[1] require.True(t, region.EmbeddedRelay) require.Len(t, region.Nodes, 1) node := region.Nodes[0] diff --git a/docs/api/agents.md b/docs/api/agents.md index 7757a89c89aeb..79ad8a5029994 100644 --- a/docs/api/agents.md +++ b/docs/api/agents.md @@ -273,18 +273,18 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/gitsshkey \ To perform this operation, you must be authenticated. [Learn more](authentication.md). -## Get authorized workspace agent metadata +## Get authorized workspace agent manifest ### Code samples ```shell # Example request using curl -curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/metadata \ +curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/manifest \ -H 'Accept: application/json' \ -H 'Coder-Session-Token: API_KEY' ``` -`GET /workspaceagents/me/metadata` +`GET /workspaceagents/me/manifest` ### Example responses @@ -368,6 +368,15 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/metadata \ "property2": "string" }, "git_auth_configs": 0, + "metadata": [ + { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + } + ], "motd_file": "string", "shutdown_script": "string", "shutdown_script_timeout": 0, @@ -381,7 +390,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/metadata \ | Status | Meaning | Description | Schema | | ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------ | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [agentsdk.Metadata](schemas.md#agentsdkmetadata) | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [agentsdk.Manifest](schemas.md#agentsdkmanifest) | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 2f76aef88bbb9..0cde0757057eb 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -94,7 +94,7 @@ | ---------------- | ------ | -------- | ------------ | ----------- | | `json_web_token` | string | true | | | -## agentsdk.Metadata +## agentsdk.Manifest ```json { @@ -174,6 +174,15 @@ "property2": "string" }, "git_auth_configs": 0, + "metadata": [ + { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + } + ], "motd_file": "string", "shutdown_script": "string", "shutdown_script_timeout": 0, @@ -185,20 +194,21 @@ ### Properties -| Name | Type | Required | Restrictions | Description | -| ------------------------- | ------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `apps` | array of [codersdk.WorkspaceApp](#codersdkworkspaceapp) | false | | | -| `derpmap` | [tailcfg.DERPMap](#tailcfgderpmap) | false | | | -| `directory` | string | false | | | -| `environment_variables` | object | false | | | -| » `[any property]` | string | false | | | -| `git_auth_configs` | integer | false | | Git auth configs stores the number of Git configurations the Coder deployment has. If this number is >0, we set up special configuration in the workspace. | -| `motd_file` | string | false | | | -| `shutdown_script` | string | false | | | -| `shutdown_script_timeout` | integer | false | | | -| `startup_script` | string | false | | | -| `startup_script_timeout` | integer | false | | | -| `vscode_port_proxy_uri` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| ------------------------- | ------------------------------------------------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `apps` | array of [codersdk.WorkspaceApp](#codersdkworkspaceapp) | false | | | +| `derpmap` | [tailcfg.DERPMap](#tailcfgderpmap) | false | | | +| `directory` | string | false | | | +| `environment_variables` | object | false | | | +| » `[any property]` | string | false | | | +| `git_auth_configs` | integer | false | | Git auth configs stores the number of Git configurations the Coder deployment has. If this number is >0, we set up special configuration in the workspace. | +| `metadata` | array of [codersdk.WorkspaceAgentMetadataDescription](#codersdkworkspaceagentmetadatadescription) | false | | | +| `motd_file` | string | false | | | +| `shutdown_script` | string | false | | | +| `shutdown_script_timeout` | integer | false | | | +| `startup_script` | string | false | | | +| `startup_script_timeout` | integer | false | | | +| `vscode_port_proxy_uri` | string | false | | | ## agentsdk.PatchStartupLogs @@ -251,6 +261,26 @@ | ------- | -------------------------------------------------------------------- | -------- | ------------ | ----------- | | `state` | [codersdk.WorkspaceAgentLifecycle](#codersdkworkspaceagentlifecycle) | false | | | +## agentsdk.PostMetadataRequest + +```json +{ + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| -------------- | ------- | -------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| `age` | integer | false | | Age is the number of seconds since the metadata was collected. It is provided in addition to CollectedAt to protect against clock skew. | +| `collected_at` | string | false | | | +| `error` | string | false | | | +| `value` | string | false | | | + ## agentsdk.PostStartupRequest ```json @@ -4680,6 +4710,28 @@ Parameter represents a set value for the scope. | ------- | ------------------------------------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `ports` | array of [codersdk.WorkspaceAgentListeningPort](#codersdkworkspaceagentlisteningport) | false | | If there are no ports in the list, nothing should be displayed in the UI. There must not be a "no ports available" message or anything similar, as there will always be no ports displayed on platforms where our port detection logic is unsupported. | +## codersdk.WorkspaceAgentMetadataDescription + +```json +{ + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| -------------- | ------- | -------- | ------------ | ----------- | +| `display_name` | string | false | | | +| `interval` | integer | false | | | +| `key` | string | false | | | +| `script` | string | false | | | +| `timeout` | integer | false | | | + ## codersdk.WorkspaceAgentStartupLog ```json diff --git a/docs/images/agent-metadata.png b/docs/images/agent-metadata.png new file mode 100644 index 0000000000000..666ee2b0b9ef3 Binary files /dev/null and b/docs/images/agent-metadata.png differ diff --git a/docs/manifest.json b/docs/manifest.json index 13c49a8d6d9fc..c180249d002e4 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -135,6 +135,13 @@ "path": "./templates/resource-metadata.md", "icon_path": "./images/icons/table-rows.svg" }, + { + "title": "Agent Metadata", + "description": "Learn how to expose live agent information to users", + "path": "./templates/agent-metadata.md", + "icon_path": "./images/icons/table-rows.svg", + "state": "alpha" + }, { "title": "Docker in Docker", "description": "Use docker inside containerized templates", diff --git a/docs/templates/agent-metadata.md b/docs/templates/agent-metadata.md new file mode 100644 index 0000000000000..1ee887ef211b8 --- /dev/null +++ b/docs/templates/agent-metadata.md @@ -0,0 +1,90 @@ +# Agent Metadata (alpha) + +
+Agent metadata is in an alpha state and may break or disappear at any time. +
+ +![agent-metadata](../images/agent-metadata.png) + +With Agent Metadata, template admin can expose operational metrics from +their workspaces to their users. It is a sibling of [Resource Metadata](./resource-metadata.md). + +See the [Terraform reference](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#metadata). + +## Examples + +All of these examples use [heredoc strings](https://developer.hashicorp.com/terraform/language/expressions/strings#heredoc-strings) for the script declaration. With heredoc strings you +can script without messy escape codes, just as if you were working in your terminal. + +Here are useful agent metadata snippets for Linux agents: + +```hcl +resource "coder_agent" "main" { + os = "linux" + ... + metadata { + display_name = "CPU Usage" + key = "cpu" + # calculates CPU usage by summing the "us", "sy" and "id" columns of + # vmstat. + script = <> + interval = 1 + timeout = 1 + } +} +``` + +## Utilities + +[vmstat](https://linux.die.net/man/8/vmstat) is available in most Linux +distributions and contains virtual memory, CPU and IO statistics. Running `vmstat` +produces output that looks like: + +``` +procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- +r b swpd free buff cache si so bi bo in cs us sy id wa st +0 0 19580 4781680 12133692 217646944 0 2 4 32 1 0 1 1 98 0 0 +``` + +[dstat](https://linux.die.net/man/1/dstat) is considerably more parseable +than `vmstat` but often not included in base images. It is easily installed by +most package managers under the name `dstat`. The output of running `dstat 1 1` looks +like: + +``` +--total-cpu-usage-- -dsk/total- -net/total- ---paging-- ---system-- +usr sys idl wai stl| read writ| recv send| in out | int csw +1 1 98 0 0|3422k 25M| 0 0 | 153k 904k| 123k 174k +``` diff --git a/docs/templates/resource-metadata.md b/docs/templates/resource-metadata.md index a5870a9925734..ebfb360c9eeb7 100644 --- a/docs/templates/resource-metadata.md +++ b/docs/templates/resource-metadata.md @@ -97,6 +97,28 @@ To make easier for you to customize your resource we added some built-in icons: We also have other icons related to the IDEs. You can see all the icons [here](https://github.com/coder/coder/tree/main/site/static/icon). +## Agent Metadata + +In cases where you want to present automatically updating, dynamic values. You +can use the `metadata` block in the `coder_agent` resource. For example: + +```hcl +resource "coder_agent" "dev" { + os = "linux" + arch = "amd64" + dir = "/workspace" + metadata { + name = "Process Count" + script = "ps aux | wc -l" + interval = 1 + timeout = 3 + } +} +``` + +Read more [here](./agent-metadata.md). + ## Up next - Learn about [secrets](../secrets.md) +- Learn about [Agent Metadata](../agent-metadata.md) diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index 5cb07eefdfb9e..0a0a3aaac5db1 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -14,6 +14,14 @@ import ( "github.com/coder/coder/provisionersdk/proto" ) +type agentMetadata struct { + Key string `mapstructure:"key"` + DisplayName string `mapstructure:"display_name"` + Script string `mapstructure:"script"` + Interval int64 `mapstructure:"interval"` + Timeout int64 `mapstructure:"timeout"` +} + // A mapping of attributes on the "coder_agent" resource. type agentAttributes struct { Auth string `mapstructure:"auth"` @@ -31,6 +39,7 @@ type agentAttributes struct { StartupScriptTimeoutSeconds int32 `mapstructure:"startup_script_timeout"` ShutdownScript string `mapstructure:"shutdown_script"` ShutdownScriptTimeoutSeconds int32 `mapstructure:"shutdown_script_timeout"` + Metadata []agentMetadata `mapstructure:"metadata"` } // A mapping of attributes on the "coder_app" resource. @@ -59,15 +68,15 @@ type appHealthcheckAttributes struct { } // A mapping of attributes on the "coder_metadata" resource. -type metadataAttributes struct { - ResourceID string `mapstructure:"resource_id"` - Hide bool `mapstructure:"hide"` - Icon string `mapstructure:"icon"` - DailyCost int32 `mapstructure:"daily_cost"` - Items []metadataItem `mapstructure:"item"` +type resourceMetadataAttributes struct { + ResourceID string `mapstructure:"resource_id"` + Hide bool `mapstructure:"hide"` + Icon string `mapstructure:"icon"` + DailyCost int32 `mapstructure:"daily_cost"` + Items []resourceMetadataItem `mapstructure:"item"` } -type metadataItem struct { +type resourceMetadataItem struct { Key string `mapstructure:"key"` Value string `mapstructure:"value"` Sensitive bool `mapstructure:"sensitive"` @@ -148,6 +157,17 @@ func ConvertState(modules []*tfjson.StateModule, rawGraph string, rawParameterNa loginBeforeReady = attrs.LoginBeforeReady } + var metadata []*proto.Agent_Metadata + for _, item := range attrs.Metadata { + metadata = append(metadata, &proto.Agent_Metadata{ + Key: item.Key, + DisplayName: item.DisplayName, + Script: item.Script, + Interval: item.Interval, + Timeout: item.Timeout, + }) + } + agent := &proto.Agent{ Name: tfResource.Name, Id: attrs.ID, @@ -163,6 +183,7 @@ func ConvertState(modules []*tfjson.StateModule, rawGraph string, rawParameterNa StartupScriptTimeoutSeconds: attrs.StartupScriptTimeoutSeconds, ShutdownScript: attrs.ShutdownScript, ShutdownScriptTimeoutSeconds: attrs.ShutdownScriptTimeoutSeconds, + Metadata: metadata, } switch attrs.Auth { case "token": @@ -356,7 +377,7 @@ func ConvertState(modules []*tfjson.StateModule, rawGraph string, rawParameterNa continue } - var attrs metadataAttributes + var attrs resourceMetadataAttributes err = mapstructure.Decode(resource.AttributeValues, &attrs) if err != nil { return nil, xerrors.Errorf("decode metadata attributes: %w", err) diff --git a/provisioner/terraform/resources_test.go b/provisioner/terraform/resources_test.go index 9e81de3b25661..34ab787b89cef 100644 --- a/provisioner/terraform/resources_test.go +++ b/provisioner/terraform/resources_test.go @@ -221,6 +221,23 @@ func TestConvertResources(t *testing.T) { Value: "squirrel", Sensitive: true, }}, + Agents: []*proto.Agent{{ + Name: "main", + Auth: &proto.Agent_Token{}, + OperatingSystem: "linux", + Architecture: "amd64", + Metadata: []*proto.Agent_Metadata{{ + Key: "process_count", + DisplayName: "Process Count", + Script: "ps -ef | wc -l", + Interval: 5, + Timeout: 1, + }}, + ShutdownScriptTimeoutSeconds: 300, + StartupScriptTimeoutSeconds: 300, + LoginBeforeReady: true, + ConnectionTimeoutSeconds: 120, + }}, }}, }, // Tests that resources with the same id correctly get metadata applied diff --git a/provisioner/terraform/testdata/calling-module/calling-module.tfplan.json b/provisioner/terraform/testdata/calling-module/calling-module.tfplan.json index 363b4f52fde5e..be7a0aaaca9da 100644 --- a/provisioner/terraform/testdata/calling-module/calling-module.tfplan.json +++ b/provisioner/terraform/testdata/calling-module/calling-module.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.1", - "terraform_version": "1.3.6", + "terraform_version": "1.3.7", "planned_values": { "root_module": { "resources": [ diff --git a/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json b/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json index fc8751b6e6724..eb44278efaacd 100644 --- a/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json +++ b/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.3.6", + "terraform_version": "1.3.7", "values": { "root_module": { "resources": [ @@ -17,11 +17,11 @@ "connection_timeout": 120, "dir": null, "env": null, - "id": "411bdd93-0ea4-4376-a032-52b1fbf44ca5", + "id": "2d84db09-c3c3-4272-8119-1847e68e2dbe", "init_script": "", "os": "linux", "startup_script": null, - "token": "eeac85aa-19f9-4a50-8002-dfd11556081b", + "token": "d5d46d6a-5e4b-493f-9b68-dd8c78727cac", "troubleshooting_url": null }, "sensitive_values": {} @@ -46,7 +46,7 @@ "outputs": { "script": "" }, - "random": "5816533441722838433" + "random": "6336115403873146745" }, "sensitive_values": { "inputs": {}, @@ -61,7 +61,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "5594550025354402054", + "id": "3671991160538447687", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.json b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.json index e3e1fe4440fb9..9097d4a673473 100644 --- a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.json +++ b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.1", - "terraform_version": "1.3.6", + "terraform_version": "1.3.7", "planned_values": { "root_module": { "resources": [ diff --git a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json index a55aa267bf5e9..75f44e52662c9 100644 --- a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json +++ b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.3.6", + "terraform_version": "1.3.7", "values": { "root_module": { "resources": [ @@ -17,11 +17,11 @@ "connection_timeout": 120, "dir": null, "env": null, - "id": "4dc52ff5-b270-47a2-8b6a-695b4872f07b", + "id": "36c5629c-8ac4-4e6b-ae6e-94b8f1bbb0bc", "init_script": "", "os": "linux", "startup_script": null, - "token": "c5c8378e-66df-4f3f-94a2-84bff1dc6fc9", + "token": "9ed706cc-8b11-4b05-b6c3-ea943af2c60c", "troubleshooting_url": null }, "sensitive_values": {} @@ -34,7 +34,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "7372487656283423086", + "id": "4876594853562919335", "triggers": null }, "sensitive_values": {}, @@ -51,7 +51,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "2553224683756509362", + "id": "3012307200839131913", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfplan.json b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfplan.json index 89793191c802a..ff45f741cfeaa 100644 --- a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfplan.json +++ b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.1", - "terraform_version": "1.3.6", + "terraform_version": "1.3.7", "planned_values": { "root_module": { "resources": [ diff --git a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json index e696c33fea0a9..ef172db9b63b6 100644 --- a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json +++ b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.3.6", + "terraform_version": "1.3.7", "values": { "root_module": { "resources": [ @@ -17,11 +17,11 @@ "connection_timeout": 120, "dir": null, "env": null, - "id": "3cd9cbba-31f7-482c-a8a0-bf39dfe42dc2", + "id": "a79d53f2-f616-4be2-b1bf-6f6ce7409e23", "init_script": "", "os": "linux", "startup_script": null, - "token": "8b063f22-9e66-4dbf-9f13-7b09ac2a470f", + "token": "bb333041-5225-490d-86a0-69d1b5afd0db", "troubleshooting_url": null }, "sensitive_values": {} @@ -34,7 +34,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "3370347998754925285", + "id": "3651093174168180448", "triggers": null }, "sensitive_values": {}, @@ -50,7 +50,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "4707694957868093590", + "id": "3820948743929828676", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfstate.json b/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfstate.json index 268ed84b45ae3..6116c9d09377d 100644 --- a/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfstate.json +++ b/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfstate.json @@ -17,7 +17,7 @@ "connection_timeout": 120, "dir": null, "env": null, - "id": "78b29f93-097d-403b-ab56-0bc943d427cc", + "id": "7060319c-a8bf-44b7-8fb9-5a4dc3bd35fe", "init_script": "", "login_before_ready": true, "motd_file": null, @@ -26,7 +26,7 @@ "shutdown_script_timeout": 300, "startup_script": null, "startup_script_timeout": 300, - "token": "a57838e5-355c-471a-9a85-f81314fbaec6", + "token": "9ea68982-fd51-4510-9c23-9e9ad1ce1ef7", "troubleshooting_url": null }, "sensitive_values": {} @@ -65,7 +65,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "1416347524569828366", + "id": "4918728983070449527", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/instance-id/instance-id.tfplan.json b/provisioner/terraform/testdata/instance-id/instance-id.tfplan.json index 6bafb713b386a..f54a0d003e4c8 100644 --- a/provisioner/terraform/testdata/instance-id/instance-id.tfplan.json +++ b/provisioner/terraform/testdata/instance-id/instance-id.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.1", - "terraform_version": "1.3.6", + "terraform_version": "1.3.7", "planned_values": { "root_module": { "resources": [ diff --git a/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json b/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json index 0344e88948fff..cc8870100a9b9 100644 --- a/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json +++ b/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.3.6", + "terraform_version": "1.3.7", "values": { "root_module": { "resources": [ @@ -17,11 +17,11 @@ "connection_timeout": 120, "dir": null, "env": null, - "id": "36189f12-6eed-4094-9179-6584a8659219", + "id": "facfb2ad-417d-412c-8304-ce50ff8a886e", "init_script": "", "os": "linux", "startup_script": null, - "token": "907fa482-fd3b-44be-8cfb-4515e3122e78", + "token": "f1b3312a-af1c-45a8-9695-23d92c4cb68d", "troubleshooting_url": null }, "sensitive_values": {} @@ -34,8 +34,8 @@ "provider_name": "registry.terraform.io/coder/coder", "schema_version": 0, "values": { - "agent_id": "36189f12-6eed-4094-9179-6584a8659219", - "id": "c9bd849e-ac37-440b-9c5b-a288344be41c", + "agent_id": "facfb2ad-417d-412c-8304-ce50ff8a886e", + "id": "6ce4c6f4-1b93-4b22-9b17-1dcb50a36e77", "instance_id": "example" }, "sensitive_values": {}, @@ -51,7 +51,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "4399071137990404376", + "id": "2761080386857837319", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfstate.json b/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfstate.json index a43e7923880f4..91f08d1e250e4 100644 --- a/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfstate.json +++ b/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfstate.json @@ -17,11 +17,11 @@ "connection_timeout": 120, "dir": null, "env": null, - "id": "5cb3c105-1a70-4c60-bd32-b75d9a60a98d", + "id": "441cfb02-3044-4d2b-ac2c-1ac150ec8c58", "init_script": "", "os": "linux", "startup_script": null, - "token": "b477690f-0a2d-4d9b-818e-7b60c845c44f", + "token": "75ee44b6-4cbb-4615-acb2-19c53f82dc58", "troubleshooting_url": null }, "sensitive_values": {} @@ -35,12 +35,12 @@ "provider_name": "registry.terraform.io/coder/coder", "schema_version": 0, "values": { - "agent_id": "5cb3c105-1a70-4c60-bd32-b75d9a60a98d", + "agent_id": "441cfb02-3044-4d2b-ac2c-1ac150ec8c58", "command": null, "display_name": "app1", "healthcheck": [], "icon": null, - "id": "72d41f36-d775-424c-9a05-b633da43cd58", + "id": "abbc4449-5dd1-4ee0-ac58-3055a6da206b", "name": null, "relative_path": null, "share": "owner", @@ -64,12 +64,12 @@ "provider_name": "registry.terraform.io/coder/coder", "schema_version": 0, "values": { - "agent_id": "5cb3c105-1a70-4c60-bd32-b75d9a60a98d", + "agent_id": "441cfb02-3044-4d2b-ac2c-1ac150ec8c58", "command": null, "display_name": "app2", "healthcheck": [], "icon": null, - "id": "810dbeda-3041-403d-86e8-146354ad2657", + "id": "ff87411e-4edb-48ce-b62f-6f792d02b25c", "name": null, "relative_path": null, "share": "owner", @@ -92,7 +92,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "1955786418433284816", + "id": "3464468981645058362", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json index f4b5ff036e511..ad6eb7118c57b 100644 --- a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json +++ b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json @@ -17,7 +17,7 @@ "connection_timeout": 120, "dir": null, "env": null, - "id": "6b912abe-50d4-48b2-be7c-1464ca69b5b9", + "id": "7fd8bb3f-704e-4d85-aaaf-1928a9a4df83", "init_script": "", "login_before_ready": true, "motd_file": null, @@ -26,7 +26,7 @@ "shutdown_script_timeout": 300, "startup_script": null, "startup_script_timeout": 300, - "token": "d296a9cd-6f7c-4c6b-b2f3-7a647512efe8", + "token": "ebdd904c-a277-49ec-97cc-e29c7326b475", "troubleshooting_url": null }, "sensitive_values": {} @@ -44,7 +44,7 @@ "connection_timeout": 1, "dir": null, "env": null, - "id": "8a2956f7-d37b-441e-bf62-bd9a45316f6a", + "id": "cd35b6c2-3f81-4857-ac8c-9cc0d1d0f0ee", "init_script": "", "login_before_ready": true, "motd_file": "/etc/motd", @@ -53,7 +53,7 @@ "shutdown_script_timeout": 30, "startup_script": null, "startup_script_timeout": 30, - "token": "b1e0fba4-5bba-439f-b3ea-3f6a8ba4d301", + "token": "bbfc3bb1-31c8-42a2-bc5a-3f4ee95eea7b", "troubleshooting_url": null }, "sensitive_values": {} @@ -71,7 +71,7 @@ "connection_timeout": 120, "dir": null, "env": null, - "id": "819b1b19-a709-463e-9aeb-5e1321b7af23", + "id": "7407c159-30e7-4c7c-8187-3bf0f6805515", "init_script": "", "login_before_ready": false, "motd_file": null, @@ -80,7 +80,7 @@ "shutdown_script_timeout": 300, "startup_script": null, "startup_script_timeout": 300, - "token": "238ff017-12ae-403f-b3f8-4dea4dc87a7d", + "token": "080070d7-cb08-4634-aa05-7ee07a193441", "troubleshooting_url": "https://coder.com/troubleshoot" }, "sensitive_values": {} @@ -93,7 +93,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "5288433022262248914", + "id": "235602507221507275", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json index 04c77276e3669..9b10a6a2d50d7 100644 --- a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json +++ b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.1", - "terraform_version": "1.3.3", + "terraform_version": "1.3.7", "planned_values": { "root_module": { "resources": [ diff --git a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfstate.json b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfstate.json index 07c2f8ce17eed..1f27d054930ec 100644 --- a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfstate.json +++ b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.3.3", + "terraform_version": "1.3.7", "values": { "root_module": { "resources": [ @@ -17,11 +17,11 @@ "connection_timeout": 120, "dir": null, "env": null, - "id": "f911bd98-54fc-476a-aec1-df6e525630a9", + "id": "54519a12-e34b-4c4f-aef9-7dfac5f4949b", "init_script": "", "os": "linux", "startup_script": null, - "token": "fa05ad9c-2062-4707-a27f-12364c89641e", + "token": "bf339e89-0594-4f44-83f0-fc7cde9ceb0c", "troubleshooting_url": null }, "sensitive_values": {} @@ -34,12 +34,12 @@ "provider_name": "registry.terraform.io/coder/coder", "schema_version": 0, "values": { - "agent_id": "f911bd98-54fc-476a-aec1-df6e525630a9", + "agent_id": "54519a12-e34b-4c4f-aef9-7dfac5f4949b", "command": null, "display_name": null, "healthcheck": [], "icon": null, - "id": "038d0f6c-90b7-465b-915a-8a9f0cf21757", + "id": "13101247-bdf1-409e-81e2-51a4ff45576b", "name": null, "relative_path": null, "share": "owner", @@ -62,7 +62,7 @@ "provider_name": "registry.terraform.io/coder/coder", "schema_version": 0, "values": { - "agent_id": "f911bd98-54fc-476a-aec1-df6e525630a9", + "agent_id": "54519a12-e34b-4c4f-aef9-7dfac5f4949b", "command": null, "display_name": null, "healthcheck": [ @@ -73,7 +73,7 @@ } ], "icon": null, - "id": "c00ec121-a167-4418-8c4e-2ccae0a0cd6e", + "id": "ef508497-0437-43eb-b773-c0622582ab5d", "name": null, "relative_path": null, "share": "owner", @@ -98,12 +98,12 @@ "provider_name": "registry.terraform.io/coder/coder", "schema_version": 0, "values": { - "agent_id": "f911bd98-54fc-476a-aec1-df6e525630a9", + "agent_id": "54519a12-e34b-4c4f-aef9-7dfac5f4949b", "command": null, "display_name": null, "healthcheck": [], "icon": null, - "id": "e9226aa6-a1a6-42a7-8557-64620cbf3dc2", + "id": "2c187306-80cc-46ba-a75c-42d4648ff94a", "name": null, "relative_path": null, "share": "owner", @@ -126,7 +126,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "5577006791947779410", + "id": "1264552698255765246", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf index 07dcdffdac150..1b8a4abea68ff 100644 --- a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf +++ b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.6.3" + version = "0.7.0" } } } @@ -10,9 +10,20 @@ terraform { resource "coder_agent" "main" { os = "linux" arch = "amd64" + metadata { + key = "process_count" + display_name = "Process Count" + script = "ps -ef | wc -l" + interval = 5 + timeout = 1 + } } -resource "null_resource" "about" {} +resource "null_resource" "about" { + depends_on = [ + coder_agent.main, + ] +} resource "coder_metadata" "about_info" { resource_id = null_resource.about.id diff --git a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfplan.dot b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfplan.dot index 4bac498b3516c..041734ac4bbc4 100644 --- a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfplan.dot +++ b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfplan.dot @@ -9,9 +9,8 @@ digraph { "[root] provider[\"registry.terraform.io/hashicorp/null\"]" [label = "provider[\"registry.terraform.io/hashicorp/null\"]", shape = "diamond"] "[root] coder_agent.main (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" "[root] coder_metadata.about_info (expand)" -> "[root] null_resource.about (expand)" - "[root] coder_metadata.about_info (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] null_resource.about (expand)" -> "[root] coder_agent.main (expand)" "[root] null_resource.about (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]" - "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_agent.main (expand)" "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_metadata.about_info (expand)" "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.about (expand)" "[root] root" -> "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" diff --git a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfplan.json b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfplan.json index 6fc6591b820a6..9fae1dcf31951 100644 --- a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfplan.json +++ b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.1", - "terraform_version": "1.3.3", + "terraform_version": "1.3.7", "planned_values": { "root_module": { "resources": [ @@ -17,11 +17,29 @@ "connection_timeout": 120, "dir": null, "env": null, + "login_before_ready": true, + "metadata": [ + { + "display_name": "Process Count", + "interval": 5, + "key": "process_count", + "script": "ps -ef | wc -l", + "timeout": 1 + } + ], + "motd_file": null, "os": "linux", + "shutdown_script": null, + "shutdown_script_timeout": 300, "startup_script": null, + "startup_script_timeout": 300, "troubleshooting_url": null }, - "sensitive_values": {} + "sensitive_values": { + "metadata": [ + {} + ] + } }, { "address": "coder_metadata.about_info", @@ -99,17 +117,37 @@ "connection_timeout": 120, "dir": null, "env": null, + "login_before_ready": true, + "metadata": [ + { + "display_name": "Process Count", + "interval": 5, + "key": "process_count", + "script": "ps -ef | wc -l", + "timeout": 1 + } + ], + "motd_file": null, "os": "linux", + "shutdown_script": null, + "shutdown_script_timeout": 300, "startup_script": null, + "startup_script_timeout": 300, "troubleshooting_url": null }, "after_unknown": { "id": true, "init_script": true, + "metadata": [ + {} + ], "token": true }, "before_sensitive": false, "after_sensitive": { + "metadata": [ + {} + ], "token": true } } @@ -208,7 +246,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.6.3" + "version_constraint": "0.7.0-rc0" }, "null": { "name": "null", @@ -227,6 +265,25 @@ "arch": { "constant_value": "amd64" }, + "metadata": [ + { + "display_name": { + "constant_value": "Process Count" + }, + "interval": { + "constant_value": 5 + }, + "key": { + "constant_value": "process_count" + }, + "script": { + "constant_value": "ps -ef | wc -l" + }, + "timeout": { + "constant_value": 1 + } + } + ], "os": { "constant_value": "linux" } @@ -298,7 +355,10 @@ "type": "null_resource", "name": "about", "provider_config_key": "null", - "schema_version": 0 + "schema_version": 0, + "depends_on": [ + "coder_agent.main" + ] } ] } diff --git a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.dot b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.dot index 4bac498b3516c..041734ac4bbc4 100644 --- a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.dot +++ b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.dot @@ -9,9 +9,8 @@ digraph { "[root] provider[\"registry.terraform.io/hashicorp/null\"]" [label = "provider[\"registry.terraform.io/hashicorp/null\"]", shape = "diamond"] "[root] coder_agent.main (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" "[root] coder_metadata.about_info (expand)" -> "[root] null_resource.about (expand)" - "[root] coder_metadata.about_info (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] null_resource.about (expand)" -> "[root] coder_agent.main (expand)" "[root] null_resource.about (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]" - "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_agent.main (expand)" "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_metadata.about_info (expand)" "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.about (expand)" "[root] root" -> "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" diff --git a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json index 0b0e51e5286a4..efcf3b98e13ed 100644 --- a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json +++ b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.3.3", + "terraform_version": "1.3.7", "values": { "root_module": { "resources": [ @@ -17,14 +17,32 @@ "connection_timeout": 120, "dir": null, "env": null, - "id": "7766b2a9-c00f-4cde-9acc-1fc05651dbdf", + "id": "2090d6c6-c4c1-4219-b8f8-9df6db6d5864", "init_script": "", + "login_before_ready": true, + "metadata": [ + { + "display_name": "Process Count", + "interval": 5, + "key": "process_count", + "script": "ps -ef | wc -l", + "timeout": 1 + } + ], + "motd_file": null, "os": "linux", + "shutdown_script": null, + "shutdown_script_timeout": 300, "startup_script": null, - "token": "5e54c173-a813-4df0-b87d-0617082769dc", + "startup_script_timeout": 300, + "token": "a984d85d-eff6-4366-9658-9719fb3dd82e", "troubleshooting_url": null }, - "sensitive_values": {} + "sensitive_values": { + "metadata": [ + {} + ] + } }, { "address": "coder_metadata.about_info", @@ -37,7 +55,7 @@ "daily_cost": 29, "hide": true, "icon": "/icon/server.svg", - "id": "e43f1cd6-5dbb-4d6b-8942-37f914b37be5", + "id": "9e239cb2-e381-423a-bf16-74ece8254eff", "item": [ { "is_null": false, @@ -64,7 +82,7 @@ "value": "squirrel" } ], - "resource_id": "5577006791947779410" + "resource_id": "3104684855633455084" }, "sensitive_values": { "item": [ @@ -75,6 +93,7 @@ ] }, "depends_on": [ + "coder_agent.main", "null_resource.about" ] }, @@ -86,10 +105,13 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "5577006791947779410", + "id": "3104684855633455084", "triggers": null }, - "sensitive_values": {} + "sensitive_values": {}, + "depends_on": [ + "coder_agent.main" + ] } ] } diff --git a/provisioner/terraform/testdata/rich-parameters/rich-parameters.tfplan.json b/provisioner/terraform/testdata/rich-parameters/rich-parameters.tfplan.json index 899e1617f7726..71b701abde02d 100644 --- a/provisioner/terraform/testdata/rich-parameters/rich-parameters.tfplan.json +++ b/provisioner/terraform/testdata/rich-parameters/rich-parameters.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.1", - "terraform_version": "1.4.0", + "terraform_version": "1.3.7", "planned_values": { "root_module": { "resources": [ @@ -105,7 +105,7 @@ ], "prior_state": { "format_version": "1.0", - "terraform_version": "1.4.0", + "terraform_version": "1.3.7", "values": { "root_module": { "resources": [ @@ -120,7 +120,7 @@ "default": null, "description": null, "icon": null, - "id": "5820e575-2637-4830-b6a3-75f4c664b447", + "id": "857b1591-ee42-4ded-9804-783ffd1eb180", "legacy_variable": null, "legacy_variable_name": null, "mutable": false, @@ -162,7 +162,7 @@ "default": "ok", "description": "blah blah", "icon": null, - "id": "9cac2300-0618-45f6-97d9-2f0395b1a0b4", + "id": "1477c44d-b36a-48cd-9942-0b532f1791db", "legacy_variable": null, "legacy_variable_name": null, "mutable": false, diff --git a/provisioner/terraform/testdata/rich-parameters/rich-parameters.tfstate.json b/provisioner/terraform/testdata/rich-parameters/rich-parameters.tfstate.json index 38a1aed88ffcb..268a678601ac4 100644 --- a/provisioner/terraform/testdata/rich-parameters/rich-parameters.tfstate.json +++ b/provisioner/terraform/testdata/rich-parameters/rich-parameters.tfstate.json @@ -1,9 +1,36 @@ { "format_version": "1.0", - "terraform_version": "1.4.0", + "terraform_version": "1.3.7", "values": { "root_module": { "resources": [ + { + "address": "coder_agent.dev", + "mode": "managed", + "type": "coder_agent", + "name": "dev", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "arch": "arm64", + "auth": "token", + "connection_timeout": 120, + "dir": null, + "env": null, + "id": "c2221717-e813-49f0-a655-4cb7aa5265e2", + "init_script": "", + "login_before_ready": true, + "motd_file": null, + "os": "windows", + "shutdown_script": null, + "shutdown_script_timeout": 300, + "startup_script": null, + "startup_script_timeout": 300, + "token": "fdb94db8-fca1-4a13-bbcb-73bfaec95b77", + "troubleshooting_url": null + }, + "sensitive_values": {} + }, { "address": "data.coder_parameter.example", "mode": "data", @@ -15,7 +42,7 @@ "default": null, "description": null, "icon": null, - "id": "30b8b963-684d-4c11-9230-a06b81473f6f", + "id": "f5f644c9-cb0c-47b1-8e02-d9f6fa99b935", "legacy_variable": null, "legacy_variable_name": null, "mutable": false, @@ -57,7 +84,7 @@ "default": "ok", "description": "blah blah", "icon": null, - "id": "c40e87d2-7694-40f7-8b7d-30dbf14dd0c0", + "id": "e2944252-1c30-43c8-9ce3-53a9755030dc", "legacy_variable": null, "legacy_variable_name": null, "mutable": false, @@ -70,33 +97,6 @@ }, "sensitive_values": {} }, - { - "address": "coder_agent.dev", - "mode": "managed", - "type": "coder_agent", - "name": "dev", - "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, - "values": { - "arch": "arm64", - "auth": "token", - "connection_timeout": 120, - "dir": null, - "env": null, - "id": "775b9977-421e-4d4d-8c02-dc38958259e3", - "init_script": "", - "login_before_ready": true, - "motd_file": null, - "os": "windows", - "shutdown_script": null, - "shutdown_script_timeout": 300, - "startup_script": null, - "startup_script_timeout": 300, - "token": "927e1872-90d0-43a2-9a55-a8438ead0ad3", - "troubleshooting_url": null - }, - "sensitive_values": {} - }, { "address": "null_resource.dev", "mode": "managed", @@ -105,7 +105,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "3727779938861599093", + "id": "5032149403215603103", "triggers": null }, "sensitive_values": {}, diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index 013f5fa054ed5..e8061dc426f30 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -1252,14 +1252,15 @@ type Agent struct { // // *Agent_Token // *Agent_InstanceId - Auth isAgent_Auth `protobuf_oneof:"auth"` - ConnectionTimeoutSeconds int32 `protobuf:"varint,11,opt,name=connection_timeout_seconds,json=connectionTimeoutSeconds,proto3" json:"connection_timeout_seconds,omitempty"` - TroubleshootingUrl string `protobuf:"bytes,12,opt,name=troubleshooting_url,json=troubleshootingUrl,proto3" json:"troubleshooting_url,omitempty"` - MotdFile string `protobuf:"bytes,13,opt,name=motd_file,json=motdFile,proto3" json:"motd_file,omitempty"` - LoginBeforeReady bool `protobuf:"varint,14,opt,name=login_before_ready,json=loginBeforeReady,proto3" json:"login_before_ready,omitempty"` - StartupScriptTimeoutSeconds int32 `protobuf:"varint,15,opt,name=startup_script_timeout_seconds,json=startupScriptTimeoutSeconds,proto3" json:"startup_script_timeout_seconds,omitempty"` - ShutdownScript string `protobuf:"bytes,16,opt,name=shutdown_script,json=shutdownScript,proto3" json:"shutdown_script,omitempty"` - ShutdownScriptTimeoutSeconds int32 `protobuf:"varint,17,opt,name=shutdown_script_timeout_seconds,json=shutdownScriptTimeoutSeconds,proto3" json:"shutdown_script_timeout_seconds,omitempty"` + Auth isAgent_Auth `protobuf_oneof:"auth"` + ConnectionTimeoutSeconds int32 `protobuf:"varint,11,opt,name=connection_timeout_seconds,json=connectionTimeoutSeconds,proto3" json:"connection_timeout_seconds,omitempty"` + TroubleshootingUrl string `protobuf:"bytes,12,opt,name=troubleshooting_url,json=troubleshootingUrl,proto3" json:"troubleshooting_url,omitempty"` + MotdFile string `protobuf:"bytes,13,opt,name=motd_file,json=motdFile,proto3" json:"motd_file,omitempty"` + LoginBeforeReady bool `protobuf:"varint,14,opt,name=login_before_ready,json=loginBeforeReady,proto3" json:"login_before_ready,omitempty"` + StartupScriptTimeoutSeconds int32 `protobuf:"varint,15,opt,name=startup_script_timeout_seconds,json=startupScriptTimeoutSeconds,proto3" json:"startup_script_timeout_seconds,omitempty"` + ShutdownScript string `protobuf:"bytes,16,opt,name=shutdown_script,json=shutdownScript,proto3" json:"shutdown_script,omitempty"` + ShutdownScriptTimeoutSeconds int32 `protobuf:"varint,17,opt,name=shutdown_script_timeout_seconds,json=shutdownScriptTimeoutSeconds,proto3" json:"shutdown_script_timeout_seconds,omitempty"` + Metadata []*Agent_Metadata `protobuf:"bytes,18,rep,name=metadata,proto3" json:"metadata,omitempty"` } func (x *Agent) Reset() { @@ -1420,6 +1421,13 @@ func (x *Agent) GetShutdownScriptTimeoutSeconds() int32 { return 0 } +func (x *Agent) GetMetadata() []*Agent_Metadata { + if x != nil { + return x.Metadata + } + return nil +} + type isAgent_Auth interface { isAgent_Auth() } @@ -1797,6 +1805,85 @@ func (*Provision) Descriptor() ([]byte, []int) { return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{18} } +type Agent_Metadata struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + Script string `protobuf:"bytes,3,opt,name=script,proto3" json:"script,omitempty"` + Interval int64 `protobuf:"varint,4,opt,name=interval,proto3" json:"interval,omitempty"` + Timeout int64 `protobuf:"varint,5,opt,name=timeout,proto3" json:"timeout,omitempty"` +} + +func (x *Agent_Metadata) Reset() { + *x = Agent_Metadata{} + if protoimpl.UnsafeEnabled { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Agent_Metadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Agent_Metadata) ProtoMessage() {} + +func (x *Agent_Metadata) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] + 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 Agent_Metadata.ProtoReflect.Descriptor instead. +func (*Agent_Metadata) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{13, 0} +} + +func (x *Agent_Metadata) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *Agent_Metadata) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *Agent_Metadata) GetScript() string { + if x != nil { + return x.Script + } + return "" +} + +func (x *Agent_Metadata) GetInterval() int64 { + if x != nil { + return x.Interval + } + return 0 +} + +func (x *Agent_Metadata) GetTimeout() int64 { + if x != nil { + return x.Timeout + } + return 0 +} + type Resource_Metadata struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1811,7 +1898,7 @@ type Resource_Metadata struct { func (x *Resource_Metadata) Reset() { *x = Resource_Metadata{} 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) } @@ -1824,7 +1911,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[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 { @@ -1879,7 +1966,7 @@ type Parse_Request struct { func (x *Parse_Request) Reset() { *x = Parse_Request{} 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) } @@ -1892,7 +1979,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[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 { @@ -1927,7 +2014,7 @@ type Parse_Complete struct { func (x *Parse_Complete) Reset() { *x = Parse_Complete{} 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) } @@ -1940,7 +2027,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[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 { @@ -1985,7 +2072,7 @@ type Parse_Response struct { func (x *Parse_Response) Reset() { *x = Parse_Response{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[23] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1998,7 +2085,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[23] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2071,7 +2158,7 @@ type Provision_Metadata struct { func (x *Provision_Metadata) Reset() { *x = Provision_Metadata{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[24] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2084,7 +2171,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[24] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2186,7 +2273,7 @@ type Provision_Config struct { func (x *Provision_Config) Reset() { *x = Provision_Config{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[25] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2199,7 +2286,7 @@ func (x *Provision_Config) String() string { func (*Provision_Config) ProtoMessage() {} func (x *Provision_Config) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[25] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2258,7 +2345,7 @@ type Provision_Plan struct { func (x *Provision_Plan) Reset() { *x = Provision_Plan{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2271,7 +2358,7 @@ func (x *Provision_Plan) String() string { func (*Provision_Plan) ProtoMessage() {} func (x *Provision_Plan) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2334,7 +2421,7 @@ type Provision_Apply struct { func (x *Provision_Apply) Reset() { *x = Provision_Apply{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[27] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2347,7 +2434,7 @@ func (x *Provision_Apply) String() string { func (*Provision_Apply) ProtoMessage() {} func (x *Provision_Apply) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[27] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2386,7 +2473,7 @@ type Provision_Cancel struct { func (x *Provision_Cancel) Reset() { *x = Provision_Cancel{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[28] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2399,7 +2486,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[28] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2431,7 +2518,7 @@ type Provision_Request struct { func (x *Provision_Request) Reset() { *x = Provision_Request{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[29] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2444,7 +2531,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[29] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2526,7 +2613,7 @@ type Provision_Complete struct { func (x *Provision_Complete) Reset() { *x = Provision_Complete{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[30] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2539,7 +2626,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[30] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2612,7 +2699,7 @@ type Provision_Response struct { func (x *Provision_Response) Reset() { *x = Provision_Response{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[31] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2625,7 +2712,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[31] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[32] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2827,7 +2914,7 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, - 0x22, 0xfe, 0x05, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x22, 0xc7, 0x07, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, @@ -2871,210 +2958,223 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x05, 0x52, 0x1c, 0x73, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, - 0x73, 0x1a, 0x36, 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, 0xb5, 0x02, 0x0a, 0x03, 0x41, 0x70, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6c, 0x75, - 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x12, 0x21, 0x0a, - 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, - 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, - 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, - 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, - 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x3a, - 0x0a, 0x0b, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x07, 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, 0x12, 0x41, 0x0a, 0x0d, 0x73, 0x68, - 0x61, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x41, 0x70, 0x70, 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, - 0x0c, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x0a, - 0x08, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x08, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 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, 0xf1, 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, 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x6e, 0x73, - 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0c, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1d, - 0x0a, 0x0a, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x5f, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x08, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x09, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x43, 0x6f, 0x73, 0x74, 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, 0xcb, 0x02, 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, 0xa3, 0x01, 0x0a, 0x08, - 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x4c, 0x0a, 0x12, 0x74, 0x65, 0x6d, 0x70, - 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x65, 0x72, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, - 0x62, 0x6c, 0x65, 0x52, 0x11, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, - 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 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, 0x90, 0x0d, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0xeb, 0x03, 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, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x65, 0x6d, 0x70, - 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0c, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x29, 0x0a, - 0x10, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, - 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x48, 0x0a, 0x21, 0x77, 0x6f, 0x72, 0x6b, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6f, 0x69, 0x64, 0x63, - 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0a, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x1d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, - 0x6e, 0x65, 0x72, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x1a, 0xad, 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 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, 0x14, 0x0a, 0x05, 0x73, - 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, - 0x65, 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, 0x32, - 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x5f, 0x6c, 0x6f, - 0x67, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x70, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, - 0x65, 0x6c, 0x1a, 0xeb, 0x02, 0x0a, 0x04, 0x50, 0x6c, 0x61, 0x6e, 0x12, 0x35, 0x0a, 0x06, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 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, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 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, 0x53, 0x0a, 0x15, 0x72, 0x69, - 0x63, 0x68, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, - 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x13, 0x72, 0x69, 0x63, 0x68, - 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, - 0x43, 0x0a, 0x0f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x73, 0x12, 0x4a, 0x0a, 0x12, 0x67, 0x69, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, - 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x47, - 0x69, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x10, - 0x67, 0x69, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, - 0x1a, 0x52, 0x0a, 0x05, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x12, 0x35, 0x0a, 0x06, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x18, 0x01, 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, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, - 0x70, 0x6c, 0x61, 0x6e, 0x1a, 0x08, 0x0a, 0x06, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x1a, 0xb3, - 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x04, 0x70, 0x6c, - 0x61, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x34, 0x0a, - 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, + 0x73, 0x12, 0x37, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x12, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x8d, 0x01, 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, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, + 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, + 0x12, 0x18, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x1a, 0x36, 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, 0xb5, 0x02, 0x0a, 0x03, 0x41, + 0x70, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, + 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, + 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, + 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, + 0x61, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x75, 0x62, + 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x75, + 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x3a, 0x0a, 0x0b, 0x68, 0x65, 0x61, 0x6c, 0x74, + 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x07, 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, 0x12, 0x41, 0x0a, 0x0d, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x6c, + 0x65, 0x76, 0x65, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, 0x53, 0x68, 0x61, 0x72, + 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x0c, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, + 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 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, 0xf1, 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, 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x69, 0x6e, 0x73, 0x74, 0x61, + 0x6e, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x61, 0x69, 0x6c, 0x79, + 0x5f, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x64, 0x61, 0x69, + 0x6c, 0x79, 0x43, 0x6f, 0x73, 0x74, 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, 0xcb, 0x02, 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, 0xa3, 0x01, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, + 0x65, 0x12, 0x4c, 0x0a, 0x12, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x61, + 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x65, 0x6d, 0x70, + 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x11, 0x74, 0x65, + 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 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, + 0x90, 0x0d, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0xeb, 0x03, + 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, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, + 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, + 0x74, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x12, 0x48, 0x0a, 0x21, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, + 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1d, 0x77, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x4f, 0x69, 0x64, 0x63, + 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x1a, 0xad, 0x01, 0x0a, 0x06, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 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, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 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, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x5f, 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x1a, 0xeb, 0x02, 0x0a, 0x04, + 0x50, 0x6c, 0x61, 0x6e, 0x12, 0x35, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, + 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, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 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, 0x53, 0x0a, 0x15, 0x72, 0x69, 0x63, 0x68, 0x5f, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x13, 0x72, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, + 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x76, 0x61, 0x72, 0x69, + 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, + 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x76, + 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x4a, 0x0a, + 0x12, 0x67, 0x69, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x47, 0x69, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x10, 0x67, 0x69, 0x74, 0x41, 0x75, 0x74, 0x68, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x1a, 0x52, 0x0a, 0x05, 0x41, 0x70, 0x70, + 0x6c, 0x79, 0x12, 0x35, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 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, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6c, 0x61, + 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x1a, 0x08, 0x0a, + 0x06, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x1a, 0xb3, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x48, 0x00, + 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x34, 0x0a, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x18, + 0x02, 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, 0x41, 0x70, + 0x70, 0x6c, 0x79, 0x48, 0x00, 0x52, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x12, 0x37, 0x0a, 0x06, + 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x03, 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, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x48, 0x00, 0x52, 0x05, 0x61, 0x70, - 0x70, 0x6c, 0x79, 0x12, 0x37, 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x03, 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, 0xe9, 0x01, 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, 0x12, 0x3a, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, - 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, - 0x65, 0x72, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x2c, - 0x0a, 0x12, 0x67, 0x69, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x67, 0x69, 0x74, 0x41, - 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x12, 0x0a, 0x04, - 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, - 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, 0x3b, 0x0a, 0x0f, 0x41, 0x70, - 0x70, 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, - 0x05, 0x4f, 0x57, 0x4e, 0x45, 0x52, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x55, 0x54, 0x48, - 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x50, - 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x02, 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, + 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, 0xe9, 0x01, + 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, 0x12, 0x3a, 0x0a, 0x0a, 0x70, + 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, + 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0a, 0x70, 0x61, 0x72, + 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x67, 0x69, 0x74, 0x5f, 0x61, + 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x10, 0x67, 0x69, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 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, 0x3b, 0x0a, 0x0f, 0x41, 0x70, 0x70, 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, + 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x57, 0x4e, 0x45, 0x52, 0x10, + 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, + 0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x02, + 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 ( @@ -3090,7 +3190,7 @@ func file_provisionersdk_proto_provisioner_proto_rawDescGZIP() []byte { } var file_provisionersdk_proto_provisioner_proto_enumTypes = make([]protoimpl.EnumInfo, 6) -var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 32) +var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 33) var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{ (LogLevel)(0), // 0: provisioner.LogLevel (AppSharingLevel)(0), // 1: provisioner.AppSharingLevel @@ -3117,19 +3217,20 @@ var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{ (*Resource)(nil), // 22: provisioner.Resource (*Parse)(nil), // 23: provisioner.Parse (*Provision)(nil), // 24: provisioner.Provision - nil, // 25: provisioner.Agent.EnvEntry - (*Resource_Metadata)(nil), // 26: provisioner.Resource.Metadata - (*Parse_Request)(nil), // 27: provisioner.Parse.Request - (*Parse_Complete)(nil), // 28: provisioner.Parse.Complete - (*Parse_Response)(nil), // 29: provisioner.Parse.Response - (*Provision_Metadata)(nil), // 30: provisioner.Provision.Metadata - (*Provision_Config)(nil), // 31: provisioner.Provision.Config - (*Provision_Plan)(nil), // 32: provisioner.Provision.Plan - (*Provision_Apply)(nil), // 33: provisioner.Provision.Apply - (*Provision_Cancel)(nil), // 34: provisioner.Provision.Cancel - (*Provision_Request)(nil), // 35: provisioner.Provision.Request - (*Provision_Complete)(nil), // 36: provisioner.Provision.Complete - (*Provision_Response)(nil), // 37: provisioner.Provision.Response + (*Agent_Metadata)(nil), // 25: provisioner.Agent.Metadata + nil, // 26: provisioner.Agent.EnvEntry + (*Resource_Metadata)(nil), // 27: provisioner.Resource.Metadata + (*Parse_Request)(nil), // 28: provisioner.Parse.Request + (*Parse_Complete)(nil), // 29: provisioner.Parse.Complete + (*Parse_Response)(nil), // 30: provisioner.Parse.Response + (*Provision_Metadata)(nil), // 31: provisioner.Provision.Metadata + (*Provision_Config)(nil), // 32: provisioner.Provision.Config + (*Provision_Plan)(nil), // 33: provisioner.Provision.Plan + (*Provision_Apply)(nil), // 34: provisioner.Provision.Apply + (*Provision_Cancel)(nil), // 35: provisioner.Provision.Cancel + (*Provision_Request)(nil), // 36: provisioner.Provision.Request + (*Provision_Complete)(nil), // 37: provisioner.Provision.Complete + (*Provision_Response)(nil), // 38: provisioner.Provision.Response } var file_provisionersdk_proto_provisioner_proto_depIdxs = []int32{ 3, // 0: provisioner.ParameterSource.scheme:type_name -> provisioner.ParameterSource.Scheme @@ -3140,40 +3241,41 @@ var file_provisionersdk_proto_provisioner_proto_depIdxs = []int32{ 5, // 5: provisioner.ParameterSchema.validation_type_system:type_name -> provisioner.ParameterSchema.TypeSystem 12, // 6: provisioner.RichParameter.options:type_name -> provisioner.RichParameterOption 0, // 7: provisioner.Log.level:type_name -> provisioner.LogLevel - 25, // 8: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry + 26, // 8: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry 20, // 9: provisioner.Agent.apps:type_name -> provisioner.App - 21, // 10: provisioner.App.healthcheck:type_name -> provisioner.Healthcheck - 1, // 11: provisioner.App.sharing_level:type_name -> provisioner.AppSharingLevel - 19, // 12: provisioner.Resource.agents:type_name -> provisioner.Agent - 26, // 13: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata - 11, // 14: provisioner.Parse.Complete.template_variables:type_name -> provisioner.TemplateVariable - 10, // 15: provisioner.Parse.Complete.parameter_schemas:type_name -> provisioner.ParameterSchema - 16, // 16: provisioner.Parse.Response.log:type_name -> provisioner.Log - 28, // 17: provisioner.Parse.Response.complete:type_name -> provisioner.Parse.Complete - 2, // 18: provisioner.Provision.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition - 30, // 19: provisioner.Provision.Config.metadata:type_name -> provisioner.Provision.Metadata - 31, // 20: provisioner.Provision.Plan.config:type_name -> provisioner.Provision.Config - 9, // 21: provisioner.Provision.Plan.parameter_values:type_name -> provisioner.ParameterValue - 14, // 22: provisioner.Provision.Plan.rich_parameter_values:type_name -> provisioner.RichParameterValue - 15, // 23: provisioner.Provision.Plan.variable_values:type_name -> provisioner.VariableValue - 18, // 24: provisioner.Provision.Plan.git_auth_providers:type_name -> provisioner.GitAuthProvider - 31, // 25: provisioner.Provision.Apply.config:type_name -> provisioner.Provision.Config - 32, // 26: provisioner.Provision.Request.plan:type_name -> provisioner.Provision.Plan - 33, // 27: provisioner.Provision.Request.apply:type_name -> provisioner.Provision.Apply - 34, // 28: provisioner.Provision.Request.cancel:type_name -> provisioner.Provision.Cancel - 22, // 29: provisioner.Provision.Complete.resources:type_name -> provisioner.Resource - 13, // 30: provisioner.Provision.Complete.parameters:type_name -> provisioner.RichParameter - 16, // 31: provisioner.Provision.Response.log:type_name -> provisioner.Log - 36, // 32: provisioner.Provision.Response.complete:type_name -> provisioner.Provision.Complete - 27, // 33: provisioner.Provisioner.Parse:input_type -> provisioner.Parse.Request - 35, // 34: provisioner.Provisioner.Provision:input_type -> provisioner.Provision.Request - 29, // 35: provisioner.Provisioner.Parse:output_type -> provisioner.Parse.Response - 37, // 36: provisioner.Provisioner.Provision:output_type -> provisioner.Provision.Response - 35, // [35:37] is the sub-list for method output_type - 33, // [33:35] is the sub-list for method input_type - 33, // [33:33] is the sub-list for extension type_name - 33, // [33:33] is the sub-list for extension extendee - 0, // [0:33] is the sub-list for field type_name + 25, // 10: provisioner.Agent.metadata:type_name -> provisioner.Agent.Metadata + 21, // 11: provisioner.App.healthcheck:type_name -> provisioner.Healthcheck + 1, // 12: provisioner.App.sharing_level:type_name -> provisioner.AppSharingLevel + 19, // 13: provisioner.Resource.agents:type_name -> provisioner.Agent + 27, // 14: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata + 11, // 15: provisioner.Parse.Complete.template_variables:type_name -> provisioner.TemplateVariable + 10, // 16: provisioner.Parse.Complete.parameter_schemas:type_name -> provisioner.ParameterSchema + 16, // 17: provisioner.Parse.Response.log:type_name -> provisioner.Log + 29, // 18: provisioner.Parse.Response.complete:type_name -> provisioner.Parse.Complete + 2, // 19: provisioner.Provision.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition + 31, // 20: provisioner.Provision.Config.metadata:type_name -> provisioner.Provision.Metadata + 32, // 21: provisioner.Provision.Plan.config:type_name -> provisioner.Provision.Config + 9, // 22: provisioner.Provision.Plan.parameter_values:type_name -> provisioner.ParameterValue + 14, // 23: provisioner.Provision.Plan.rich_parameter_values:type_name -> provisioner.RichParameterValue + 15, // 24: provisioner.Provision.Plan.variable_values:type_name -> provisioner.VariableValue + 18, // 25: provisioner.Provision.Plan.git_auth_providers:type_name -> provisioner.GitAuthProvider + 32, // 26: provisioner.Provision.Apply.config:type_name -> provisioner.Provision.Config + 33, // 27: provisioner.Provision.Request.plan:type_name -> provisioner.Provision.Plan + 34, // 28: provisioner.Provision.Request.apply:type_name -> provisioner.Provision.Apply + 35, // 29: provisioner.Provision.Request.cancel:type_name -> provisioner.Provision.Cancel + 22, // 30: provisioner.Provision.Complete.resources:type_name -> provisioner.Resource + 13, // 31: provisioner.Provision.Complete.parameters:type_name -> provisioner.RichParameter + 16, // 32: provisioner.Provision.Response.log:type_name -> provisioner.Log + 37, // 33: provisioner.Provision.Response.complete:type_name -> provisioner.Provision.Complete + 28, // 34: provisioner.Provisioner.Parse:input_type -> provisioner.Parse.Request + 36, // 35: provisioner.Provisioner.Provision:input_type -> provisioner.Provision.Request + 30, // 36: provisioner.Provisioner.Parse:output_type -> provisioner.Parse.Response + 38, // 37: provisioner.Provisioner.Provision:output_type -> provisioner.Provision.Response + 36, // [36:38] is the sub-list for method output_type + 34, // [34:36] is the sub-list for method input_type + 34, // [34:34] is the sub-list for extension type_name + 34, // [34:34] is the sub-list for extension extendee + 0, // [0:34] is the sub-list for field type_name } func init() { file_provisionersdk_proto_provisioner_proto_init() } @@ -3410,8 +3512,8 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Resource_Metadata); i { + file_provisionersdk_proto_provisioner_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Agent_Metadata); i { case 0: return &v.state case 1: @@ -3423,7 +3525,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Parse_Request); i { + switch v := v.(*Resource_Metadata); i { case 0: return &v.state case 1: @@ -3435,7 +3537,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Parse_Complete); i { + switch v := v.(*Parse_Request); i { case 0: return &v.state case 1: @@ -3447,7 +3549,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Parse_Response); i { + switch v := v.(*Parse_Complete); i { case 0: return &v.state case 1: @@ -3459,7 +3561,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Provision_Metadata); i { + switch v := v.(*Parse_Response); i { case 0: return &v.state case 1: @@ -3471,7 +3573,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Provision_Config); i { + switch v := v.(*Provision_Metadata); i { case 0: return &v.state case 1: @@ -3483,7 +3585,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Provision_Plan); i { + switch v := v.(*Provision_Config); i { case 0: return &v.state case 1: @@ -3495,7 +3597,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Provision_Apply); i { + switch v := v.(*Provision_Plan); i { case 0: return &v.state case 1: @@ -3507,7 +3609,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Provision_Cancel); i { + switch v := v.(*Provision_Apply); i { case 0: return &v.state case 1: @@ -3519,7 +3621,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Provision_Request); i { + switch v := v.(*Provision_Cancel); i { case 0: return &v.state case 1: @@ -3531,7 +3633,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Provision_Complete); i { + switch v := v.(*Provision_Request); i { case 0: return &v.state case 1: @@ -3543,6 +3645,18 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Provision_Complete); 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[32].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Provision_Response); i { case 0: return &v.state @@ -3559,16 +3673,16 @@ func file_provisionersdk_proto_provisioner_proto_init() { (*Agent_Token)(nil), (*Agent_InstanceId)(nil), } - file_provisionersdk_proto_provisioner_proto_msgTypes[23].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[24].OneofWrappers = []interface{}{ (*Parse_Response_Log)(nil), (*Parse_Response_Complete)(nil), } - file_provisionersdk_proto_provisioner_proto_msgTypes[29].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[30].OneofWrappers = []interface{}{ (*Provision_Request_Plan)(nil), (*Provision_Request_Apply)(nil), (*Provision_Request_Cancel)(nil), } - file_provisionersdk_proto_provisioner_proto_msgTypes[31].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[32].OneofWrappers = []interface{}{ (*Provision_Response_Log)(nil), (*Provision_Response_Complete)(nil), } @@ -3578,7 +3692,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_provisionersdk_proto_provisioner_proto_rawDesc, NumEnums: 6, - NumMessages: 32, + NumMessages: 33, NumExtensions: 0, NumServices: 1, }, diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index 135c616d35f14..5737ade95b7fe 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -127,6 +127,13 @@ message GitAuthProvider { // Agent represents a running agent on the workspace. message Agent { + message Metadata { + string key = 1; + string display_name = 2; + string script = 3; + int64 interval = 4; + int64 timeout = 5; + } string id = 1; string name = 2; map env = 3; @@ -146,6 +153,7 @@ message Agent { int32 startup_script_timeout_seconds = 15; string shutdown_script = 16; int32 shutdown_script_timeout_seconds = 17; + repeated Metadata metadata = 18; } enum AppSharingLevel { diff --git a/site/package.json b/site/package.json index 3e9e4b7df7044..a47bd32efad13 100644 --- a/site/package.json +++ b/site/package.json @@ -138,6 +138,8 @@ "prettier": "2.8.1", "resize-observer": "1.0.4", "semver": "7.3.7", + "storybook-addon-mock": "^3.2.0", + "storybook-react-context": "^0.6.0", "typescript": "4.8.2" }, "browserslist": [ diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 190ec4baae23b..b686b2936594e 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1046,6 +1046,18 @@ const getMissingParameters = ( return missingParameters } +/** + * + * @param agentId + * @returns An EventSource that emits agent metadata event objects (ServerSentEvent) + */ +export const watchAgentMetadata = (agentId: string): EventSource => { + return new EventSource( + `${location.protocol}//${location.host}/api/v2/workspaceagents/${agentId}/watch-metadata`, + { withCredentials: true }, + ) +} + export const watchBuildLogs = ( versionId: string, onMessage: (log: TypesGen.ProvisionerJobLog) => void, diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index f1b40dddc60cd..1bb3a823548b4 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1086,6 +1086,29 @@ export interface WorkspaceAgentListeningPortsResponse { readonly ports: WorkspaceAgentListeningPort[] } +// From codersdk/workspaceagents.go +export interface WorkspaceAgentMetadata { + readonly result: WorkspaceAgentMetadataResult + readonly description: WorkspaceAgentMetadataDescription +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentMetadataDescription { + readonly display_name: string + readonly key: string + readonly script: string + readonly interval: number + readonly timeout: number +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentMetadataResult { + readonly collected_at: string + readonly age: number + readonly value: string + readonly error: string +} + // From codersdk/workspaceagents.go export interface WorkspaceAgentStartupLog { readonly id: number diff --git a/site/src/components/Resources/AgentMetadata.stories.tsx b/site/src/components/Resources/AgentMetadata.stories.tsx new file mode 100644 index 0000000000000..8b6fc80a9f683 --- /dev/null +++ b/site/src/components/Resources/AgentMetadata.stories.tsx @@ -0,0 +1,107 @@ +import { Story } from "@storybook/react" +import { + WorkspaceAgentMetadataDescription, + WorkspaceAgentMetadataResult, +} from "api/typesGenerated" +import { AgentMetadataView, AgentMetadataViewProps } from "./AgentMetadata" + +export default { + title: "components/AgentMetadata", + component: AgentMetadataView, +} + +const Template: Story = (args) => ( + +) + +const resultDefaults: WorkspaceAgentMetadataResult = { + collected_at: "2021-05-05T00:00:00Z", + error: "", + value: "defvalue", + age: 5, +} + +const descriptionDefaults: WorkspaceAgentMetadataDescription = { + display_name: "DisPlay", + key: "defkey", + interval: 10, + timeout: 10, + script: "some command", +} + +export const Example = Template.bind({}) +Example.args = { + metadata: [ + { + result: { + ...resultDefaults, + value: "110%", + }, + description: { + ...descriptionDefaults, + display_name: "CPU", + key: "CPU", + }, + }, + { + result: { + ...resultDefaults, + value: "50GB", + }, + description: { + ...descriptionDefaults, + display_name: "Memory", + key: "Memory", + }, + }, + { + result: { + ...resultDefaults, + value: "cant see it", + age: 300, + }, + description: { + ...descriptionDefaults, + interval: 5, + display_name: "Stale", + key: "stale", + }, + }, + { + result: { + ...resultDefaults, + value: "oops", + error: "fatal error", + }, + description: { + ...descriptionDefaults, + display_name: "Error", + }, + }, + { + result: { + ...resultDefaults, + value: "oops", + error: "fatal error", + }, + description: { + ...descriptionDefaults, + display_name: "Error", + key: "stale", + }, + }, + { + result: { + ...resultDefaults, + value: "", + collected_at: "0001-01-01T00:00:00Z", + age: 1000000, + }, + description: { + ...descriptionDefaults, + display_name: "Never loads", + key: "nloads", + }, + }, + ], +} diff --git a/site/src/components/Resources/AgentMetadata.tsx b/site/src/components/Resources/AgentMetadata.tsx new file mode 100644 index 0000000000000..df70543a30351 --- /dev/null +++ b/site/src/components/Resources/AgentMetadata.tsx @@ -0,0 +1,319 @@ +import Popover from "@material-ui/core/Popover" +import CircularProgress from "@material-ui/core/CircularProgress" +import makeStyles from "@material-ui/core/styles/makeStyles" +import { watchAgentMetadata } from "api/api" +import { WorkspaceAgent, WorkspaceAgentMetadata } from "api/typesGenerated" +import { CodeExample } from "components/CodeExample/CodeExample" +import { Stack } from "components/Stack/Stack" +import { + HelpTooltipText, + HelpTooltipTitle, +} from "components/Tooltips/HelpTooltip" +import dayjs from "dayjs" +import { + createContext, + FC, + PropsWithChildren, + useContext, + useEffect, + useRef, + useState, +} from "react" + +export const WatchAgentMetadataContext = createContext(watchAgentMetadata) + +const MetadataItemValue: FC< + PropsWithChildren<{ item: WorkspaceAgentMetadata }> +> = ({ item, children }) => { + const [isOpen, setIsOpen] = useState(false) + const anchorRef = useRef(null) + const styles = useStyles() + return ( + <> +
setIsOpen(true)} + role="presentation" + > + {children} +
+ setIsOpen(false)} + PaperProps={{ + onMouseEnter: () => setIsOpen(true), + onMouseLeave: () => setIsOpen(false), + }} + classes={{ paper: styles.metadataPopover }} + > + {item.description.display_name} + {item.result.value.length > 0 && ( + <> + Last result: + + + + + )} + {item.result.error.length > 0 && ( + <> + Last error: + + + + + )} + + + ) +} + +const MetadataItem: FC<{ item: WorkspaceAgentMetadata }> = ({ item }) => { + const styles = useStyles() + + const [isOpen, setIsOpen] = useState(false) + + const labelAnchorRef = useRef(null) + + if (item.result === undefined) { + throw new Error("Metadata item result is undefined") + } + if (item.description === undefined) { + throw new Error("Metadata item description is undefined") + } + + const staleThreshold = Math.max( + item.description.interval + item.description.timeout * 2, + 5, + ) + + const status: "stale" | "valid" | "loading" = (() => { + const year = dayjs(item.result.collected_at).year() + if (year <= 1970 || isNaN(year)) { + return "loading" + } + if (item.result.age > staleThreshold) { + return "stale" + } + return "valid" + })() + + // Stale data is as good as no data. Plus, we want to build confidence in our + // users that what's shown is real. If times aren't correctly synced this + // could be buggy. But, how common is that anyways? + const value = + status === "stale" || status === "loading" ? ( + + ) : ( +
+ {item.result.value} +
+ ) + + const updatesInSeconds = -(item.description.interval - item.result.age) + + return ( + <> +
+
setIsOpen(true)} + // onMouseLeave={() => setIsOpen(false)} + role="presentation" + ref={labelAnchorRef} + > + {item.description.display_name} +
+ {value} +
+ setIsOpen(false)} + PaperProps={{ + onMouseEnter: () => setIsOpen(true), + onMouseLeave: () => setIsOpen(false), + }} + classes={{ paper: styles.metadataPopover }} + > + {item.description.display_name} + {status === "stale" ? ( + + This item is now stale because the agent hasn{"'"}t reported a new + value in {dayjs.duration(item.result.age, "s").humanize()}. + + ) : ( + <> + )} + {status === "valid" ? ( + + The agent collected this value{" "} + {dayjs.duration(item.result.age, "s").humanize()} ago and will + update it in{" "} + {dayjs.duration(Math.min(updatesInSeconds, 0), "s").humanize()}. + + ) : ( + <> + )} + {status === "loading" ? ( + + This value is loading for the first time... + + ) : ( + <> + )} + + This value is produced by the following script: + + + + + + + ) +} + +export interface AgentMetadataViewProps { + metadata: WorkspaceAgentMetadata[] +} + +export const AgentMetadataView: FC = ({ metadata }) => { + const styles = useStyles() + if (metadata.length === 0) { + return <> + } + return ( + +
+ {metadata.map((m) => { + if (m.description === undefined) { + throw new Error("Metadata item description is undefined") + } + return + })} +
+
+ ) +} + +export const AgentMetadata: FC<{ + agent: WorkspaceAgent +}> = ({ agent }) => { + const [metadata, setMetadata] = useState< + WorkspaceAgentMetadata[] | undefined + >(undefined) + + const watchAgentMetadata = useContext(WatchAgentMetadataContext) + + useEffect(() => { + const source = watchAgentMetadata(agent.id) + + source.onerror = (e) => { + console.error("received error in watch stream", e) + } + source.addEventListener("data", (e) => { + const data = JSON.parse(e.data) + setMetadata(data) + }) + return () => { + source.close() + } + }, [agent.id, watchAgentMetadata]) + + if (metadata === undefined) { + return + } + + return +} + +// These are more or less copied from +// site/src/components/Resources/ResourceCard.tsx +const useStyles = makeStyles((theme) => ({ + metadataStack: { + border: `2px dashed ${theme.palette.divider}`, + borderRadius: theme.shape.borderRadius, + width: "100%", + }, + metadataHeader: { + padding: "8px", + display: "grid", + gridTemplateColumns: "repeat(4, minmax(0, 1fr))", + gap: theme.spacing(5), + rowGap: theme.spacing(3), + }, + + metadata: { + fontSize: 16, + }, + + metadataLabel: { + fontSize: 12, + color: theme.palette.text.secondary, + textOverflow: "ellipsis", + overflow: "hidden", + whiteSpace: "nowrap", + fontWeight: "bold", + }, + + metadataValue: { + textOverflow: "ellipsis", + overflow: "hidden", + whiteSpace: "nowrap", + }, + + metadataValueSuccess: { + color: theme.palette.success.light, + }, + metadataValueError: { + color: theme.palette.error.main, + }, + + metadataPopover: { + marginTop: theme.spacing(0.5), + padding: theme.spacing(2.5), + color: theme.palette.text.secondary, + pointerEvents: "auto", + maxWidth: "480px", + + "& .MuiButton-root": { + padding: theme.spacing(1, 2), + borderRadius: 0, + border: 0, + + "&:hover": { + background: theme.palette.action.hover, + }, + }, + }, +})) diff --git a/site/src/components/Resources/AgentRow.tsx b/site/src/components/Resources/AgentRow.tsx index 00d84793fb2d4..c00ca2a1793a0 100644 --- a/site/src/components/Resources/AgentRow.tsx +++ b/site/src/components/Resources/AgentRow.tsx @@ -35,6 +35,7 @@ import { SSHButton } from "../SSHButton/SSHButton" import { Stack } from "../Stack/Stack" import { TerminalLink } from "../TerminalLink/TerminalLink" import { AgentLatency } from "./AgentLatency" +import { AgentMetadata } from "./AgentMetadata" import { AgentStatus } from "./AgentStatus" import { AgentVersion } from "./AgentVersion" @@ -169,178 +170,202 @@ export const AgentRow: FC = ({ - -
- -
-
-
{agent.name}
- - {agent.operating_system} +
+ +
+ + + +
+
{agent.name}
+ + + {agent.operating_system} + - - - + + + - + - - - - + + + + - - {t("unableToConnect")} - + + {t("unableToConnect")} + + +
- {hasStartupFeatures && ( - + {showApps && agent.status === "connected" && ( + <> + {agent.apps.map((app) => ( + + ))} + + + {!hideSSHButton && ( + + )} + {!hideVSCodeDesktopButton && ( + + )} + {applicationsHost !== undefined && + applicationsHost !== "" && ( + + )} + + )} + {showApps && agent.status === "connecting" && ( + <> + + + + )} + +
+ + {hasStartupFeatures && ( + + { + setShowStartupLogs(!showStartupLogs) + }} > + {showStartupLogs ? ( + + ) : ( + + )} + {showStartupLogs ? "Hide" : "Show"} Startup Logs + + + {agent.startup_script && ( { - setShowStartupLogs(!showStartupLogs) + setStartupScriptOpen(!startupScriptOpen) }} > - {showStartupLogs ? ( - - ) : ( - - )} - {showStartupLogs ? "Hide" : "Show"} Startup Logs + + View Startup Script + )} - {agent.startup_script && ( - { - setStartupScriptOpen(!startupScriptOpen) + setStartupScriptOpen(false)} + anchorEl={startupScriptAnchorRef.current} + anchorOrigin={{ + vertical: "bottom", + horizontal: "left", + }} + transformOrigin={{ + vertical: "top", + horizontal: "left", + }} + > +
+ - - View Startup Script - - )} - - setStartupScriptOpen(false)} - anchorEl={startupScriptAnchorRef.current} - anchorOrigin={{ - vertical: "bottom", - horizontal: "left", - }} - transformOrigin={{ - vertical: "top", - horizontal: "left", - }} - > -
- - {agent.startup_script || ""} - -
-
- - )} -
-
- - - {showApps && agent.status === "connected" && ( - <> - {agent.apps.map((app) => ( - - ))} - - - {!hideSSHButton && ( - - )} - {!hideVSCodeDesktopButton && ( - - )} - {applicationsHost !== undefined && applicationsHost !== "" && ( - - )} - - )} - {showApps && agent.status === "connecting" && ( - <> - - - + {agent.startup_script || ""} + +
+ +
)}
- {showStartupLogs && ( {({ width }) => ( diff --git a/site/src/components/Workspace/Workspace.stories.tsx b/site/src/components/Workspace/Workspace.stories.tsx index a6fb225a20a7d..30bf79507f3e5 100644 --- a/site/src/components/Workspace/Workspace.stories.tsx +++ b/site/src/components/Workspace/Workspace.stories.tsx @@ -1,13 +1,25 @@ import { action } from "@storybook/addon-actions" import { Story } from "@storybook/react" +import { WatchAgentMetadataContext } from "components/Resources/AgentMetadata" import { ProvisionerJobLog } from "api/typesGenerated" import * as Mocks from "../../testHelpers/entities" import { Workspace, WorkspaceErrors, WorkspaceProps } from "./Workspace" +import { withReactContext } from "storybook-react-context" +import EventSource from "eventsourcemock" export default { title: "components/Workspace", component: Workspace, argTypes: {}, + decorators: [ + withReactContext({ + Context: WatchAgentMetadataContext, + initialState: (_: string): EventSource => { + // Need Bruno's help here. + return new EventSource() + }, + }), + ], } const Template: Story = (args) => diff --git a/site/yarn.lock b/site/yarn.lock index 6e81f10bab216..58e74b6699818 100644 --- a/site/yarn.lock +++ b/site/yarn.lock @@ -2219,6 +2219,23 @@ global "^4.4.0" regenerator-runtime "^0.13.7" +"@storybook/addons@^6.3.6": + version "6.5.16" + resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.5.16.tgz#07e8f2205f86fa4c9dada719e3e096cb468e3cdd" + integrity sha512-p3DqQi+8QRL5k7jXhXmJZLsE/GqHqyY6PcoA1oNTJr0try48uhTGUOYkgzmqtDaa/qPFO5LP+xCPzZXckGtquQ== + dependencies: + "@storybook/api" "6.5.16" + "@storybook/channels" "6.5.16" + "@storybook/client-logger" "6.5.16" + "@storybook/core-events" "6.5.16" + "@storybook/csf" "0.0.2--canary.4566f4d.1" + "@storybook/router" "6.5.16" + "@storybook/theming" "6.5.16" + "@types/webpack-env" "^1.16.0" + core-js "^3.8.2" + global "^4.4.0" + regenerator-runtime "^0.13.7" + "@storybook/api@6.5.12": version "6.5.12" resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.5.12.tgz#7cc82087fc9298be03f15bf4ab9c4aab294b3bac" @@ -2242,6 +2259,29 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" +"@storybook/api@6.5.16": + version "6.5.16" + resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.5.16.tgz#897915b76de05587fd702951d5d836f708043662" + integrity sha512-HOsuT8iomqeTMQJrRx5U8nsC7lJTwRr1DhdD0SzlqL4c80S/7uuCy4IZvOt4sYQjOzW5fOo/kamcoBXyLproTA== + dependencies: + "@storybook/channels" "6.5.16" + "@storybook/client-logger" "6.5.16" + "@storybook/core-events" "6.5.16" + "@storybook/csf" "0.0.2--canary.4566f4d.1" + "@storybook/router" "6.5.16" + "@storybook/semver" "^7.3.2" + "@storybook/theming" "6.5.16" + core-js "^3.8.2" + fast-deep-equal "^3.1.3" + global "^4.4.0" + lodash "^4.17.21" + memoizerific "^1.11.3" + regenerator-runtime "^0.13.7" + store2 "^2.12.0" + telejson "^6.0.8" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + "@storybook/api@6.5.9": version "6.5.9" resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.5.9.tgz#303733214c9de0422d162f7c54ae05d088b89bf9" @@ -2351,6 +2391,15 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" +"@storybook/channels@6.5.16": + version "6.5.16" + resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.5.16.tgz#3fb9a3b5666ecb951a2d0cf8b0699b084ef2d3c6" + integrity sha512-VylzaWQZaMozEwZPJdyJoz+0jpDa8GRyaqu9TGG6QGv+KU5POoZaGLDkRE7TzWkyyP0KQLo80K99MssZCpgSeg== + dependencies: + core-js "^3.8.2" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + "@storybook/channels@6.5.9": version "6.5.9" resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.5.9.tgz#abfab89a6587a2688e9926d4aafeb11c9d8b2e79" @@ -2394,6 +2443,14 @@ core-js "^3.8.2" global "^4.4.0" +"@storybook/client-logger@6.5.16": + version "6.5.16" + resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.5.16.tgz#955cc46b389e7151c9eb1585a75e6a0605af61a1" + integrity sha512-pxcNaCj3ItDdicPTXTtmYJE3YC1SjxFrBmHcyrN+nffeNyiMuViJdOOZzzzucTUG0wcOOX8jaSyak+nnHg5H1Q== + dependencies: + core-js "^3.8.2" + global "^4.4.0" + "@storybook/client-logger@6.5.9": version "6.5.9" resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.5.9.tgz#dc1669abe8c45af1cc38f74c6f4b15ff33e63014" @@ -2521,6 +2578,13 @@ dependencies: core-js "^3.8.2" +"@storybook/core-events@6.5.16": + version "6.5.16" + resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.5.16.tgz#b1c265dac755007dae172d9d4b72656c9e5d7bb3" + integrity sha512-qMZQwmvzpH5F2uwNUllTPg6eZXr2OaYZQRRN8VZJiuorZzDNdAFmiVWMWdkThwmyLEJuQKXxqCL8lMj/7PPM+g== + dependencies: + core-js "^3.8.2" + "@storybook/core-events@6.5.9": version "6.5.9" resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.5.9.tgz#5b0783c7d22a586c0f5e927a61fe1b1223e19637" @@ -2790,6 +2854,17 @@ qs "^6.10.0" regenerator-runtime "^0.13.7" +"@storybook/router@6.5.16": + version "6.5.16" + resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.5.16.tgz#28fb4d34e8219351a40bee1fc94dcacda6e1bd8b" + integrity sha512-ZgeP8a5YV/iuKbv31V8DjPxlV4AzorRiR8OuSt/KqaiYXNXlOoQDz/qMmiNcrshrfLpmkzoq7fSo4T8lWo2UwQ== + dependencies: + "@storybook/client-logger" "6.5.16" + core-js "^3.8.2" + memoizerific "^1.11.3" + qs "^6.10.0" + regenerator-runtime "^0.13.7" + "@storybook/router@6.5.9": version "6.5.9" resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.5.9.tgz#4740248f8517425b2056273fb366ace8a17c65e8" @@ -2874,6 +2949,16 @@ memoizerific "^1.11.3" regenerator-runtime "^0.13.7" +"@storybook/theming@6.5.16": + version "6.5.16" + resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.5.16.tgz#b999bdb98945b605b93b9dfdf7408535b701e2aa" + integrity sha512-hNLctkjaYLRdk1+xYTkC1mg4dYz2wSv6SqbLpcKMbkPHTE0ElhddGPHQqB362md/w9emYXNkt1LSMD8Xk9JzVQ== + dependencies: + "@storybook/client-logger" "6.5.16" + core-js "^3.8.2" + memoizerific "^1.11.3" + regenerator-runtime "^0.13.7" + "@storybook/theming@6.5.9": version "6.5.9" resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.5.9.tgz#13f60a3a3cd73ceb5caf9f188e1627e79f1891aa" @@ -8853,7 +8938,7 @@ is-plain-obj@^4.0.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== -is-plain-object@5.0.0: +is-plain-object@5.0.0, is-plain-object@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== @@ -11043,6 +11128,11 @@ mock-socket@^9.1.0: resolved "https://registry.yarnpkg.com/mock-socket/-/mock-socket-9.2.1.tgz#cc9c0810aa4d0afe02d721dcb2b7e657c00e2282" integrity sha512-aw9F9T9G2zpGipLLhSNh6ZpgUyUl4frcVmRN08uE1NWPWg43Wx6+sGPDbQ7E5iFZZDJW5b5bypMeAEHqTbIFag== +mock-xmlhttprequest@^7.0.3: + version "7.0.4" + resolved "https://registry.yarnpkg.com/mock-xmlhttprequest/-/mock-xmlhttprequest-7.0.4.tgz#5e188da009cf46900e522f690cbea8d26274a872" + integrity sha512-hA0fIHy/74p5DE0rdmrpU0sV1U+gnWTcgShWequGRLy0L1eT+zY0ozFukawpLaxMwIA+orRcqFRElYwT+5p81A== + monaco-editor@0.34.1: version "0.34.1" resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.34.1.tgz#1b75c4ad6bc4c1f9da656d740d98e0b850a22f87" @@ -12590,6 +12680,14 @@ react@18.2.0: dependencies: loose-envify "^1.1.0" +react@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" + integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + reactcss@^1.2.0: version "1.2.3" resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.2.3.tgz#c00013875e557b1cf0dfd9a368a1c3dab3b548dd" @@ -13638,6 +13736,24 @@ store2@^2.12.0: resolved "https://registry.yarnpkg.com/store2/-/store2-2.14.2.tgz#56138d200f9fe5f582ad63bc2704dbc0e4a45068" integrity sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w== +storybook-addon-mock@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/storybook-addon-mock/-/storybook-addon-mock-3.2.0.tgz#5832b1e49ff39ffab7a0ae8ec7de8bfdb8ddea45" + integrity sha512-LaggsF/6Lt0AyHiotIEVQpwKfIiZ3KsNqtdXKVnIdOetjaD7GaOQeX0jIZiZUFX/i6QLmMuNoXFngqqkdVtfSg== + dependencies: + mock-xmlhttprequest "^7.0.3" + path-to-regexp "^6.2.0" + polished "^4.2.2" + +storybook-react-context@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/storybook-react-context/-/storybook-react-context-0.6.0.tgz#06c7b48dc95f4619cf12e59429305fbd6f2b1373" + integrity sha512-6IOUbSoC1WW68x8zQBEh8tZsVXjEvOBSJSOhkaD9o8IF9caIg/o1jnwuGibdyAd47ARN6g95O0N0vFBjXcB7pA== + dependencies: + "@storybook/addons" "^6.3.6" + is-plain-object "^5.0.0" + react "^17.0.2" + stream-browserify@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b"