diff --git a/.github/workflows/contrib.yaml b/.github/workflows/contrib.yaml index 48d93b31fd..6a89324381 100644 --- a/.github/workflows/contrib.yaml +++ b/.github/workflows/contrib.yaml @@ -84,7 +84,7 @@ jobs: repo: context.repo.repo, } - if (action === "opened" || action === "reopened") { + if (action === "opened" || action === "reopened" || action === "ready_for_review") { if (isBreakingTitle && !labels.includes(releaseLabels.breaking)) { console.log('Add "%s" label', releaseLabels.breaking) await github.rest.issues.addLabels({ diff --git a/agent/agent.go b/agent/agent.go index c98585f063..b18f9b3069 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -376,7 +376,6 @@ func (a *agent) collectMetadata(ctx context.Context, md codersdk.WorkspaceAgentM // Important: if the command times out, we may see a misleading error like // "exit status 1", so it's important to include the context error. err = errors.Join(err, ctx.Err()) - if err != nil { result.Error = fmt.Sprintf("run cmd: %+v", err) } diff --git a/agent/agent_test.go b/agent/agent_test.go index cfc5ebb419..834e0a3e68 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -138,7 +138,7 @@ func TestAgent_Stats_Magic(t *testing.T) { defer sshClient.Close() session, err := sshClient.NewSession() require.NoError(t, err) - session.Setenv(agentssh.MagicSessionTypeEnvironmentVariable, agentssh.MagicSessionTypeVSCode) + session.Setenv(agentssh.MagicSessionTypeEnvironmentVariable, string(agentssh.MagicSessionTypeVSCode)) defer session.Close() command := "sh -c 'echo $" + agentssh.MagicSessionTypeEnvironmentVariable + "'" @@ -165,7 +165,7 @@ func TestAgent_Stats_Magic(t *testing.T) { defer sshClient.Close() session, err := sshClient.NewSession() require.NoError(t, err) - session.Setenv(agentssh.MagicSessionTypeEnvironmentVariable, agentssh.MagicSessionTypeVSCode) + session.Setenv(agentssh.MagicSessionTypeEnvironmentVariable, string(agentssh.MagicSessionTypeVSCode)) defer session.Close() stdin, err := session.StdinPipe() require.NoError(t, err) diff --git a/agent/agentssh/agentssh.go b/agent/agentssh/agentssh.go index d17e9cd761..0f7d0adadc 100644 --- a/agent/agentssh/agentssh.go +++ b/agent/agentssh/agentssh.go @@ -26,6 +26,7 @@ import ( "github.com/spf13/afero" "go.uber.org/atomic" gossh "golang.org/x/crypto/ssh" + "golang.org/x/exp/slices" "golang.org/x/xerrors" "cdr.dev/slog" @@ -42,14 +43,6 @@ const ( // unlikely to shadow other exit codes, which are typically 1, 2, 3, etc. MagicSessionErrorCode = 229 - // MagicSessionTypeEnvironmentVariable is used to track the purpose behind an SSH connection. - // This is stripped from any commands being executed, and is counted towards connection stats. - MagicSessionTypeEnvironmentVariable = "CODER_SSH_SESSION_TYPE" - // MagicSessionTypeVSCode is set in the SSH config by the VS Code extension to identify itself. - MagicSessionTypeVSCode = "vscode" - // MagicSessionTypeJetBrains is set in the SSH config by the JetBrains - // extension to identify itself. - MagicSessionTypeJetBrains = "jetbrains" // MagicProcessCmdlineJetBrains is a string in a process's command line that // uniquely identifies it as JetBrains software. MagicProcessCmdlineJetBrains = "idea.vendor.name=JetBrains" @@ -60,6 +53,29 @@ const ( BlockedFileTransferErrorMessage = "File transfer has been disabled." ) +// MagicSessionType is a type that represents the type of session that is being +// established. +type MagicSessionType string + +const ( + // MagicSessionTypeEnvironmentVariable is used to track the purpose behind an SSH connection. + // This is stripped from any commands being executed, and is counted towards connection stats. + MagicSessionTypeEnvironmentVariable = "CODER_SSH_SESSION_TYPE" +) + +// MagicSessionType enums. +const ( + // MagicSessionTypeUnknown means the session type could not be determined. + MagicSessionTypeUnknown MagicSessionType = "unknown" + // MagicSessionTypeSSH is the default session type. + MagicSessionTypeSSH MagicSessionType = "ssh" + // MagicSessionTypeVSCode is set in the SSH config by the VS Code extension to identify itself. + MagicSessionTypeVSCode MagicSessionType = "vscode" + // MagicSessionTypeJetBrains is set in the SSH config by the JetBrains + // extension to identify itself. + MagicSessionTypeJetBrains MagicSessionType = "jetbrains" +) + // BlockedFileTransferCommands contains a list of restricted file transfer commands. var BlockedFileTransferCommands = []string{"nc", "rsync", "scp", "sftp"} @@ -255,14 +271,42 @@ func (s *Server) ConnStats() ConnStats { } } +func extractMagicSessionType(env []string) (magicType MagicSessionType, rawType string, filteredEnv []string) { + for _, kv := range env { + if !strings.HasPrefix(kv, MagicSessionTypeEnvironmentVariable) { + continue + } + + rawType = strings.TrimPrefix(kv, MagicSessionTypeEnvironmentVariable+"=") + // Keep going, we'll use the last instance of the env. + } + + // Always force lowercase checking to be case-insensitive. + switch MagicSessionType(strings.ToLower(rawType)) { + case MagicSessionTypeVSCode: + magicType = MagicSessionTypeVSCode + case MagicSessionTypeJetBrains: + magicType = MagicSessionTypeJetBrains + case "", MagicSessionTypeSSH: + magicType = MagicSessionTypeSSH + default: + magicType = MagicSessionTypeUnknown + } + + return magicType, rawType, slices.DeleteFunc(env, func(kv string) bool { + return strings.HasPrefix(kv, MagicSessionTypeEnvironmentVariable+"=") + }) +} + func (s *Server) sessionHandler(session ssh.Session) { ctx := session.Context() + id := uuid.New() logger := s.logger.With( slog.F("remote_addr", session.RemoteAddr()), slog.F("local_addr", session.LocalAddr()), // Assigning a random uuid for each session is useful for tracking // logs for the same ssh session. - slog.F("id", uuid.NewString()), + slog.F("id", id.String()), ) logger.Info(ctx, "handling ssh session") @@ -274,16 +318,21 @@ func (s *Server) sessionHandler(session ssh.Session) { } defer s.trackSession(session, false) - extraEnv := make([]string, 0) - x11, hasX11 := session.X11() - if hasX11 { - display, handled := s.x11Handler(session.Context(), x11) - if !handled { - _ = session.Exit(1) - logger.Error(ctx, "x11 handler failed") - return - } - extraEnv = append(extraEnv, fmt.Sprintf("DISPLAY=localhost:%d.%d", display, x11.ScreenNumber)) + env := session.Environ() + magicType, magicTypeRaw, env := extractMagicSessionType(env) + + switch magicType { + case MagicSessionTypeVSCode: + s.connCountVSCode.Add(1) + defer s.connCountVSCode.Add(-1) + case MagicSessionTypeJetBrains: + // Do nothing here because JetBrains launches hundreds of ssh sessions. + // We instead track JetBrains in the single persistent tcp forwarding channel. + case MagicSessionTypeSSH: + s.connCountSSHSession.Add(1) + defer s.connCountSSHSession.Add(-1) + case MagicSessionTypeUnknown: + logger.Warn(ctx, "invalid magic ssh session type specified", slog.F("raw_type", magicTypeRaw)) } if s.fileTransferBlocked(session) { @@ -309,7 +358,18 @@ func (s *Server) sessionHandler(session ssh.Session) { return } - err := s.sessionStart(logger, session, extraEnv) + x11, hasX11 := session.X11() + if hasX11 { + display, handled := s.x11Handler(session.Context(), x11) + if !handled { + _ = session.Exit(1) + logger.Error(ctx, "x11 handler failed") + return + } + env = append(env, fmt.Sprintf("DISPLAY=localhost:%d.%d", display, x11.ScreenNumber)) + } + + err := s.sessionStart(logger, session, env, magicType) var exitError *exec.ExitError if xerrors.As(err, &exitError) { code := exitError.ExitCode() @@ -379,32 +439,8 @@ func (s *Server) fileTransferBlocked(session ssh.Session) bool { return false } -func (s *Server) sessionStart(logger slog.Logger, session ssh.Session, extraEnv []string) (retErr error) { +func (s *Server) sessionStart(logger slog.Logger, session ssh.Session, env []string, magicType MagicSessionType) (retErr error) { ctx := session.Context() - env := append(session.Environ(), extraEnv...) - var magicType string - for index, kv := range env { - if !strings.HasPrefix(kv, MagicSessionTypeEnvironmentVariable) { - continue - } - magicType = strings.ToLower(strings.TrimPrefix(kv, MagicSessionTypeEnvironmentVariable+"=")) - env = append(env[:index], env[index+1:]...) - } - - // Always force lowercase checking to be case-insensitive. - switch magicType { - case MagicSessionTypeVSCode: - s.connCountVSCode.Add(1) - defer s.connCountVSCode.Add(-1) - case MagicSessionTypeJetBrains: - // Do nothing here because JetBrains launches hundreds of ssh sessions. - // We instead track JetBrains in the single persistent tcp forwarding channel. - case "": - s.connCountSSHSession.Add(1) - defer s.connCountSSHSession.Add(-1) - default: - logger.Warn(ctx, "invalid magic ssh session type specified", slog.F("type", magicType)) - } magicTypeLabel := magicTypeMetricLabel(magicType) sshPty, windowSize, isPty := session.Pty() @@ -473,7 +509,7 @@ func (s *Server) startNonPTYSession(logger slog.Logger, session ssh.Session, mag }() go func() { for sig := range sigs { - s.handleSignal(logger, sig, cmd.Process, magicTypeLabel) + handleSignal(logger, sig, cmd.Process, s.metrics, magicTypeLabel) } }() return cmd.Wait() @@ -558,7 +594,7 @@ func (s *Server) startPTYSession(logger slog.Logger, session ptySession, magicTy sigs = nil continue } - s.handleSignal(logger, sig, process, magicTypeLabel) + handleSignal(logger, sig, process, s.metrics, magicTypeLabel) case win, ok := <-windowSize: if !ok { windowSize = nil @@ -612,7 +648,7 @@ func (s *Server) startPTYSession(logger slog.Logger, session ptySession, magicTy return nil } -func (s *Server) handleSignal(logger slog.Logger, ssig ssh.Signal, signaler interface{ Signal(os.Signal) error }, magicTypeLabel string) { +func handleSignal(logger slog.Logger, ssig ssh.Signal, signaler interface{ Signal(os.Signal) error }, metrics *sshServerMetrics, magicTypeLabel string) { ctx := context.Background() sig := osSignalFrom(ssig) logger = logger.With(slog.F("ssh_signal", ssig), slog.F("signal", sig.String())) @@ -620,7 +656,7 @@ func (s *Server) handleSignal(logger slog.Logger, ssig ssh.Signal, signaler inte err := signaler.Signal(sig) if err != nil { logger.Warn(ctx, "signaling the process failed", slog.Error(err)) - s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "yes", "signal").Add(1) + metrics.sessionErrors.WithLabelValues(magicTypeLabel, "yes", "signal").Add(1) } } diff --git a/agent/agentssh/metrics.go b/agent/agentssh/metrics.go index 9c6f2fbb3c..22bbf1fd80 100644 --- a/agent/agentssh/metrics.go +++ b/agent/agentssh/metrics.go @@ -71,15 +71,15 @@ func newSSHServerMetrics(registerer prometheus.Registerer) *sshServerMetrics { } } -func magicTypeMetricLabel(magicType string) string { +func magicTypeMetricLabel(magicType MagicSessionType) string { switch magicType { case MagicSessionTypeVSCode: case MagicSessionTypeJetBrains: - case "": - magicType = "ssh" + case MagicSessionTypeSSH: + case MagicSessionTypeUnknown: default: - magicType = "unknown" + magicType = MagicSessionTypeUnknown } // Always be case insensitive - return strings.ToLower(magicType) + return strings.ToLower(string(magicType)) } diff --git a/agent/agenttest/client.go b/agent/agenttest/client.go index ea53ae9a67..f176691632 100644 --- a/agent/agenttest/client.go +++ b/agent/agenttest/client.go @@ -15,6 +15,7 @@ import ( "golang.org/x/exp/slices" "golang.org/x/xerrors" "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/emptypb" "storj.io/drpc/drpcmux" "storj.io/drpc/drpcserver" "tailscale.com/tailcfg" @@ -171,6 +172,7 @@ type FakeAgentAPI struct { lifecycleStates []codersdk.WorkspaceAgentLifecycle metadata map[string]agentsdk.Metadata timings []*agentproto.Timing + connections []*agentproto.Connection getAnnouncementBannersFunc func() ([]codersdk.BannerConfig, error) getResourcesMonitoringConfigurationFunc func() (*agentproto.GetResourcesMonitoringConfigurationResponse, error) @@ -339,12 +341,20 @@ func (f *FakeAgentAPI) BatchCreateLogs(ctx context.Context, req *agentproto.Batc func (f *FakeAgentAPI) ScriptCompleted(_ context.Context, req *agentproto.WorkspaceAgentScriptCompletedRequest) (*agentproto.WorkspaceAgentScriptCompletedResponse, error) { f.Lock() - f.timings = append(f.timings, req.Timing) + f.timings = append(f.timings, req.GetTiming()) f.Unlock() return &agentproto.WorkspaceAgentScriptCompletedResponse{}, nil } +func (f *FakeAgentAPI) ReportConnection(_ context.Context, req *agentproto.ReportConnectionRequest) (*emptypb.Empty, error) { + f.Lock() + f.connections = append(f.connections, req.GetConnection()) + f.Unlock() + + return &emptypb.Empty{}, nil +} + func NewFakeAgentAPI(t testing.TB, logger slog.Logger, manifest *agentproto.Manifest, statsCh chan *agentproto.Stats) *FakeAgentAPI { return &FakeAgentAPI{ t: t, diff --git a/agent/proto/agent.pb.go b/agent/proto/agent.pb.go index 613ce3d2d6..e4318e6fdc 100644 --- a/agent/proto/agent.pb.go +++ b/agent/proto/agent.pb.go @@ -11,6 +11,7 @@ import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" durationpb "google.golang.org/protobuf/types/known/durationpb" + emptypb "google.golang.org/protobuf/types/known/emptypb" timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" @@ -515,6 +516,110 @@ func (Timing_Status) EnumDescriptor() ([]byte, []int) { return file_agent_proto_agent_proto_rawDescGZIP(), []int{27, 1} } +type Connection_Action int32 + +const ( + Connection_ACTION_UNSPECIFIED Connection_Action = 0 + Connection_CONNECT Connection_Action = 1 + Connection_DISCONNECT Connection_Action = 2 +) + +// Enum value maps for Connection_Action. +var ( + Connection_Action_name = map[int32]string{ + 0: "ACTION_UNSPECIFIED", + 1: "CONNECT", + 2: "DISCONNECT", + } + Connection_Action_value = map[string]int32{ + "ACTION_UNSPECIFIED": 0, + "CONNECT": 1, + "DISCONNECT": 2, + } +) + +func (x Connection_Action) Enum() *Connection_Action { + p := new(Connection_Action) + *p = x + return p +} + +func (x Connection_Action) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Connection_Action) Descriptor() protoreflect.EnumDescriptor { + return file_agent_proto_agent_proto_enumTypes[9].Descriptor() +} + +func (Connection_Action) Type() protoreflect.EnumType { + return &file_agent_proto_agent_proto_enumTypes[9] +} + +func (x Connection_Action) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Connection_Action.Descriptor instead. +func (Connection_Action) EnumDescriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{32, 0} +} + +type Connection_Type int32 + +const ( + Connection_TYPE_UNSPECIFIED Connection_Type = 0 + Connection_SSH Connection_Type = 1 + Connection_VSCODE Connection_Type = 2 + Connection_JETBRAINS Connection_Type = 3 + Connection_RECONNECTING_PTY Connection_Type = 4 +) + +// Enum value maps for Connection_Type. +var ( + Connection_Type_name = map[int32]string{ + 0: "TYPE_UNSPECIFIED", + 1: "SSH", + 2: "VSCODE", + 3: "JETBRAINS", + 4: "RECONNECTING_PTY", + } + Connection_Type_value = map[string]int32{ + "TYPE_UNSPECIFIED": 0, + "SSH": 1, + "VSCODE": 2, + "JETBRAINS": 3, + "RECONNECTING_PTY": 4, + } +) + +func (x Connection_Type) Enum() *Connection_Type { + p := new(Connection_Type) + *p = x + return p +} + +func (x Connection_Type) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Connection_Type) Descriptor() protoreflect.EnumDescriptor { + return file_agent_proto_agent_proto_enumTypes[10].Descriptor() +} + +func (Connection_Type) Type() protoreflect.EnumType { + return &file_agent_proto_agent_proto_enumTypes[10] +} + +func (x Connection_Type) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Connection_Type.Descriptor instead. +func (Connection_Type) EnumDescriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{32, 1} +} + type WorkspaceApp struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2490,6 +2595,148 @@ func (*PushResourcesMonitoringUsageResponse) Descriptor() ([]byte, []int) { return file_agent_proto_agent_proto_rawDescGZIP(), []int{31} } +type Connection struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Action Connection_Action `protobuf:"varint,2,opt,name=action,proto3,enum=coder.agent.v2.Connection_Action" json:"action,omitempty"` + Type Connection_Type `protobuf:"varint,3,opt,name=type,proto3,enum=coder.agent.v2.Connection_Type" json:"type,omitempty"` + Timestamp *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + Ip string `protobuf:"bytes,5,opt,name=ip,proto3" json:"ip,omitempty"` + StatusCode int32 `protobuf:"varint,6,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` + Reason *string `protobuf:"bytes,7,opt,name=reason,proto3,oneof" json:"reason,omitempty"` +} + +func (x *Connection) Reset() { + *x = Connection{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_proto_agent_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Connection) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Connection) ProtoMessage() {} + +func (x *Connection) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[32] + 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 Connection.ProtoReflect.Descriptor instead. +func (*Connection) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{32} +} + +func (x *Connection) GetId() []byte { + if x != nil { + return x.Id + } + return nil +} + +func (x *Connection) GetAction() Connection_Action { + if x != nil { + return x.Action + } + return Connection_ACTION_UNSPECIFIED +} + +func (x *Connection) GetType() Connection_Type { + if x != nil { + return x.Type + } + return Connection_TYPE_UNSPECIFIED +} + +func (x *Connection) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +func (x *Connection) GetIp() string { + if x != nil { + return x.Ip + } + return "" +} + +func (x *Connection) GetStatusCode() int32 { + if x != nil { + return x.StatusCode + } + return 0 +} + +func (x *Connection) GetReason() string { + if x != nil && x.Reason != nil { + return *x.Reason + } + return "" +} + +type ReportConnectionRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Connection *Connection `protobuf:"bytes,1,opt,name=connection,proto3" json:"connection,omitempty"` +} + +func (x *ReportConnectionRequest) Reset() { + *x = ReportConnectionRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_proto_agent_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ReportConnectionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportConnectionRequest) ProtoMessage() {} + +func (x *ReportConnectionRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[33] + 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 ReportConnectionRequest.ProtoReflect.Descriptor instead. +func (*ReportConnectionRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{33} +} + +func (x *ReportConnectionRequest) GetConnection() *Connection { + if x != nil { + return x.Connection + } + return nil +} + type WorkspaceApp_Healthcheck struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2503,7 +2750,7 @@ type WorkspaceApp_Healthcheck struct { func (x *WorkspaceApp_Healthcheck) Reset() { *x = WorkspaceApp_Healthcheck{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[32] + mi := &file_agent_proto_agent_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2516,7 +2763,7 @@ func (x *WorkspaceApp_Healthcheck) String() string { func (*WorkspaceApp_Healthcheck) ProtoMessage() {} func (x *WorkspaceApp_Healthcheck) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[32] + mi := &file_agent_proto_agent_proto_msgTypes[34] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2567,7 +2814,7 @@ type WorkspaceAgentMetadata_Result struct { func (x *WorkspaceAgentMetadata_Result) Reset() { *x = WorkspaceAgentMetadata_Result{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[33] + mi := &file_agent_proto_agent_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2580,7 +2827,7 @@ func (x *WorkspaceAgentMetadata_Result) String() string { func (*WorkspaceAgentMetadata_Result) ProtoMessage() {} func (x *WorkspaceAgentMetadata_Result) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[33] + mi := &file_agent_proto_agent_proto_msgTypes[35] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2639,7 +2886,7 @@ type WorkspaceAgentMetadata_Description struct { func (x *WorkspaceAgentMetadata_Description) Reset() { *x = WorkspaceAgentMetadata_Description{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[34] + mi := &file_agent_proto_agent_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2652,7 +2899,7 @@ func (x *WorkspaceAgentMetadata_Description) String() string { func (*WorkspaceAgentMetadata_Description) ProtoMessage() {} func (x *WorkspaceAgentMetadata_Description) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[34] + mi := &file_agent_proto_agent_proto_msgTypes[36] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2717,7 +2964,7 @@ type Stats_Metric struct { func (x *Stats_Metric) Reset() { *x = Stats_Metric{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[37] + mi := &file_agent_proto_agent_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2730,7 +2977,7 @@ func (x *Stats_Metric) String() string { func (*Stats_Metric) ProtoMessage() {} func (x *Stats_Metric) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[37] + mi := &file_agent_proto_agent_proto_msgTypes[39] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2786,7 +3033,7 @@ type Stats_Metric_Label struct { func (x *Stats_Metric_Label) Reset() { *x = Stats_Metric_Label{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[38] + mi := &file_agent_proto_agent_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2799,7 +3046,7 @@ func (x *Stats_Metric_Label) String() string { func (*Stats_Metric_Label) ProtoMessage() {} func (x *Stats_Metric_Label) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[38] + mi := &file_agent_proto_agent_proto_msgTypes[40] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2841,7 +3088,7 @@ type BatchUpdateAppHealthRequest_HealthUpdate struct { func (x *BatchUpdateAppHealthRequest_HealthUpdate) Reset() { *x = BatchUpdateAppHealthRequest_HealthUpdate{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[39] + mi := &file_agent_proto_agent_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2854,7 +3101,7 @@ func (x *BatchUpdateAppHealthRequest_HealthUpdate) String() string { func (*BatchUpdateAppHealthRequest_HealthUpdate) ProtoMessage() {} func (x *BatchUpdateAppHealthRequest_HealthUpdate) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[39] + mi := &file_agent_proto_agent_proto_msgTypes[41] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2896,7 +3143,7 @@ type GetResourcesMonitoringConfigurationResponse_Config struct { func (x *GetResourcesMonitoringConfigurationResponse_Config) Reset() { *x = GetResourcesMonitoringConfigurationResponse_Config{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[40] + mi := &file_agent_proto_agent_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2909,7 +3156,7 @@ func (x *GetResourcesMonitoringConfigurationResponse_Config) String() string { func (*GetResourcesMonitoringConfigurationResponse_Config) ProtoMessage() {} func (x *GetResourcesMonitoringConfigurationResponse_Config) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[40] + mi := &file_agent_proto_agent_proto_msgTypes[42] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2950,7 +3197,7 @@ type GetResourcesMonitoringConfigurationResponse_Memory struct { func (x *GetResourcesMonitoringConfigurationResponse_Memory) Reset() { *x = GetResourcesMonitoringConfigurationResponse_Memory{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[41] + mi := &file_agent_proto_agent_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2963,7 +3210,7 @@ func (x *GetResourcesMonitoringConfigurationResponse_Memory) String() string { func (*GetResourcesMonitoringConfigurationResponse_Memory) ProtoMessage() {} func (x *GetResourcesMonitoringConfigurationResponse_Memory) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[41] + mi := &file_agent_proto_agent_proto_msgTypes[43] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2998,7 +3245,7 @@ type GetResourcesMonitoringConfigurationResponse_Volume struct { func (x *GetResourcesMonitoringConfigurationResponse_Volume) Reset() { *x = GetResourcesMonitoringConfigurationResponse_Volume{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[42] + mi := &file_agent_proto_agent_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3011,7 +3258,7 @@ func (x *GetResourcesMonitoringConfigurationResponse_Volume) String() string { func (*GetResourcesMonitoringConfigurationResponse_Volume) ProtoMessage() {} func (x *GetResourcesMonitoringConfigurationResponse_Volume) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[42] + mi := &file_agent_proto_agent_proto_msgTypes[44] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3054,7 +3301,7 @@ type PushResourcesMonitoringUsageRequest_Datapoint struct { func (x *PushResourcesMonitoringUsageRequest_Datapoint) Reset() { *x = PushResourcesMonitoringUsageRequest_Datapoint{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[43] + mi := &file_agent_proto_agent_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3067,7 +3314,7 @@ func (x *PushResourcesMonitoringUsageRequest_Datapoint) String() string { func (*PushResourcesMonitoringUsageRequest_Datapoint) ProtoMessage() {} func (x *PushResourcesMonitoringUsageRequest_Datapoint) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[43] + mi := &file_agent_proto_agent_proto_msgTypes[45] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3116,7 +3363,7 @@ type PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage struct { func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) Reset() { *x = PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[44] + mi := &file_agent_proto_agent_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3129,7 +3376,7 @@ func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) String() str func (*PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) ProtoMessage() {} func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[44] + mi := &file_agent_proto_agent_proto_msgTypes[46] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3172,7 +3419,7 @@ type PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage struct { func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) Reset() { *x = PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[45] + mi := &file_agent_proto_agent_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3185,7 +3432,7 @@ func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) String() str func (*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) ProtoMessage() {} func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[45] + mi := &file_agent_proto_agent_proto_msgTypes[47] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3233,556 +3480,596 @@ var file_agent_proto_agent_proto_rawDesc = []byte{ 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x94, 0x06, 0x0a, 0x0c, 0x57, 0x6f, 0x72, 0x6b, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x70, 0x70, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x78, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x65, 0x78, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x18, 0x04, - 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, 0x05, 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, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x73, - 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, - 0x73, 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x75, 0x62, - 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0d, 0x73, 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4e, 0x61, 0x6d, 0x65, - 0x12, 0x4e, 0x0a, 0x0d, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x6c, 0x65, 0x76, 0x65, - 0x6c, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x41, 0x70, 0x70, 0x2e, 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, 0x4a, 0x0a, 0x0b, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, - 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x41, 0x70, 0x70, 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, 0x3b, 0x0a, 0x06, - 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x70, 0x70, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, - 0x68, 0x52, 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x69, 0x64, - 0x64, 0x65, 0x6e, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x68, 0x69, 0x64, 0x64, 0x65, - 0x6e, 0x1a, 0x74, 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, 0x35, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 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, 0x57, 0x0a, 0x0c, 0x53, 0x68, 0x61, 0x72, 0x69, - 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x19, 0x53, 0x48, 0x41, 0x52, 0x49, - 0x4e, 0x47, 0x5f, 0x4c, 0x45, 0x56, 0x45, 0x4c, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, - 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x57, 0x4e, 0x45, 0x52, 0x10, - 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, - 0x45, 0x44, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x03, - 0x22, 0x5c, 0x0a, 0x06, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x12, 0x48, 0x45, - 0x41, 0x4c, 0x54, 0x48, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, - 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, - 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x4c, 0x49, 0x5a, 0x49, 0x4e, 0x47, - 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x03, 0x12, - 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x04, 0x22, 0xd9, - 0x02, 0x0a, 0x14, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, - 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x12, 0x22, 0x0a, 0x0d, 0x6c, 0x6f, 0x67, 0x5f, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, - 0x6c, 0x6f, 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x6c, - 0x6f, 0x67, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, - 0x6f, 0x67, 0x50, 0x61, 0x74, 0x68, 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, 0x12, - 0x0a, 0x04, 0x63, 0x72, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x72, - 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0c, 0x72, 0x75, 0x6e, 0x5f, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x61, - 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x72, 0x75, 0x6e, 0x4f, 0x6e, 0x53, - 0x74, 0x61, 0x72, 0x74, 0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x75, 0x6e, 0x5f, 0x6f, 0x6e, 0x5f, 0x73, - 0x74, 0x6f, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x72, 0x75, 0x6e, 0x4f, 0x6e, - 0x53, 0x74, 0x6f, 0x70, 0x12, 0x2c, 0x0a, 0x12, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x62, 0x6c, - 0x6f, 0x63, 0x6b, 0x73, 0x5f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x10, 0x73, 0x74, 0x61, 0x72, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x4c, 0x6f, 0x67, - 0x69, 0x6e, 0x12, 0x33, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, - 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, - 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, - 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x22, 0x86, 0x04, 0x0a, 0x16, 0x57, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x45, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x41, 0x67, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x52, 0x65, - 0x73, 0x75, 0x6c, 0x74, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x54, 0x0a, 0x0b, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x32, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, - 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x1a, 0x85, 0x01, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x3d, 0x0a, - 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, - 0x0b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x10, 0x0a, 0x03, - 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x61, 0x67, 0x65, 0x12, 0x14, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x1a, 0xc6, 0x01, 0x0a, 0x0b, 0x44, - 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, - 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, - 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 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, 0x35, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x76, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x33, - 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x65, - 0x6f, 0x75, 0x74, 0x22, 0xea, 0x06, 0x0a, 0x08, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, - 0x12, 0x19, 0x0a, 0x08, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x07, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x61, - 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x6f, 0x77, - 0x6e, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0d, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, - 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, - 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x67, - 0x69, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x67, 0x69, 0x74, 0x41, 0x75, 0x74, 0x68, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x73, 0x12, 0x67, 0x0a, 0x15, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, - 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x18, 0x03, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x2e, 0x45, - 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, - 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x14, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, - 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x12, 0x1c, - 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x32, 0x0a, 0x16, - 0x76, 0x73, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x70, 0x72, 0x6f, - 0x78, 0x79, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x76, 0x73, - 0x43, 0x6f, 0x64, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x55, 0x72, 0x69, - 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x6f, 0x74, 0x64, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x6f, 0x74, 0x64, 0x50, 0x61, 0x74, 0x68, 0x12, 0x3c, 0x0a, - 0x1a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, - 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x18, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, - 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x64, - 0x65, 0x72, 0x70, 0x5f, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x5f, 0x77, 0x65, 0x62, 0x73, 0x6f, 0x63, - 0x6b, 0x65, 0x74, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x64, 0x65, 0x72, 0x70, - 0x46, 0x6f, 0x72, 0x63, 0x65, 0x57, 0x65, 0x62, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x12, - 0x34, 0x0a, 0x08, 0x64, 0x65, 0x72, 0x70, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x09, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, - 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x44, 0x45, 0x52, 0x50, 0x4d, 0x61, 0x70, 0x52, 0x07, 0x64, 0x65, - 0x72, 0x70, 0x4d, 0x61, 0x70, 0x12, 0x3e, 0x0a, 0x07, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x73, - 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, - 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x52, 0x07, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x73, 0x12, 0x30, 0x0a, 0x04, 0x61, 0x70, 0x70, 0x73, 0x18, 0x0b, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x70, - 0x70, 0x52, 0x04, 0x61, 0x70, 0x70, 0x73, 0x12, 0x4e, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x2e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x6d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x47, 0x0a, 0x19, 0x45, 0x6e, 0x76, 0x69, 0x72, - 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 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, - 0x22, 0x14, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x6e, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, - 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, - 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x62, - 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, - 0x64, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x22, 0x19, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x22, 0xb3, 0x07, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x5f, 0x0a, 0x14, 0x63, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x62, 0x79, 0x5f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, - 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x50, 0x72, - 0x6f, 0x74, 0x6f, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x12, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x29, 0x0a, 0x10, - 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3f, 0x0a, 0x1c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x65, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x6c, 0x61, 0x74, - 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x52, 0x19, 0x63, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x64, 0x69, 0x61, 0x6e, 0x4c, - 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x4d, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x78, 0x5f, 0x70, - 0x61, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x78, - 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x72, 0x78, 0x5f, 0x62, 0x79, - 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x72, 0x78, 0x42, 0x79, 0x74, - 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x78, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x73, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x78, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, - 0x73, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x78, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x07, 0x74, 0x78, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x30, 0x0a, 0x14, - 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x76, 0x73, - 0x63, 0x6f, 0x64, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x73, 0x65, 0x73, 0x73, - 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x56, 0x73, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x36, - 0x0a, 0x17, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, - 0x6a, 0x65, 0x74, 0x62, 0x72, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x15, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4a, 0x65, 0x74, - 0x62, 0x72, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x43, 0x0a, 0x1e, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x70, 0x74, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x1b, - 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x74, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x73, - 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x73, 0x73, 0x68, - 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, - 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x73, 0x68, 0x12, 0x36, 0x0a, 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, - 0x63, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, - 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x52, 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x1a, - 0x45, 0x0a, 0x17, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, - 0x50, 0x72, 0x6f, 0x74, 0x6f, 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, 0x03, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x8e, 0x02, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, - 0x63, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, - 0x63, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x12, 0x3a, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, - 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x1a, 0x31, - 0x0a, 0x05, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x22, 0x34, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x10, 0x54, 0x59, 0x50, - 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, - 0x0b, 0x0a, 0x07, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x45, 0x52, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, - 0x47, 0x41, 0x55, 0x47, 0x45, 0x10, 0x02, 0x22, 0x41, 0x0a, 0x12, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, - 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, - 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x22, 0x59, 0x0a, 0x13, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x42, 0x0a, 0x0f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0e, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x74, - 0x65, 0x72, 0x76, 0x61, 0x6c, 0x22, 0xae, 0x02, 0x0a, 0x09, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, - 0x63, 0x6c, 0x65, 0x12, 0x35, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x2e, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x68, - 0x61, 0x6e, 0x67, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, - 0x67, 0x65, 0x64, 0x41, 0x74, 0x22, 0xae, 0x01, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x15, 0x0a, 0x11, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, - 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, - 0x44, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x54, 0x41, 0x52, 0x54, 0x49, 0x4e, 0x47, 0x10, - 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, - 0x55, 0x54, 0x10, 0x03, 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x45, 0x52, - 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x45, 0x41, 0x44, 0x59, 0x10, 0x05, - 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x48, 0x55, 0x54, 0x54, 0x49, 0x4e, 0x47, 0x5f, 0x44, 0x4f, 0x57, - 0x4e, 0x10, 0x06, 0x12, 0x14, 0x0a, 0x10, 0x53, 0x48, 0x55, 0x54, 0x44, 0x4f, 0x57, 0x4e, 0x5f, - 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x07, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x48, 0x55, - 0x54, 0x44, 0x4f, 0x57, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x08, 0x12, 0x07, 0x0a, - 0x03, 0x4f, 0x46, 0x46, 0x10, 0x09, 0x22, 0x51, 0x0a, 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x37, 0x0a, 0x09, 0x6c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x09, - 0x6c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x22, 0xc4, 0x01, 0x0a, 0x1b, 0x42, 0x61, - 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, - 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x52, 0x0a, 0x07, 0x75, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, - 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x52, 0x07, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x1a, 0x51, 0x0a, - 0x0c, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, - 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x31, 0x0a, - 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, - 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x41, - 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, - 0x22, 0x1e, 0x0a, 0x1c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, - 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0xe8, 0x01, 0x0a, 0x07, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x18, 0x0a, 0x07, - 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2d, 0x0a, 0x12, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, - 0x65, 0x64, 0x5f, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x11, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x65, 0x64, 0x44, 0x69, 0x72, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x41, 0x0a, 0x0a, 0x73, 0x75, 0x62, 0x73, 0x79, 0x73, 0x74, - 0x65, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, - 0x75, 0x70, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x0a, 0x73, 0x75, - 0x62, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x51, 0x0a, 0x09, 0x53, 0x75, 0x62, 0x73, - 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x55, 0x42, 0x53, 0x59, 0x53, 0x54, - 0x45, 0x4d, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, - 0x12, 0x0a, 0x0a, 0x06, 0x45, 0x4e, 0x56, 0x42, 0x4f, 0x58, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, - 0x45, 0x4e, 0x56, 0x42, 0x55, 0x49, 0x4c, 0x44, 0x45, 0x52, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, - 0x45, 0x58, 0x45, 0x43, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x03, 0x22, 0x49, 0x0a, 0x14, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x07, 0x73, - 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x22, 0x63, 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, 0x45, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x94, 0x06, 0x0a, 0x0c, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x41, 0x70, 0x70, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x78, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x65, 0x78, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x18, 0x04, 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, 0x05, 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, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, + 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x75, 0x62, 0x64, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x75, 0x62, + 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x75, 0x62, 0x64, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, + 0x73, 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x4e, 0x0a, + 0x0d, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, - 0x67, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x52, 0x65, 0x73, - 0x75, 0x6c, 0x74, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x52, 0x0a, 0x1a, 0x42, - 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x08, 0x6d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, 0x6f, - 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, - 0x1d, 0x0a, 0x1b, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xde, - 0x01, 0x0a, 0x03, 0x4c, 0x6f, 0x67, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x64, 0x5f, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, - 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x2f, 0x0a, 0x05, 0x6c, 0x65, 0x76, - 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x6f, 0x67, 0x2e, 0x4c, 0x65, - 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x53, 0x0a, 0x05, 0x4c, 0x65, - 0x76, 0x65, 0x6c, 0x12, 0x15, 0x0a, 0x11, 0x4c, 0x45, 0x56, 0x45, 0x4c, 0x5f, 0x55, 0x4e, 0x53, - 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, - 0x41, 0x43, 0x45, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x02, - 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, - 0x52, 0x4e, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x05, 0x22, - 0x65, 0x0a, 0x16, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, - 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x0d, 0x6c, 0x6f, 0x67, - 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x0b, 0x6c, 0x6f, 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x27, 0x0a, - 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x6f, - 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x6f, 0x67, - 0x52, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x22, 0x47, 0x0a, 0x17, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x5f, 0x65, - 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x6c, - 0x6f, 0x67, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x45, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x22, - 0x1f, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x22, 0x71, 0x0a, 0x1e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x14, 0x61, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x5f, 0x62, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x13, - 0x61, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, - 0x65, 0x72, 0x73, 0x22, 0x6d, 0x0a, 0x0c, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x18, 0x0a, - 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x62, 0x61, 0x63, 0x6b, 0x67, - 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0f, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x43, 0x6f, 0x6c, - 0x6f, 0x72, 0x22, 0x56, 0x0a, 0x24, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, - 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, - 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x06, 0x74, 0x69, - 0x6d, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x69, 0x6d, 0x69, - 0x6e, 0x67, 0x52, 0x06, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x22, 0x27, 0x0a, 0x25, 0x57, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0xfd, 0x02, 0x0a, 0x06, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x12, 0x1b, - 0x0a, 0x09, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x08, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x49, 0x64, 0x12, 0x30, 0x0a, 0x05, 0x73, - 0x74, 0x61, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x2c, 0x0a, - 0x03, 0x65, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x65, - 0x78, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, - 0x65, 0x78, 0x69, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x67, - 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x2e, - 0x53, 0x74, 0x61, 0x67, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x12, 0x35, 0x0a, 0x06, - 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x69, - 0x6d, 0x69, 0x6e, 0x67, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x22, 0x26, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x09, 0x0a, 0x05, - 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f, 0x50, 0x10, - 0x01, 0x12, 0x08, 0x0a, 0x04, 0x43, 0x52, 0x4f, 0x4e, 0x10, 0x02, 0x22, 0x46, 0x0a, 0x06, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x06, 0x0a, 0x02, 0x4f, 0x4b, 0x10, 0x00, 0x12, 0x10, 0x0a, - 0x0c, 0x45, 0x58, 0x49, 0x54, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, 0x01, 0x12, - 0x0d, 0x0a, 0x09, 0x54, 0x49, 0x4d, 0x45, 0x44, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x02, 0x12, 0x13, - 0x0a, 0x0f, 0x50, 0x49, 0x50, 0x45, 0x53, 0x5f, 0x4c, 0x45, 0x46, 0x54, 0x5f, 0x4f, 0x50, 0x45, - 0x4e, 0x10, 0x03, 0x22, 0x2c, 0x0a, 0x2a, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x22, 0xa0, 0x04, 0x0a, 0x2b, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x5a, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x42, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, - 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x5f, 0x0a, - 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x42, 0x2e, - 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, - 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, - 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x72, - 0x79, 0x48, 0x00, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x88, 0x01, 0x01, 0x12, 0x5c, - 0x0a, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x42, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, - 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, - 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x56, 0x6f, 0x6c, - 0x75, 0x6d, 0x65, 0x52, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x1a, 0x6f, 0x0a, 0x06, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x6e, 0x75, 0x6d, 0x5f, 0x64, 0x61, - 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, - 0x6e, 0x75, 0x6d, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x3e, 0x0a, - 0x1b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x76, 0x61, 0x6c, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x19, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, - 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x1a, 0x22, 0x0a, - 0x06, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, - 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, - 0x64, 0x1a, 0x36, 0x0a, 0x06, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x65, - 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, - 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x6d, 0x65, - 0x6d, 0x6f, 0x72, 0x79, 0x22, 0xb3, 0x04, 0x0a, 0x23, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, - 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x5d, 0x0a, 0x0a, - 0x64, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x3d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, - 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, - 0x0a, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x1a, 0xac, 0x03, 0x0a, 0x09, - 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3d, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x66, 0x0a, 0x06, 0x6d, 0x65, 0x6d, 0x6f, - 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x49, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, - 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, - 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x55, 0x73, - 0x61, 0x67, 0x65, 0x48, 0x00, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x88, 0x01, 0x01, - 0x12, 0x63, 0x0a, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x49, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, - 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, - 0x2e, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x07, 0x76, 0x6f, - 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x1a, 0x37, 0x0a, 0x0b, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x55, - 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x04, 0x75, 0x73, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, - 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x1a, 0x4f, - 0x0a, 0x0b, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x16, 0x0a, - 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x76, - 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x04, 0x75, 0x73, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, - 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, - 0x09, 0x0a, 0x07, 0x5f, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x22, 0x26, 0x0a, 0x24, 0x50, 0x75, - 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, - 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x2a, 0x63, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, - 0x1a, 0x0a, 0x16, 0x41, 0x50, 0x50, 0x5f, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x5f, 0x55, 0x4e, - 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x44, - 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x4e, 0x49, - 0x54, 0x49, 0x41, 0x4c, 0x49, 0x5a, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x48, - 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x48, 0x45, - 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x04, 0x32, 0x9c, 0x0a, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, - 0x74, 0x12, 0x4b, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, - 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x5a, - 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, - 0x65, 0x72, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, - 0x6e, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x63, 0x6f, - 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x56, 0x0a, 0x0b, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, - 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, - 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, - 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, - 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, - 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x42, 0x61, 0x74, 0x63, - 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, - 0x73, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, - 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, - 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, - 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, - 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x0d, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x24, 0x2e, - 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x6e, 0x0a, 0x13, - 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, - 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x0f, - 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x12, - 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, - 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x77, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, - 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, - 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, - 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7e, 0x0a, 0x0f, 0x53, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x34, 0x2e, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x70, 0x70, 0x2e, 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, 0x4a, 0x0a, + 0x0b, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x0b, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x70, 0x70, + 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, 0x3b, 0x0a, 0x06, 0x68, 0x65, 0x61, + 0x6c, 0x74, 0x68, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x41, 0x70, 0x70, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x06, + 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, + 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x1a, 0x74, + 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, + 0x35, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 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, 0x57, 0x0a, 0x0c, 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, + 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x19, 0x53, 0x48, 0x41, 0x52, 0x49, 0x4e, 0x47, 0x5f, + 0x4c, 0x45, 0x56, 0x45, 0x4c, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x57, 0x4e, 0x45, 0x52, 0x10, 0x01, 0x12, 0x11, + 0x0a, 0x0d, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x45, 0x44, 0x10, + 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x03, 0x22, 0x5c, 0x0a, + 0x06, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x12, 0x48, 0x45, 0x41, 0x4c, 0x54, + 0x48, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, + 0x0c, 0x0a, 0x08, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x10, 0x0a, + 0x0c, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x4c, 0x49, 0x5a, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, + 0x0b, 0x0a, 0x07, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, + 0x55, 0x4e, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x04, 0x22, 0xd9, 0x02, 0x0a, 0x14, + 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x12, 0x22, 0x0a, 0x0d, 0x6c, 0x6f, 0x67, 0x5f, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x6c, 0x6f, 0x67, + 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x6f, 0x67, 0x5f, + 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x50, + 0x61, 0x74, 0x68, 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, 0x12, 0x0a, 0x04, 0x63, + 0x72, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x72, 0x6f, 0x6e, 0x12, + 0x20, 0x0a, 0x0c, 0x72, 0x75, 0x6e, 0x5f, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x72, 0x75, 0x6e, 0x4f, 0x6e, 0x53, 0x74, 0x61, 0x72, + 0x74, 0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x75, 0x6e, 0x5f, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x6f, 0x70, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x72, 0x75, 0x6e, 0x4f, 0x6e, 0x53, 0x74, 0x6f, + 0x70, 0x12, 0x2c, 0x0a, 0x12, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, + 0x73, 0x5f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73, + 0x74, 0x61, 0x72, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, + 0x33, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x74, 0x69, 0x6d, + 0x65, 0x6f, 0x75, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, + 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x22, 0x86, 0x04, 0x0a, 0x16, 0x57, 0x6f, 0x72, 0x6b, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x12, 0x45, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, - 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, - 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x9e, 0x01, 0x0a, 0x23, 0x47, 0x65, + 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x54, 0x0a, 0x0b, 0x64, 0x65, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, + 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x1a, + 0x85, 0x01, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x3d, 0x0a, 0x0c, 0x63, 0x6f, + 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x63, 0x6f, + 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x67, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x61, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x1a, 0xc6, 0x01, 0x0a, 0x0b, 0x44, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, + 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, + 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 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, 0x35, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x33, 0x0a, 0x07, 0x74, + 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, + 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, + 0x22, 0xea, 0x06, 0x0a, 0x08, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, + 0x08, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x07, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x6f, 0x77, 0x6e, 0x65, 0x72, + 0x5f, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, + 0x0a, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0e, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, + 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x67, 0x69, 0x74, 0x5f, + 0x61, 0x75, 0x74, 0x68, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x0e, 0x67, 0x69, 0x74, 0x41, 0x75, 0x74, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x73, 0x12, 0x67, 0x0a, 0x15, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, + 0x74, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x32, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, + 0x76, 0x32, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x2e, 0x45, 0x6e, 0x76, 0x69, + 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x14, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, + 0x6e, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x64, + 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x32, 0x0a, 0x16, 0x76, 0x73, 0x5f, + 0x63, 0x6f, 0x64, 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, + 0x75, 0x72, 0x69, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x76, 0x73, 0x43, 0x6f, 0x64, + 0x65, 0x50, 0x6f, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x55, 0x72, 0x69, 0x12, 0x1b, 0x0a, + 0x09, 0x6d, 0x6f, 0x74, 0x64, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x6d, 0x6f, 0x74, 0x64, 0x50, 0x61, 0x74, 0x68, 0x12, 0x3c, 0x0a, 0x1a, 0x64, 0x69, + 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, 0x63, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x18, + 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x64, 0x65, 0x72, 0x70, + 0x5f, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x5f, 0x77, 0x65, 0x62, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, + 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x64, 0x65, 0x72, 0x70, 0x46, 0x6f, 0x72, + 0x63, 0x65, 0x57, 0x65, 0x62, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x12, 0x34, 0x0a, 0x08, + 0x64, 0x65, 0x72, 0x70, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, + 0x32, 0x2e, 0x44, 0x45, 0x52, 0x50, 0x4d, 0x61, 0x70, 0x52, 0x07, 0x64, 0x65, 0x72, 0x70, 0x4d, + 0x61, 0x70, 0x12, 0x3e, 0x0a, 0x07, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x73, 0x18, 0x0a, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, + 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x52, 0x07, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x73, 0x12, 0x30, 0x0a, 0x04, 0x61, 0x70, 0x70, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, + 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x70, 0x70, 0x52, 0x04, + 0x61, 0x70, 0x70, 0x73, 0x12, 0x4e, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x44, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x1a, 0x47, 0x0a, 0x19, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, + 0x65, 0x6e, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 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, 0x22, 0x14, 0x0a, + 0x12, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x22, 0x6e, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, + 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x18, + 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x62, 0x61, 0x63, 0x6b, + 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0f, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x43, 0x6f, + 0x6c, 0x6f, 0x72, 0x22, 0x19, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xb3, + 0x07, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x5f, 0x0a, 0x14, 0x63, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x62, 0x79, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x12, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x42, 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43, + 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3f, 0x0a, 0x1c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x6d, 0x65, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, + 0x79, 0x5f, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x52, 0x19, 0x63, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x64, 0x69, 0x61, 0x6e, 0x4c, 0x61, 0x74, 0x65, + 0x6e, 0x63, 0x79, 0x4d, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x78, 0x5f, 0x70, 0x61, 0x63, 0x6b, + 0x65, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x78, 0x50, 0x61, 0x63, + 0x6b, 0x65, 0x74, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x72, 0x78, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x72, 0x78, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, + 0x1d, 0x0a, 0x0a, 0x74, 0x78, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x78, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x12, 0x19, + 0x0a, 0x08, 0x74, 0x78, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x07, 0x74, 0x78, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x30, 0x0a, 0x14, 0x73, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x76, 0x73, 0x63, 0x6f, 0x64, + 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x56, 0x73, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x17, 0x73, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x6a, 0x65, 0x74, + 0x62, 0x72, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x15, 0x73, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4a, 0x65, 0x74, 0x62, 0x72, 0x61, + 0x69, 0x6e, 0x73, 0x12, 0x43, 0x0a, 0x1e, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6e, + 0x67, 0x5f, 0x70, 0x74, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x1b, 0x73, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x74, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x73, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x73, 0x73, 0x68, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x53, 0x73, 0x68, 0x12, 0x36, 0x0a, 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x18, + 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x4d, 0x65, 0x74, + 0x72, 0x69, 0x63, 0x52, 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x1a, 0x45, 0x0a, 0x17, + 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 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, 0x03, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x1a, 0x8e, 0x02, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x12, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x21, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, + 0x32, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x2e, 0x54, + 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, + 0x3a, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, + 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x2e, 0x4c, 0x61, + 0x62, 0x65, 0x6c, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x1a, 0x31, 0x0a, 0x05, 0x4c, + 0x61, 0x62, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x34, + 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x10, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, + 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x45, 0x52, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x47, 0x41, 0x55, + 0x47, 0x45, 0x10, 0x02, 0x22, 0x41, 0x0a, 0x12, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, + 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x05, 0x73, 0x74, + 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, + 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x22, 0x59, 0x0a, 0x13, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, + 0x0a, 0x0f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, + 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x0e, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, + 0x61, 0x6c, 0x22, 0xae, 0x02, 0x0a, 0x09, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, + 0x12, 0x35, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x1f, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, + 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x67, + 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, + 0x41, 0x74, 0x22, 0xae, 0x01, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x15, 0x0a, 0x11, + 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, + 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x54, 0x41, 0x52, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x11, + 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, + 0x03, 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, + 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x45, 0x41, 0x44, 0x59, 0x10, 0x05, 0x12, 0x11, 0x0a, + 0x0d, 0x53, 0x48, 0x55, 0x54, 0x54, 0x49, 0x4e, 0x47, 0x5f, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x06, + 0x12, 0x14, 0x0a, 0x10, 0x53, 0x48, 0x55, 0x54, 0x44, 0x4f, 0x57, 0x4e, 0x5f, 0x54, 0x49, 0x4d, + 0x45, 0x4f, 0x55, 0x54, 0x10, 0x07, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x48, 0x55, 0x54, 0x44, 0x4f, + 0x57, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x08, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x46, + 0x46, 0x10, 0x09, 0x22, 0x51, 0x0a, 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, + 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x37, 0x0a, + 0x09, 0x6c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, + 0x32, 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x09, 0x6c, 0x69, 0x66, + 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x22, 0xc4, 0x01, 0x0a, 0x1b, 0x42, 0x61, 0x74, 0x63, 0x68, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x52, 0x0a, 0x07, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x52, 0x07, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x1a, 0x51, 0x0a, 0x0c, 0x48, 0x65, + 0x61, 0x6c, 0x74, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x31, 0x0a, 0x06, 0x68, 0x65, + 0x61, 0x6c, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x41, 0x70, 0x70, 0x48, + 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x22, 0x1e, 0x0a, + 0x1c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, + 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xe8, 0x01, + 0x0a, 0x07, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x2d, 0x0a, 0x12, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x65, 0x64, 0x5f, + 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x11, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x65, 0x64, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x79, 0x12, 0x41, 0x0a, 0x0a, 0x73, 0x75, 0x62, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, + 0x18, 0x03, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x2e, + 0x53, 0x75, 0x62, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x0a, 0x73, 0x75, 0x62, 0x73, 0x79, + 0x73, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x51, 0x0a, 0x09, 0x53, 0x75, 0x62, 0x73, 0x79, 0x73, 0x74, + 0x65, 0x6d, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x55, 0x42, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x5f, + 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0a, 0x0a, + 0x06, 0x45, 0x4e, 0x56, 0x42, 0x4f, 0x58, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x45, 0x4e, 0x56, + 0x42, 0x55, 0x49, 0x4c, 0x44, 0x45, 0x52, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x45, 0x58, 0x45, + 0x43, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x03, 0x22, 0x49, 0x0a, 0x14, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x31, 0x0a, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, + 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x07, 0x73, 0x74, 0x61, 0x72, + 0x74, 0x75, 0x70, 0x22, 0x63, 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, 0x45, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, + 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, + 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, + 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x52, 0x0a, 0x1a, 0x42, 0x61, 0x74, 0x63, + 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x1d, 0x0a, 0x1b, + 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xde, 0x01, 0x0a, 0x03, + 0x4c, 0x6f, 0x67, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x16, + 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x2f, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x6f, 0x67, 0x2e, 0x4c, 0x65, 0x76, 0x65, 0x6c, + 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x53, 0x0a, 0x05, 0x4c, 0x65, 0x76, 0x65, 0x6c, + 0x12, 0x15, 0x0a, 0x11, 0x4c, 0x45, 0x56, 0x45, 0x4c, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, + 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, + 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x02, 0x12, 0x08, 0x0a, + 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, + 0x04, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x05, 0x22, 0x65, 0x0a, 0x16, + 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x0d, 0x6c, 0x6f, 0x67, 0x5f, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x6c, + 0x6f, 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x27, 0x0a, 0x04, 0x6c, 0x6f, + 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x6f, 0x67, 0x52, 0x04, 0x6c, + 0x6f, 0x67, 0x73, 0x22, 0x47, 0x0a, 0x17, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, + 0x0a, 0x12, 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x5f, 0x65, 0x78, 0x63, 0x65, + 0x65, 0x64, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x6c, 0x6f, 0x67, 0x4c, + 0x69, 0x6d, 0x69, 0x74, 0x45, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x22, 0x1f, 0x0a, 0x1d, + 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, + 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x71, 0x0a, + 0x1e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x4f, 0x0a, 0x14, 0x61, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, + 0x62, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, + 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x13, 0x61, 0x6e, 0x6e, + 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, + 0x22, 0x6d, 0x0a, 0x0c, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, + 0x6e, 0x64, 0x5f, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, + 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x22, + 0x56, 0x0a, 0x24, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, + 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x06, 0x74, 0x69, 0x6d, 0x69, 0x6e, + 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x52, + 0x06, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x22, 0x27, 0x0a, 0x25, 0x57, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, + 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0xfd, 0x02, 0x0a, 0x06, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x12, 0x1b, 0x0a, 0x09, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x49, 0x64, 0x12, 0x30, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x03, 0x65, 0x6e, + 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x65, 0x78, 0x69, 0x74, + 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x65, 0x78, 0x69, + 0x74, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x2e, 0x53, 0x74, 0x61, + 0x67, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x12, 0x35, 0x0a, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x69, 0x6d, 0x69, 0x6e, + 0x67, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x22, 0x26, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, + 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f, 0x50, 0x10, 0x01, 0x12, 0x08, + 0x0a, 0x04, 0x43, 0x52, 0x4f, 0x4e, 0x10, 0x02, 0x22, 0x46, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x06, 0x0a, 0x02, 0x4f, 0x4b, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x45, 0x58, + 0x49, 0x54, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, + 0x54, 0x49, 0x4d, 0x45, 0x44, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x50, + 0x49, 0x50, 0x45, 0x53, 0x5f, 0x4c, 0x45, 0x46, 0x54, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x03, + 0x22, 0x2c, 0x0a, 0x2a, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, + 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xa0, + 0x04, 0x0a, 0x2b, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, + 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, + 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x42, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, + 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, + 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x5f, 0x0a, 0x06, 0x6d, 0x65, + 0x6d, 0x6f, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, + 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x48, 0x00, + 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x88, 0x01, 0x01, 0x12, 0x5c, 0x0a, 0x07, 0x76, + 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x3a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, - 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3b, 0x2e, - 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, - 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, - 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x89, 0x01, 0x0a, 0x1c, 0x50, + 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, + 0x52, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x1a, 0x6f, 0x0a, 0x06, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x6e, 0x75, 0x6d, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x70, + 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x6e, 0x75, 0x6d, + 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x3e, 0x0a, 0x1b, 0x63, 0x6f, + 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, + 0x6c, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x19, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x74, 0x65, 0x72, + 0x76, 0x61, 0x6c, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x1a, 0x22, 0x0a, 0x06, 0x4d, 0x65, + 0x6d, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x1a, 0x36, + 0x0a, 0x06, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x6d, 0x65, 0x6d, 0x6f, 0x72, + 0x79, 0x22, 0xb3, 0x04, 0x0a, 0x23, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, + 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x5d, 0x0a, 0x0a, 0x64, 0x61, 0x74, + 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3d, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, - 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x33, 0x2e, 0x63, 0x6f, - 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, - 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, - 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x34, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, - 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x0a, 0x64, 0x61, + 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x1a, 0xac, 0x03, 0x0a, 0x09, 0x44, 0x61, 0x74, + 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3d, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, + 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, + 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x66, 0x0a, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x49, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, + 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, + 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x55, 0x73, 0x61, 0x67, 0x65, + 0x48, 0x00, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x88, 0x01, 0x01, 0x12, 0x63, 0x0a, + 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x49, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, + 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, + 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x56, 0x6f, + 0x6c, 0x75, 0x6d, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, + 0x65, 0x73, 0x1a, 0x37, 0x0a, 0x0b, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x55, 0x73, 0x61, 0x67, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x04, 0x75, 0x73, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x1a, 0x4f, 0x0a, 0x0b, 0x56, + 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x6f, + 0x6c, 0x75, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x76, 0x6f, 0x6c, 0x75, + 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x04, 0x75, 0x73, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x09, 0x0a, 0x07, + 0x5f, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x22, 0x26, 0x0a, 0x24, 0x50, 0x75, 0x73, 0x68, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, + 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0xb6, 0x03, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, + 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x39, + 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, + 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x33, 0x0a, 0x04, 0x74, 0x79, 0x70, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x38, + 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1b, 0x0a, 0x06, 0x72, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x72, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x22, 0x3d, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x16, 0x0a, 0x12, 0x41, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, + 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x4f, 0x4e, 0x4e, + 0x45, 0x43, 0x54, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x44, 0x49, 0x53, 0x43, 0x4f, 0x4e, 0x4e, + 0x45, 0x43, 0x54, 0x10, 0x02, 0x22, 0x56, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, + 0x10, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x53, 0x53, 0x48, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, + 0x56, 0x53, 0x43, 0x4f, 0x44, 0x45, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x4a, 0x45, 0x54, 0x42, + 0x52, 0x41, 0x49, 0x4e, 0x53, 0x10, 0x03, 0x12, 0x14, 0x0a, 0x10, 0x52, 0x45, 0x43, 0x4f, 0x4e, + 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4e, 0x47, 0x5f, 0x50, 0x54, 0x59, 0x10, 0x04, 0x42, 0x09, 0x0a, + 0x07, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x55, 0x0a, 0x17, 0x52, 0x65, 0x70, 0x6f, + 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x3a, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2a, + 0x63, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x16, + 0x41, 0x50, 0x50, 0x5f, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, + 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x49, 0x53, 0x41, + 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, + 0x4c, 0x49, 0x5a, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x48, 0x45, 0x41, 0x4c, + 0x54, 0x48, 0x59, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x48, 0x45, 0x41, 0x4c, 0x54, + 0x48, 0x59, 0x10, 0x04, 0x32, 0xf1, 0x0a, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x4b, + 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x22, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, + 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, + 0x76, 0x32, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x5a, 0x0a, 0x10, 0x47, + 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, + 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, + 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, + 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x56, 0x0a, 0x0b, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, + 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x54, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, + 0x6c, 0x65, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, + 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x66, 0x65, + 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x73, 0x12, 0x2b, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, + 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, + 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, + 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, + 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, + 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x0d, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x24, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, + 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x6e, 0x0a, 0x13, 0x42, 0x61, 0x74, + 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, + 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, + 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x0f, 0x42, 0x61, 0x74, + 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x26, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, + 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x77, 0x0a, + 0x16, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, + 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, + 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7e, 0x0a, 0x0f, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x34, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, + 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x35, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, + 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x9e, 0x01, 0x0a, 0x23, 0x47, 0x65, 0x74, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, + 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3a, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, + 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, + 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3b, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, + 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x89, 0x01, 0x0a, 0x1c, 0x50, 0x75, 0x73, 0x68, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, + 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x33, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, + 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, + 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, + 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x10, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x43, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -3797,8 +4084,8 @@ func file_agent_proto_agent_proto_rawDescGZIP() []byte { return file_agent_proto_agent_proto_rawDescData } -var file_agent_proto_agent_proto_enumTypes = make([]protoimpl.EnumInfo, 9) -var file_agent_proto_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 46) +var file_agent_proto_agent_proto_enumTypes = make([]protoimpl.EnumInfo, 11) +var file_agent_proto_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 48) var file_agent_proto_agent_proto_goTypes = []interface{}{ (AppHealth)(0), // 0: coder.agent.v2.AppHealth (WorkspaceApp_SharingLevel)(0), // 1: coder.agent.v2.WorkspaceApp.SharingLevel @@ -3809,132 +4096,143 @@ var file_agent_proto_agent_proto_goTypes = []interface{}{ (Log_Level)(0), // 6: coder.agent.v2.Log.Level (Timing_Stage)(0), // 7: coder.agent.v2.Timing.Stage (Timing_Status)(0), // 8: coder.agent.v2.Timing.Status - (*WorkspaceApp)(nil), // 9: coder.agent.v2.WorkspaceApp - (*WorkspaceAgentScript)(nil), // 10: coder.agent.v2.WorkspaceAgentScript - (*WorkspaceAgentMetadata)(nil), // 11: coder.agent.v2.WorkspaceAgentMetadata - (*Manifest)(nil), // 12: coder.agent.v2.Manifest - (*GetManifestRequest)(nil), // 13: coder.agent.v2.GetManifestRequest - (*ServiceBanner)(nil), // 14: coder.agent.v2.ServiceBanner - (*GetServiceBannerRequest)(nil), // 15: coder.agent.v2.GetServiceBannerRequest - (*Stats)(nil), // 16: coder.agent.v2.Stats - (*UpdateStatsRequest)(nil), // 17: coder.agent.v2.UpdateStatsRequest - (*UpdateStatsResponse)(nil), // 18: coder.agent.v2.UpdateStatsResponse - (*Lifecycle)(nil), // 19: coder.agent.v2.Lifecycle - (*UpdateLifecycleRequest)(nil), // 20: coder.agent.v2.UpdateLifecycleRequest - (*BatchUpdateAppHealthRequest)(nil), // 21: coder.agent.v2.BatchUpdateAppHealthRequest - (*BatchUpdateAppHealthResponse)(nil), // 22: coder.agent.v2.BatchUpdateAppHealthResponse - (*Startup)(nil), // 23: coder.agent.v2.Startup - (*UpdateStartupRequest)(nil), // 24: coder.agent.v2.UpdateStartupRequest - (*Metadata)(nil), // 25: coder.agent.v2.Metadata - (*BatchUpdateMetadataRequest)(nil), // 26: coder.agent.v2.BatchUpdateMetadataRequest - (*BatchUpdateMetadataResponse)(nil), // 27: coder.agent.v2.BatchUpdateMetadataResponse - (*Log)(nil), // 28: coder.agent.v2.Log - (*BatchCreateLogsRequest)(nil), // 29: coder.agent.v2.BatchCreateLogsRequest - (*BatchCreateLogsResponse)(nil), // 30: coder.agent.v2.BatchCreateLogsResponse - (*GetAnnouncementBannersRequest)(nil), // 31: coder.agent.v2.GetAnnouncementBannersRequest - (*GetAnnouncementBannersResponse)(nil), // 32: coder.agent.v2.GetAnnouncementBannersResponse - (*BannerConfig)(nil), // 33: coder.agent.v2.BannerConfig - (*WorkspaceAgentScriptCompletedRequest)(nil), // 34: coder.agent.v2.WorkspaceAgentScriptCompletedRequest - (*WorkspaceAgentScriptCompletedResponse)(nil), // 35: coder.agent.v2.WorkspaceAgentScriptCompletedResponse - (*Timing)(nil), // 36: coder.agent.v2.Timing - (*GetResourcesMonitoringConfigurationRequest)(nil), // 37: coder.agent.v2.GetResourcesMonitoringConfigurationRequest - (*GetResourcesMonitoringConfigurationResponse)(nil), // 38: coder.agent.v2.GetResourcesMonitoringConfigurationResponse - (*PushResourcesMonitoringUsageRequest)(nil), // 39: coder.agent.v2.PushResourcesMonitoringUsageRequest - (*PushResourcesMonitoringUsageResponse)(nil), // 40: coder.agent.v2.PushResourcesMonitoringUsageResponse - (*WorkspaceApp_Healthcheck)(nil), // 41: coder.agent.v2.WorkspaceApp.Healthcheck - (*WorkspaceAgentMetadata_Result)(nil), // 42: coder.agent.v2.WorkspaceAgentMetadata.Result - (*WorkspaceAgentMetadata_Description)(nil), // 43: coder.agent.v2.WorkspaceAgentMetadata.Description - nil, // 44: coder.agent.v2.Manifest.EnvironmentVariablesEntry - nil, // 45: coder.agent.v2.Stats.ConnectionsByProtoEntry - (*Stats_Metric)(nil), // 46: coder.agent.v2.Stats.Metric - (*Stats_Metric_Label)(nil), // 47: coder.agent.v2.Stats.Metric.Label - (*BatchUpdateAppHealthRequest_HealthUpdate)(nil), // 48: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate - (*GetResourcesMonitoringConfigurationResponse_Config)(nil), // 49: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Config - (*GetResourcesMonitoringConfigurationResponse_Memory)(nil), // 50: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Memory - (*GetResourcesMonitoringConfigurationResponse_Volume)(nil), // 51: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Volume - (*PushResourcesMonitoringUsageRequest_Datapoint)(nil), // 52: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint - (*PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage)(nil), // 53: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.MemoryUsage - (*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage)(nil), // 54: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.VolumeUsage - (*durationpb.Duration)(nil), // 55: google.protobuf.Duration - (*proto.DERPMap)(nil), // 56: coder.tailnet.v2.DERPMap - (*timestamppb.Timestamp)(nil), // 57: google.protobuf.Timestamp + (Connection_Action)(0), // 9: coder.agent.v2.Connection.Action + (Connection_Type)(0), // 10: coder.agent.v2.Connection.Type + (*WorkspaceApp)(nil), // 11: coder.agent.v2.WorkspaceApp + (*WorkspaceAgentScript)(nil), // 12: coder.agent.v2.WorkspaceAgentScript + (*WorkspaceAgentMetadata)(nil), // 13: coder.agent.v2.WorkspaceAgentMetadata + (*Manifest)(nil), // 14: coder.agent.v2.Manifest + (*GetManifestRequest)(nil), // 15: coder.agent.v2.GetManifestRequest + (*ServiceBanner)(nil), // 16: coder.agent.v2.ServiceBanner + (*GetServiceBannerRequest)(nil), // 17: coder.agent.v2.GetServiceBannerRequest + (*Stats)(nil), // 18: coder.agent.v2.Stats + (*UpdateStatsRequest)(nil), // 19: coder.agent.v2.UpdateStatsRequest + (*UpdateStatsResponse)(nil), // 20: coder.agent.v2.UpdateStatsResponse + (*Lifecycle)(nil), // 21: coder.agent.v2.Lifecycle + (*UpdateLifecycleRequest)(nil), // 22: coder.agent.v2.UpdateLifecycleRequest + (*BatchUpdateAppHealthRequest)(nil), // 23: coder.agent.v2.BatchUpdateAppHealthRequest + (*BatchUpdateAppHealthResponse)(nil), // 24: coder.agent.v2.BatchUpdateAppHealthResponse + (*Startup)(nil), // 25: coder.agent.v2.Startup + (*UpdateStartupRequest)(nil), // 26: coder.agent.v2.UpdateStartupRequest + (*Metadata)(nil), // 27: coder.agent.v2.Metadata + (*BatchUpdateMetadataRequest)(nil), // 28: coder.agent.v2.BatchUpdateMetadataRequest + (*BatchUpdateMetadataResponse)(nil), // 29: coder.agent.v2.BatchUpdateMetadataResponse + (*Log)(nil), // 30: coder.agent.v2.Log + (*BatchCreateLogsRequest)(nil), // 31: coder.agent.v2.BatchCreateLogsRequest + (*BatchCreateLogsResponse)(nil), // 32: coder.agent.v2.BatchCreateLogsResponse + (*GetAnnouncementBannersRequest)(nil), // 33: coder.agent.v2.GetAnnouncementBannersRequest + (*GetAnnouncementBannersResponse)(nil), // 34: coder.agent.v2.GetAnnouncementBannersResponse + (*BannerConfig)(nil), // 35: coder.agent.v2.BannerConfig + (*WorkspaceAgentScriptCompletedRequest)(nil), // 36: coder.agent.v2.WorkspaceAgentScriptCompletedRequest + (*WorkspaceAgentScriptCompletedResponse)(nil), // 37: coder.agent.v2.WorkspaceAgentScriptCompletedResponse + (*Timing)(nil), // 38: coder.agent.v2.Timing + (*GetResourcesMonitoringConfigurationRequest)(nil), // 39: coder.agent.v2.GetResourcesMonitoringConfigurationRequest + (*GetResourcesMonitoringConfigurationResponse)(nil), // 40: coder.agent.v2.GetResourcesMonitoringConfigurationResponse + (*PushResourcesMonitoringUsageRequest)(nil), // 41: coder.agent.v2.PushResourcesMonitoringUsageRequest + (*PushResourcesMonitoringUsageResponse)(nil), // 42: coder.agent.v2.PushResourcesMonitoringUsageResponse + (*Connection)(nil), // 43: coder.agent.v2.Connection + (*ReportConnectionRequest)(nil), // 44: coder.agent.v2.ReportConnectionRequest + (*WorkspaceApp_Healthcheck)(nil), // 45: coder.agent.v2.WorkspaceApp.Healthcheck + (*WorkspaceAgentMetadata_Result)(nil), // 46: coder.agent.v2.WorkspaceAgentMetadata.Result + (*WorkspaceAgentMetadata_Description)(nil), // 47: coder.agent.v2.WorkspaceAgentMetadata.Description + nil, // 48: coder.agent.v2.Manifest.EnvironmentVariablesEntry + nil, // 49: coder.agent.v2.Stats.ConnectionsByProtoEntry + (*Stats_Metric)(nil), // 50: coder.agent.v2.Stats.Metric + (*Stats_Metric_Label)(nil), // 51: coder.agent.v2.Stats.Metric.Label + (*BatchUpdateAppHealthRequest_HealthUpdate)(nil), // 52: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate + (*GetResourcesMonitoringConfigurationResponse_Config)(nil), // 53: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Config + (*GetResourcesMonitoringConfigurationResponse_Memory)(nil), // 54: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Memory + (*GetResourcesMonitoringConfigurationResponse_Volume)(nil), // 55: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Volume + (*PushResourcesMonitoringUsageRequest_Datapoint)(nil), // 56: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint + (*PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage)(nil), // 57: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.MemoryUsage + (*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage)(nil), // 58: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.VolumeUsage + (*durationpb.Duration)(nil), // 59: google.protobuf.Duration + (*proto.DERPMap)(nil), // 60: coder.tailnet.v2.DERPMap + (*timestamppb.Timestamp)(nil), // 61: google.protobuf.Timestamp + (*emptypb.Empty)(nil), // 62: google.protobuf.Empty } var file_agent_proto_agent_proto_depIdxs = []int32{ 1, // 0: coder.agent.v2.WorkspaceApp.sharing_level:type_name -> coder.agent.v2.WorkspaceApp.SharingLevel - 41, // 1: coder.agent.v2.WorkspaceApp.healthcheck:type_name -> coder.agent.v2.WorkspaceApp.Healthcheck + 45, // 1: coder.agent.v2.WorkspaceApp.healthcheck:type_name -> coder.agent.v2.WorkspaceApp.Healthcheck 2, // 2: coder.agent.v2.WorkspaceApp.health:type_name -> coder.agent.v2.WorkspaceApp.Health - 55, // 3: coder.agent.v2.WorkspaceAgentScript.timeout:type_name -> google.protobuf.Duration - 42, // 4: coder.agent.v2.WorkspaceAgentMetadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result - 43, // 5: coder.agent.v2.WorkspaceAgentMetadata.description:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description - 44, // 6: coder.agent.v2.Manifest.environment_variables:type_name -> coder.agent.v2.Manifest.EnvironmentVariablesEntry - 56, // 7: coder.agent.v2.Manifest.derp_map:type_name -> coder.tailnet.v2.DERPMap - 10, // 8: coder.agent.v2.Manifest.scripts:type_name -> coder.agent.v2.WorkspaceAgentScript - 9, // 9: coder.agent.v2.Manifest.apps:type_name -> coder.agent.v2.WorkspaceApp - 43, // 10: coder.agent.v2.Manifest.metadata:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description - 45, // 11: coder.agent.v2.Stats.connections_by_proto:type_name -> coder.agent.v2.Stats.ConnectionsByProtoEntry - 46, // 12: coder.agent.v2.Stats.metrics:type_name -> coder.agent.v2.Stats.Metric - 16, // 13: coder.agent.v2.UpdateStatsRequest.stats:type_name -> coder.agent.v2.Stats - 55, // 14: coder.agent.v2.UpdateStatsResponse.report_interval:type_name -> google.protobuf.Duration + 59, // 3: coder.agent.v2.WorkspaceAgentScript.timeout:type_name -> google.protobuf.Duration + 46, // 4: coder.agent.v2.WorkspaceAgentMetadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result + 47, // 5: coder.agent.v2.WorkspaceAgentMetadata.description:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description + 48, // 6: coder.agent.v2.Manifest.environment_variables:type_name -> coder.agent.v2.Manifest.EnvironmentVariablesEntry + 60, // 7: coder.agent.v2.Manifest.derp_map:type_name -> coder.tailnet.v2.DERPMap + 12, // 8: coder.agent.v2.Manifest.scripts:type_name -> coder.agent.v2.WorkspaceAgentScript + 11, // 9: coder.agent.v2.Manifest.apps:type_name -> coder.agent.v2.WorkspaceApp + 47, // 10: coder.agent.v2.Manifest.metadata:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description + 49, // 11: coder.agent.v2.Stats.connections_by_proto:type_name -> coder.agent.v2.Stats.ConnectionsByProtoEntry + 50, // 12: coder.agent.v2.Stats.metrics:type_name -> coder.agent.v2.Stats.Metric + 18, // 13: coder.agent.v2.UpdateStatsRequest.stats:type_name -> coder.agent.v2.Stats + 59, // 14: coder.agent.v2.UpdateStatsResponse.report_interval:type_name -> google.protobuf.Duration 4, // 15: coder.agent.v2.Lifecycle.state:type_name -> coder.agent.v2.Lifecycle.State - 57, // 16: coder.agent.v2.Lifecycle.changed_at:type_name -> google.protobuf.Timestamp - 19, // 17: coder.agent.v2.UpdateLifecycleRequest.lifecycle:type_name -> coder.agent.v2.Lifecycle - 48, // 18: coder.agent.v2.BatchUpdateAppHealthRequest.updates:type_name -> coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate + 61, // 16: coder.agent.v2.Lifecycle.changed_at:type_name -> google.protobuf.Timestamp + 21, // 17: coder.agent.v2.UpdateLifecycleRequest.lifecycle:type_name -> coder.agent.v2.Lifecycle + 52, // 18: coder.agent.v2.BatchUpdateAppHealthRequest.updates:type_name -> coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate 5, // 19: coder.agent.v2.Startup.subsystems:type_name -> coder.agent.v2.Startup.Subsystem - 23, // 20: coder.agent.v2.UpdateStartupRequest.startup:type_name -> coder.agent.v2.Startup - 42, // 21: coder.agent.v2.Metadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result - 25, // 22: coder.agent.v2.BatchUpdateMetadataRequest.metadata:type_name -> coder.agent.v2.Metadata - 57, // 23: coder.agent.v2.Log.created_at:type_name -> google.protobuf.Timestamp + 25, // 20: coder.agent.v2.UpdateStartupRequest.startup:type_name -> coder.agent.v2.Startup + 46, // 21: coder.agent.v2.Metadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result + 27, // 22: coder.agent.v2.BatchUpdateMetadataRequest.metadata:type_name -> coder.agent.v2.Metadata + 61, // 23: coder.agent.v2.Log.created_at:type_name -> google.protobuf.Timestamp 6, // 24: coder.agent.v2.Log.level:type_name -> coder.agent.v2.Log.Level - 28, // 25: coder.agent.v2.BatchCreateLogsRequest.logs:type_name -> coder.agent.v2.Log - 33, // 26: coder.agent.v2.GetAnnouncementBannersResponse.announcement_banners:type_name -> coder.agent.v2.BannerConfig - 36, // 27: coder.agent.v2.WorkspaceAgentScriptCompletedRequest.timing:type_name -> coder.agent.v2.Timing - 57, // 28: coder.agent.v2.Timing.start:type_name -> google.protobuf.Timestamp - 57, // 29: coder.agent.v2.Timing.end:type_name -> google.protobuf.Timestamp + 30, // 25: coder.agent.v2.BatchCreateLogsRequest.logs:type_name -> coder.agent.v2.Log + 35, // 26: coder.agent.v2.GetAnnouncementBannersResponse.announcement_banners:type_name -> coder.agent.v2.BannerConfig + 38, // 27: coder.agent.v2.WorkspaceAgentScriptCompletedRequest.timing:type_name -> coder.agent.v2.Timing + 61, // 28: coder.agent.v2.Timing.start:type_name -> google.protobuf.Timestamp + 61, // 29: coder.agent.v2.Timing.end:type_name -> google.protobuf.Timestamp 7, // 30: coder.agent.v2.Timing.stage:type_name -> coder.agent.v2.Timing.Stage 8, // 31: coder.agent.v2.Timing.status:type_name -> coder.agent.v2.Timing.Status - 49, // 32: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.config:type_name -> coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Config - 50, // 33: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.memory:type_name -> coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Memory - 51, // 34: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.volumes:type_name -> coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Volume - 52, // 35: coder.agent.v2.PushResourcesMonitoringUsageRequest.datapoints:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint - 55, // 36: coder.agent.v2.WorkspaceApp.Healthcheck.interval:type_name -> google.protobuf.Duration - 57, // 37: coder.agent.v2.WorkspaceAgentMetadata.Result.collected_at:type_name -> google.protobuf.Timestamp - 55, // 38: coder.agent.v2.WorkspaceAgentMetadata.Description.interval:type_name -> google.protobuf.Duration - 55, // 39: coder.agent.v2.WorkspaceAgentMetadata.Description.timeout:type_name -> google.protobuf.Duration - 3, // 40: coder.agent.v2.Stats.Metric.type:type_name -> coder.agent.v2.Stats.Metric.Type - 47, // 41: coder.agent.v2.Stats.Metric.labels:type_name -> coder.agent.v2.Stats.Metric.Label - 0, // 42: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate.health:type_name -> coder.agent.v2.AppHealth - 57, // 43: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.collected_at:type_name -> google.protobuf.Timestamp - 53, // 44: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.memory:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.MemoryUsage - 54, // 45: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.volumes:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.VolumeUsage - 13, // 46: coder.agent.v2.Agent.GetManifest:input_type -> coder.agent.v2.GetManifestRequest - 15, // 47: coder.agent.v2.Agent.GetServiceBanner:input_type -> coder.agent.v2.GetServiceBannerRequest - 17, // 48: coder.agent.v2.Agent.UpdateStats:input_type -> coder.agent.v2.UpdateStatsRequest - 20, // 49: coder.agent.v2.Agent.UpdateLifecycle:input_type -> coder.agent.v2.UpdateLifecycleRequest - 21, // 50: coder.agent.v2.Agent.BatchUpdateAppHealths:input_type -> coder.agent.v2.BatchUpdateAppHealthRequest - 24, // 51: coder.agent.v2.Agent.UpdateStartup:input_type -> coder.agent.v2.UpdateStartupRequest - 26, // 52: coder.agent.v2.Agent.BatchUpdateMetadata:input_type -> coder.agent.v2.BatchUpdateMetadataRequest - 29, // 53: coder.agent.v2.Agent.BatchCreateLogs:input_type -> coder.agent.v2.BatchCreateLogsRequest - 31, // 54: coder.agent.v2.Agent.GetAnnouncementBanners:input_type -> coder.agent.v2.GetAnnouncementBannersRequest - 34, // 55: coder.agent.v2.Agent.ScriptCompleted:input_type -> coder.agent.v2.WorkspaceAgentScriptCompletedRequest - 37, // 56: coder.agent.v2.Agent.GetResourcesMonitoringConfiguration:input_type -> coder.agent.v2.GetResourcesMonitoringConfigurationRequest - 39, // 57: coder.agent.v2.Agent.PushResourcesMonitoringUsage:input_type -> coder.agent.v2.PushResourcesMonitoringUsageRequest - 12, // 58: coder.agent.v2.Agent.GetManifest:output_type -> coder.agent.v2.Manifest - 14, // 59: coder.agent.v2.Agent.GetServiceBanner:output_type -> coder.agent.v2.ServiceBanner - 18, // 60: coder.agent.v2.Agent.UpdateStats:output_type -> coder.agent.v2.UpdateStatsResponse - 19, // 61: coder.agent.v2.Agent.UpdateLifecycle:output_type -> coder.agent.v2.Lifecycle - 22, // 62: coder.agent.v2.Agent.BatchUpdateAppHealths:output_type -> coder.agent.v2.BatchUpdateAppHealthResponse - 23, // 63: coder.agent.v2.Agent.UpdateStartup:output_type -> coder.agent.v2.Startup - 27, // 64: coder.agent.v2.Agent.BatchUpdateMetadata:output_type -> coder.agent.v2.BatchUpdateMetadataResponse - 30, // 65: coder.agent.v2.Agent.BatchCreateLogs:output_type -> coder.agent.v2.BatchCreateLogsResponse - 32, // 66: coder.agent.v2.Agent.GetAnnouncementBanners:output_type -> coder.agent.v2.GetAnnouncementBannersResponse - 35, // 67: coder.agent.v2.Agent.ScriptCompleted:output_type -> coder.agent.v2.WorkspaceAgentScriptCompletedResponse - 38, // 68: coder.agent.v2.Agent.GetResourcesMonitoringConfiguration:output_type -> coder.agent.v2.GetResourcesMonitoringConfigurationResponse - 40, // 69: coder.agent.v2.Agent.PushResourcesMonitoringUsage:output_type -> coder.agent.v2.PushResourcesMonitoringUsageResponse - 58, // [58:70] is the sub-list for method output_type - 46, // [46:58] is the sub-list for method input_type - 46, // [46:46] is the sub-list for extension type_name - 46, // [46:46] is the sub-list for extension extendee - 0, // [0:46] is the sub-list for field type_name + 53, // 32: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.config:type_name -> coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Config + 54, // 33: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.memory:type_name -> coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Memory + 55, // 34: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.volumes:type_name -> coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Volume + 56, // 35: coder.agent.v2.PushResourcesMonitoringUsageRequest.datapoints:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint + 9, // 36: coder.agent.v2.Connection.action:type_name -> coder.agent.v2.Connection.Action + 10, // 37: coder.agent.v2.Connection.type:type_name -> coder.agent.v2.Connection.Type + 61, // 38: coder.agent.v2.Connection.timestamp:type_name -> google.protobuf.Timestamp + 43, // 39: coder.agent.v2.ReportConnectionRequest.connection:type_name -> coder.agent.v2.Connection + 59, // 40: coder.agent.v2.WorkspaceApp.Healthcheck.interval:type_name -> google.protobuf.Duration + 61, // 41: coder.agent.v2.WorkspaceAgentMetadata.Result.collected_at:type_name -> google.protobuf.Timestamp + 59, // 42: coder.agent.v2.WorkspaceAgentMetadata.Description.interval:type_name -> google.protobuf.Duration + 59, // 43: coder.agent.v2.WorkspaceAgentMetadata.Description.timeout:type_name -> google.protobuf.Duration + 3, // 44: coder.agent.v2.Stats.Metric.type:type_name -> coder.agent.v2.Stats.Metric.Type + 51, // 45: coder.agent.v2.Stats.Metric.labels:type_name -> coder.agent.v2.Stats.Metric.Label + 0, // 46: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate.health:type_name -> coder.agent.v2.AppHealth + 61, // 47: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.collected_at:type_name -> google.protobuf.Timestamp + 57, // 48: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.memory:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.MemoryUsage + 58, // 49: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.volumes:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.VolumeUsage + 15, // 50: coder.agent.v2.Agent.GetManifest:input_type -> coder.agent.v2.GetManifestRequest + 17, // 51: coder.agent.v2.Agent.GetServiceBanner:input_type -> coder.agent.v2.GetServiceBannerRequest + 19, // 52: coder.agent.v2.Agent.UpdateStats:input_type -> coder.agent.v2.UpdateStatsRequest + 22, // 53: coder.agent.v2.Agent.UpdateLifecycle:input_type -> coder.agent.v2.UpdateLifecycleRequest + 23, // 54: coder.agent.v2.Agent.BatchUpdateAppHealths:input_type -> coder.agent.v2.BatchUpdateAppHealthRequest + 26, // 55: coder.agent.v2.Agent.UpdateStartup:input_type -> coder.agent.v2.UpdateStartupRequest + 28, // 56: coder.agent.v2.Agent.BatchUpdateMetadata:input_type -> coder.agent.v2.BatchUpdateMetadataRequest + 31, // 57: coder.agent.v2.Agent.BatchCreateLogs:input_type -> coder.agent.v2.BatchCreateLogsRequest + 33, // 58: coder.agent.v2.Agent.GetAnnouncementBanners:input_type -> coder.agent.v2.GetAnnouncementBannersRequest + 36, // 59: coder.agent.v2.Agent.ScriptCompleted:input_type -> coder.agent.v2.WorkspaceAgentScriptCompletedRequest + 39, // 60: coder.agent.v2.Agent.GetResourcesMonitoringConfiguration:input_type -> coder.agent.v2.GetResourcesMonitoringConfigurationRequest + 41, // 61: coder.agent.v2.Agent.PushResourcesMonitoringUsage:input_type -> coder.agent.v2.PushResourcesMonitoringUsageRequest + 44, // 62: coder.agent.v2.Agent.ReportConnection:input_type -> coder.agent.v2.ReportConnectionRequest + 14, // 63: coder.agent.v2.Agent.GetManifest:output_type -> coder.agent.v2.Manifest + 16, // 64: coder.agent.v2.Agent.GetServiceBanner:output_type -> coder.agent.v2.ServiceBanner + 20, // 65: coder.agent.v2.Agent.UpdateStats:output_type -> coder.agent.v2.UpdateStatsResponse + 21, // 66: coder.agent.v2.Agent.UpdateLifecycle:output_type -> coder.agent.v2.Lifecycle + 24, // 67: coder.agent.v2.Agent.BatchUpdateAppHealths:output_type -> coder.agent.v2.BatchUpdateAppHealthResponse + 25, // 68: coder.agent.v2.Agent.UpdateStartup:output_type -> coder.agent.v2.Startup + 29, // 69: coder.agent.v2.Agent.BatchUpdateMetadata:output_type -> coder.agent.v2.BatchUpdateMetadataResponse + 32, // 70: coder.agent.v2.Agent.BatchCreateLogs:output_type -> coder.agent.v2.BatchCreateLogsResponse + 34, // 71: coder.agent.v2.Agent.GetAnnouncementBanners:output_type -> coder.agent.v2.GetAnnouncementBannersResponse + 37, // 72: coder.agent.v2.Agent.ScriptCompleted:output_type -> coder.agent.v2.WorkspaceAgentScriptCompletedResponse + 40, // 73: coder.agent.v2.Agent.GetResourcesMonitoringConfiguration:output_type -> coder.agent.v2.GetResourcesMonitoringConfigurationResponse + 42, // 74: coder.agent.v2.Agent.PushResourcesMonitoringUsage:output_type -> coder.agent.v2.PushResourcesMonitoringUsageResponse + 62, // 75: coder.agent.v2.Agent.ReportConnection:output_type -> google.protobuf.Empty + 63, // [63:76] is the sub-list for method output_type + 50, // [50:63] is the sub-list for method input_type + 50, // [50:50] is the sub-list for extension type_name + 50, // [50:50] is the sub-list for extension extendee + 0, // [0:50] is the sub-list for field type_name } func init() { file_agent_proto_agent_proto_init() } @@ -4328,7 +4626,7 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WorkspaceApp_Healthcheck); i { + switch v := v.(*Connection); i { case 0: return &v.state case 1: @@ -4340,7 +4638,7 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WorkspaceAgentMetadata_Result); i { + switch v := v.(*ReportConnectionRequest); i { case 0: return &v.state case 1: @@ -4352,6 +4650,30 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WorkspaceApp_Healthcheck); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_agent_proto_agent_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WorkspaceAgentMetadata_Result); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_agent_proto_agent_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*WorkspaceAgentMetadata_Description); i { case 0: return &v.state @@ -4363,7 +4685,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Stats_Metric); i { case 0: return &v.state @@ -4375,7 +4697,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Stats_Metric_Label); i { case 0: return &v.state @@ -4387,7 +4709,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[41].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*BatchUpdateAppHealthRequest_HealthUpdate); i { case 0: return &v.state @@ -4399,7 +4721,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[42].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GetResourcesMonitoringConfigurationResponse_Config); i { case 0: return &v.state @@ -4411,7 +4733,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[41].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[43].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GetResourcesMonitoringConfigurationResponse_Memory); i { case 0: return &v.state @@ -4423,7 +4745,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[42].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[44].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GetResourcesMonitoringConfigurationResponse_Volume); i { case 0: return &v.state @@ -4435,7 +4757,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[43].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[45].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PushResourcesMonitoringUsageRequest_Datapoint); i { case 0: return &v.state @@ -4447,7 +4769,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[44].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[46].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage); i { case 0: return &v.state @@ -4459,7 +4781,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[45].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[47].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage); i { case 0: return &v.state @@ -4473,14 +4795,15 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[29].OneofWrappers = []interface{}{} - file_agent_proto_agent_proto_msgTypes[43].OneofWrappers = []interface{}{} + file_agent_proto_agent_proto_msgTypes[32].OneofWrappers = []interface{}{} + file_agent_proto_agent_proto_msgTypes[45].OneofWrappers = []interface{}{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_agent_proto_agent_proto_rawDesc, - NumEnums: 9, - NumMessages: 46, + NumEnums: 11, + NumMessages: 48, NumExtensions: 0, NumServices: 1, }, diff --git a/agent/proto/agent.proto b/agent/proto/agent.proto index 6bb802d966..1e59c109ea 100644 --- a/agent/proto/agent.proto +++ b/agent/proto/agent.proto @@ -6,6 +6,7 @@ package coder.agent.v2; import "tailnet/proto/tailnet.proto"; import "google/protobuf/timestamp.proto"; import "google/protobuf/duration.proto"; +import "google/protobuf/empty.proto"; message WorkspaceApp { bytes id = 1; @@ -340,6 +341,33 @@ message PushResourcesMonitoringUsageRequest { message PushResourcesMonitoringUsageResponse { } +message Connection { + enum Action { + ACTION_UNSPECIFIED = 0; + CONNECT = 1; + DISCONNECT = 2; + } + enum Type { + TYPE_UNSPECIFIED = 0; + SSH = 1; + VSCODE = 2; + JETBRAINS = 3; + RECONNECTING_PTY = 4; + } + + bytes id = 1; + Action action = 2; + Type type = 3; + google.protobuf.Timestamp timestamp = 4; + string ip = 5; + int32 status_code = 6; + optional string reason = 7; +} + +message ReportConnectionRequest { + Connection connection = 1; +} + service Agent { rpc GetManifest(GetManifestRequest) returns (Manifest); rpc GetServiceBanner(GetServiceBannerRequest) returns (ServiceBanner); @@ -353,4 +381,5 @@ service Agent { rpc ScriptCompleted(WorkspaceAgentScriptCompletedRequest) returns (WorkspaceAgentScriptCompletedResponse); rpc GetResourcesMonitoringConfiguration(GetResourcesMonitoringConfigurationRequest) returns (GetResourcesMonitoringConfigurationResponse); rpc PushResourcesMonitoringUsage(PushResourcesMonitoringUsageRequest) returns (PushResourcesMonitoringUsageResponse); + rpc ReportConnection(ReportConnectionRequest) returns (google.protobuf.Empty); } diff --git a/agent/proto/agent_drpc.pb.go b/agent/proto/agent_drpc.pb.go index 2a90380185..a9dd8cda72 100644 --- a/agent/proto/agent_drpc.pb.go +++ b/agent/proto/agent_drpc.pb.go @@ -9,6 +9,7 @@ import ( errors "errors" protojson "google.golang.org/protobuf/encoding/protojson" proto "google.golang.org/protobuf/proto" + emptypb "google.golang.org/protobuf/types/known/emptypb" drpc "storj.io/drpc" drpcerr "storj.io/drpc/drpcerr" ) @@ -50,6 +51,7 @@ type DRPCAgentClient interface { ScriptCompleted(ctx context.Context, in *WorkspaceAgentScriptCompletedRequest) (*WorkspaceAgentScriptCompletedResponse, error) GetResourcesMonitoringConfiguration(ctx context.Context, in *GetResourcesMonitoringConfigurationRequest) (*GetResourcesMonitoringConfigurationResponse, error) PushResourcesMonitoringUsage(ctx context.Context, in *PushResourcesMonitoringUsageRequest) (*PushResourcesMonitoringUsageResponse, error) + ReportConnection(ctx context.Context, in *ReportConnectionRequest) (*emptypb.Empty, error) } type drpcAgentClient struct { @@ -170,6 +172,15 @@ func (c *drpcAgentClient) PushResourcesMonitoringUsage(ctx context.Context, in * return out, nil } +func (c *drpcAgentClient) ReportConnection(ctx context.Context, in *ReportConnectionRequest) (*emptypb.Empty, error) { + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, "/coder.agent.v2.Agent/ReportConnection", drpcEncoding_File_agent_proto_agent_proto{}, in, out) + if err != nil { + return nil, err + } + return out, nil +} + type DRPCAgentServer interface { GetManifest(context.Context, *GetManifestRequest) (*Manifest, error) GetServiceBanner(context.Context, *GetServiceBannerRequest) (*ServiceBanner, error) @@ -183,6 +194,7 @@ type DRPCAgentServer interface { ScriptCompleted(context.Context, *WorkspaceAgentScriptCompletedRequest) (*WorkspaceAgentScriptCompletedResponse, error) GetResourcesMonitoringConfiguration(context.Context, *GetResourcesMonitoringConfigurationRequest) (*GetResourcesMonitoringConfigurationResponse, error) PushResourcesMonitoringUsage(context.Context, *PushResourcesMonitoringUsageRequest) (*PushResourcesMonitoringUsageResponse, error) + ReportConnection(context.Context, *ReportConnectionRequest) (*emptypb.Empty, error) } type DRPCAgentUnimplementedServer struct{} @@ -235,9 +247,13 @@ func (s *DRPCAgentUnimplementedServer) PushResourcesMonitoringUsage(context.Cont return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) } +func (s *DRPCAgentUnimplementedServer) ReportConnection(context.Context, *ReportConnectionRequest) (*emptypb.Empty, error) { + return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) +} + type DRPCAgentDescription struct{} -func (DRPCAgentDescription) NumMethods() int { return 12 } +func (DRPCAgentDescription) NumMethods() int { return 13 } func (DRPCAgentDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) { switch n { @@ -349,6 +365,15 @@ func (DRPCAgentDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, in1.(*PushResourcesMonitoringUsageRequest), ) }, DRPCAgentServer.PushResourcesMonitoringUsage, true + case 12: + return "/coder.agent.v2.Agent/ReportConnection", drpcEncoding_File_agent_proto_agent_proto{}, + func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { + return srv.(DRPCAgentServer). + ReportConnection( + ctx, + in1.(*ReportConnectionRequest), + ) + }, DRPCAgentServer.ReportConnection, true default: return "", nil, nil, nil, false } @@ -549,3 +574,19 @@ func (x *drpcAgent_PushResourcesMonitoringUsageStream) SendAndClose(m *PushResou } return x.CloseSend() } + +type DRPCAgent_ReportConnectionStream interface { + drpc.Stream + SendAndClose(*emptypb.Empty) error +} + +type drpcAgent_ReportConnectionStream struct { + drpc.Stream +} + +func (x *drpcAgent_ReportConnectionStream) SendAndClose(m *emptypb.Empty) error { + if err := x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil { + return err + } + return x.CloseSend() +} diff --git a/agent/proto/agent_drpc_old.go b/agent/proto/agent_drpc_old.go index f1db351428..63b666a259 100644 --- a/agent/proto/agent_drpc_old.go +++ b/agent/proto/agent_drpc_old.go @@ -3,6 +3,7 @@ package proto import ( "context" + emptypb "google.golang.org/protobuf/types/known/emptypb" "storj.io/drpc" ) @@ -41,10 +42,11 @@ type DRPCAgentClient23 interface { ScriptCompleted(ctx context.Context, in *WorkspaceAgentScriptCompletedRequest) (*WorkspaceAgentScriptCompletedResponse, error) } -// DRPCAgentClient24 is the Agent API at v2.4. It adds the GetResourcesMonitoringConfiguration and -// PushResourcesMonitoringUsage RPCs. Compatible with Coder v2.19+ +// DRPCAgentClient24 is the Agent API at v2.4. It adds the GetResourcesMonitoringConfiguration, +// PushResourcesMonitoringUsage and ReportConnection RPCs. Compatible with Coder v2.19+ type DRPCAgentClient24 interface { DRPCAgentClient23 GetResourcesMonitoringConfiguration(ctx context.Context, in *GetResourcesMonitoringConfigurationRequest) (*GetResourcesMonitoringConfigurationResponse, error) PushResourcesMonitoringUsage(ctx context.Context, in *PushResourcesMonitoringUsageRequest) (*PushResourcesMonitoringUsageResponse, error) + ReportConnection(ctx context.Context, in *ReportConnectionRequest) (*emptypb.Empty, error) } diff --git a/coderd/agentapi/api.go b/coderd/agentapi/api.go index aae67d9185..97e9bfdeb9 100644 --- a/coderd/agentapi/api.go +++ b/coderd/agentapi/api.go @@ -19,6 +19,7 @@ import ( agentproto "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/agentapi/resourcesmonitor" "github.com/coder/coder/v2/coderd/appearance" + "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/pubsub" "github.com/coder/coder/v2/coderd/externalauth" @@ -48,6 +49,7 @@ type API struct { *ResourcesMonitoringAPI *LogsAPI *ScriptsAPI + *AuditAPI *tailnet.DRPCService mu sync.Mutex @@ -66,6 +68,7 @@ type Options struct { Database database.Store NotificationsEnqueuer notifications.Enqueuer Pubsub pubsub.Pubsub + Auditor *atomic.Pointer[audit.Auditor] DerpMapFn func() *tailcfg.DERPMap TailnetCoordinator *atomic.Pointer[tailnet.Coordinator] StatsReporter *workspacestats.Reporter @@ -176,6 +179,13 @@ func New(opts Options) *API { Database: opts.Database, } + api.AuditAPI = &AuditAPI{ + AgentFn: api.agent, + Auditor: opts.Auditor, + Database: opts.Database, + Log: opts.Log, + } + api.DRPCService = &tailnet.DRPCService{ CoordPtr: opts.TailnetCoordinator, Logger: opts.Log, diff --git a/coderd/agentapi/audit.go b/coderd/agentapi/audit.go new file mode 100644 index 0000000000..2025b2d6cd --- /dev/null +++ b/coderd/agentapi/audit.go @@ -0,0 +1,105 @@ +package agentapi + +import ( + "context" + "encoding/json" + "strconv" + "sync/atomic" + + "github.com/google/uuid" + "golang.org/x/xerrors" + "google.golang.org/protobuf/types/known/emptypb" + + "cdr.dev/slog" + + agentproto "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/coderd/audit" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/db2sdk" + "github.com/coder/coder/v2/codersdk/agentsdk" +) + +type AuditAPI struct { + AgentFn func(context.Context) (database.WorkspaceAgent, error) + Auditor *atomic.Pointer[audit.Auditor] + Database database.Store + Log slog.Logger +} + +func (a *AuditAPI) ReportConnection(ctx context.Context, req *agentproto.ReportConnectionRequest) (*emptypb.Empty, error) { + // We will use connection ID as request ID, typically this is the + // SSH session ID as reported by the agent. + connectionID, err := uuid.FromBytes(req.GetConnection().GetId()) + if err != nil { + return nil, xerrors.Errorf("connection id from bytes: %w", err) + } + + action, err := db2sdk.AuditActionFromAgentProtoConnectionAction(req.GetConnection().GetAction()) + if err != nil { + return nil, err + } + connectionType, err := agentsdk.ConnectionTypeFromProto(req.GetConnection().GetType()) + if err != nil { + return nil, err + } + + // Fetch contextual data for this audit event. + workspaceAgent, err := a.AgentFn(ctx) + if err != nil { + return nil, xerrors.Errorf("get agent: %w", err) + } + workspace, err := a.Database.GetWorkspaceByAgentID(ctx, workspaceAgent.ID) + if err != nil { + return nil, xerrors.Errorf("get workspace by agent id: %w", err) + } + build, err := a.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) + if err != nil { + return nil, xerrors.Errorf("get latest workspace build by workspace id: %w", err) + } + + // We pass the below information to the Auditor so that it + // can form a friendly string for the user to view in the UI. + type additionalFields struct { + audit.AdditionalFields + + ConnectionType agentsdk.ConnectionType `json:"connection_type"` + Reason string `json:"reason,omitempty"` + } + resourceInfo := additionalFields{ + AdditionalFields: audit.AdditionalFields{ + WorkspaceID: workspace.ID, + WorkspaceName: workspace.Name, + WorkspaceOwner: workspace.OwnerUsername, + BuildNumber: strconv.FormatInt(int64(build.BuildNumber), 10), + BuildReason: database.BuildReason(string(build.Reason)), + }, + ConnectionType: connectionType, + Reason: req.GetConnection().GetReason(), + } + + riBytes, err := json.Marshal(resourceInfo) + if err != nil { + a.Log.Error(ctx, "marshal resource info for agent connection failed", slog.Error(err)) + riBytes = []byte("{}") + } + + audit.BackgroundAudit(ctx, &audit.BackgroundAuditParams[database.WorkspaceAgent]{ + Audit: *a.Auditor.Load(), + Log: a.Log, + Time: req.GetConnection().GetTimestamp().AsTime(), + OrganizationID: workspace.OrganizationID, + RequestID: connectionID, + Action: action, + New: workspaceAgent, + Old: workspaceAgent, + IP: req.GetConnection().GetIp(), + Status: int(req.GetConnection().GetStatusCode()), + AdditionalFields: riBytes, + + // It's not possible to tell which user connected. Once we have + // the capability, this may be reported by the agent. + UserID: uuid.Nil, + }) + + return &emptypb.Empty{}, nil +} diff --git a/coderd/agentapi/audit_test.go b/coderd/agentapi/audit_test.go new file mode 100644 index 0000000000..8b4ae3ea60 --- /dev/null +++ b/coderd/agentapi/audit_test.go @@ -0,0 +1,179 @@ +package agentapi_test + +import ( + "context" + "encoding/json" + "net" + "sync/atomic" + "testing" + "time" + + "github.com/google/uuid" + "github.com/sqlc-dev/pqtype" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "google.golang.org/protobuf/types/known/timestamppb" + + agentproto "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/coderd/agentapi" + "github.com/coder/coder/v2/coderd/audit" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/db2sdk" + "github.com/coder/coder/v2/coderd/database/dbmock" + "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/codersdk/agentsdk" +) + +func TestAuditReport(t *testing.T) { + t.Parallel() + + var ( + owner = database.User{ + ID: uuid.New(), + Username: "cool-user", + } + workspace = database.Workspace{ + ID: uuid.New(), + OrganizationID: uuid.New(), + OwnerID: owner.ID, + Name: "cool-workspace", + } + build = database.WorkspaceBuild{ + ID: uuid.New(), + WorkspaceID: workspace.ID, + } + agent = database.WorkspaceAgent{ + ID: uuid.New(), + } + ) + + tests := []struct { + name string + id uuid.UUID + action *agentproto.Connection_Action + typ *agentproto.Connection_Type + time time.Time + ip string + status int32 + reason string + }{ + { + name: "SSH Connect", + id: uuid.New(), + action: agentproto.Connection_CONNECT.Enum(), + typ: agentproto.Connection_SSH.Enum(), + time: time.Now(), + ip: "127.0.0.1", + status: 200, + }, + { + name: "VS Code Connect", + id: uuid.New(), + action: agentproto.Connection_CONNECT.Enum(), + typ: agentproto.Connection_VSCODE.Enum(), + time: time.Now(), + ip: "8.8.8.8", + }, + { + name: "JetBrains Connect", + id: uuid.New(), + action: agentproto.Connection_CONNECT.Enum(), + typ: agentproto.Connection_JETBRAINS.Enum(), + time: time.Now(), + }, + { + name: "Reconnecting PTY Connect", + id: uuid.New(), + action: agentproto.Connection_CONNECT.Enum(), + typ: agentproto.Connection_RECONNECTING_PTY.Enum(), + time: time.Now(), + }, + { + name: "SSH Disconnect", + id: uuid.New(), + action: agentproto.Connection_DISCONNECT.Enum(), + typ: agentproto.Connection_SSH.Enum(), + time: time.Now(), + }, + { + name: "SSH Disconnect", + id: uuid.New(), + action: agentproto.Connection_DISCONNECT.Enum(), + typ: agentproto.Connection_SSH.Enum(), + time: time.Now(), + status: 500, + reason: "because error says so", + }, + } + //nolint:paralleltest // No longer necessary to reinitialise the variable tt. + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + mAudit := audit.NewMock() + + mDB := dbmock.NewMockStore(gomock.NewController(t)) + mDB.EXPECT().GetWorkspaceByAgentID(gomock.Any(), agent.ID).Return(workspace, nil) + mDB.EXPECT().GetLatestWorkspaceBuildByWorkspaceID(gomock.Any(), workspace.ID).Return(build, nil) + + api := &agentapi.AuditAPI{ + Auditor: asAtomicPointer[audit.Auditor](mAudit), + Database: mDB, + AgentFn: func(context.Context) (database.WorkspaceAgent, error) { + return agent, nil + }, + } + api.ReportConnection(context.Background(), &agentproto.ReportConnectionRequest{ + Connection: &agentproto.Connection{ + Id: tt.id[:], + Action: *tt.action, + Type: *tt.typ, + Timestamp: timestamppb.New(tt.time), + Ip: tt.ip, + StatusCode: tt.status, + Reason: &tt.reason, + }, + }) + + mAudit.Contains(t, database.AuditLog{ + Time: dbtime.Time(tt.time).In(time.UTC), + Action: agentProtoConnectionActionToAudit(t, *tt.action), + OrganizationID: workspace.OrganizationID, + UserID: uuid.Nil, + RequestID: tt.id, + ResourceType: database.ResourceTypeWorkspaceAgent, + ResourceID: agent.ID, + ResourceTarget: agent.Name, + Ip: pqtype.Inet{Valid: true, IPNet: net.IPNet{IP: net.ParseIP(tt.ip), Mask: net.CIDRMask(32, 32)}}, + StatusCode: tt.status, + }) + + // Check some additional fields. + var m map[string]any + err := json.Unmarshal(mAudit.AuditLogs()[0].AdditionalFields, &m) + require.NoError(t, err) + require.Equal(t, string(agentProtoConnectionTypeToSDK(t, *tt.typ)), m["connection_type"].(string)) + if tt.reason != "" { + require.Equal(t, tt.reason, m["reason"]) + } + }) + } +} + +func agentProtoConnectionActionToAudit(t *testing.T, action agentproto.Connection_Action) database.AuditAction { + a, err := db2sdk.AuditActionFromAgentProtoConnectionAction(action) + require.NoError(t, err) + return a +} + +func agentProtoConnectionTypeToSDK(t *testing.T, typ agentproto.Connection_Type) agentsdk.ConnectionType { + action, err := agentsdk.ConnectionTypeFromProto(typ) + require.NoError(t, err) + return action +} + +func asAtomicPointer[T any](v T) *atomic.Pointer[T] { + var p atomic.Pointer[T] + p.Store(&v) + return &p +} diff --git a/coderd/audit/request.go b/coderd/audit/request.go index 3ed6891f12..1621c91762 100644 --- a/coderd/audit/request.go +++ b/coderd/audit/request.go @@ -9,6 +9,7 @@ import ( "net" "net/http" "strconv" + "time" "github.com/google/uuid" "github.com/sqlc-dev/pqtype" @@ -65,6 +66,7 @@ type BackgroundAuditParams[T Auditable] struct { UserID uuid.UUID RequestID uuid.UUID + Time time.Time Status int Action database.AuditAction OrganizationID uuid.UUID @@ -461,13 +463,19 @@ func BackgroundAudit[T Auditable](ctx context.Context, p *BackgroundAuditParams[ diffRaw = []byte("{}") } + if p.Time.IsZero() { + p.Time = dbtime.Now() + } else { + // NOTE(mafredri): dbtime.Time does not currently enforce UTC. + p.Time = dbtime.Time(p.Time.In(time.UTC)) + } if p.AdditionalFields == nil { p.AdditionalFields = json.RawMessage("{}") } auditLog := database.AuditLog{ ID: uuid.New(), - Time: dbtime.Now(), + Time: p.Time, UserID: p.UserID, OrganizationID: requireOrgID[T](ctx, p.OrganizationID, p.Log), Ip: ip, diff --git a/coderd/coderd.go b/coderd/coderd.go index ec15428a1c..05b094f30f 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -933,6 +933,25 @@ func New(options *Options) *API { r.Route("/audit", func(r chi.Router) { r.Use( apiKeyMiddleware, + // This middleware only checks the site and orgs for the audit_log read + // permission. + // In the future if it makes sense to have this permission on the user as + // well we will need to update this middleware to include that check. + func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + if api.Authorize(r, policy.ActionRead, rbac.ResourceAuditLog) { + next.ServeHTTP(rw, r) + return + } + + if api.Authorize(r, policy.ActionRead, rbac.ResourceAuditLog.AnyOrganization()) { + next.ServeHTTP(rw, r) + return + } + + httpapi.Forbidden(rw) + }) + }, ) r.Get("/", api.auditLogs) diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index 8d2a75960b..2249e0c9f3 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -15,6 +15,7 @@ import ( "golang.org/x/xerrors" "tailscale.com/tailcfg" + agentproto "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" @@ -705,3 +706,26 @@ func TemplateRoleActions(role codersdk.TemplateRole) []policy.Action { } return []policy.Action{} } + +func AuditActionFromAgentProtoConnectionAction(action agentproto.Connection_Action) (database.AuditAction, error) { + switch action { + case agentproto.Connection_CONNECT: + return database.AuditActionConnect, nil + case agentproto.Connection_DISCONNECT: + return database.AuditActionDisconnect, nil + default: + // Also Connection_ACTION_UNSPECIFIED, no mapping. + return "", xerrors.Errorf("unknown agent connection action %q", action) + } +} + +func AgentProtoConnectionActionToAuditAction(action database.AuditAction) (agentproto.Connection_Action, error) { + switch action { + case database.AuditActionConnect: + return agentproto.Connection_CONNECT, nil + case database.AuditActionDisconnect: + return agentproto.Connection_DISCONNECT, nil + default: + return agentproto.Connection_ACTION_UNSPECIFIED, xerrors.Errorf("unknown agent connection action %q", action) + } +} diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 3bf63c3300..c960f06c65 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -3918,7 +3918,8 @@ func (s *MethodTestSuite) TestSystemFunctions() { s.Run("InsertWorkspaceAgent", s.Subtest(func(db database.Store, check *expects) { dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) check.Args(database.InsertWorkspaceAgentParams{ - ID: uuid.New(), + ID: uuid.New(), + Name: "dev", }).Asserts(rbac.ResourceSystem, policy.ActionCreate) })) s.Run("InsertWorkspaceApp", s.Subtest(func(db database.Store, check *expects) { diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 9c5a09f40f..197502ebac 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -91,7 +91,8 @@ func (b WorkspaceBuildBuilder) WithAgent(mutations ...func([]*sdkproto.Agent) [] //nolint: revive // returns modified struct b.agentToken = uuid.NewString() agents := []*sdkproto.Agent{{ - Id: uuid.NewString(), + Id: uuid.NewString(), + Name: "dev", Auth: &sdkproto.Agent_Token{ Token: b.agentToken, }, diff --git a/coderd/presets_test.go b/coderd/presets_test.go index 96d1a03e94..08ff7c76f2 100644 --- a/coderd/presets_test.go +++ b/coderd/presets_test.go @@ -17,58 +17,128 @@ import ( func TestTemplateVersionPresets(t *testing.T) { t.Parallel() - givenPreset := codersdk.Preset{ - Name: "My Preset", - Parameters: []codersdk.PresetParameter{ - { - Name: "preset_param1", - Value: "A1B2C3", + testCases := []struct { + name string + presets []codersdk.Preset + }{ + { + name: "no presets", + presets: []codersdk.Preset{}, + }, + { + name: "single preset with parameters", + presets: []codersdk.Preset{ + { + Name: "My Preset", + Parameters: []codersdk.PresetParameter{ + { + Name: "preset_param1", + Value: "A1B2C3", + }, + { + Name: "preset_param2", + Value: "D4E5F6", + }, + }, + }, }, - { - Name: "preset_param2", - Value: "D4E5F6", + }, + { + name: "multiple presets with overlapping parameters", + presets: []codersdk.Preset{ + { + Name: "Preset 1", + Parameters: []codersdk.PresetParameter{ + { + Name: "shared_param", + Value: "value1", + }, + { + Name: "unique_param1", + Value: "unique1", + }, + }, + }, + { + Name: "Preset 2", + Parameters: []codersdk.PresetParameter{ + { + Name: "shared_param", + Value: "value2", + }, + { + Name: "unique_param2", + Value: "unique2", + }, + }, + }, }, }, } - ctx := testutil.Context(t, testutil.WaitShort) - client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) - // nolint:gocritic // This is a test - provisionerCtx := dbauthz.AsProvisionerd(ctx) + client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - dbPreset, err := db.InsertPreset(provisionerCtx, database.InsertPresetParams{ - Name: givenPreset.Name, - TemplateVersionID: version.ID, - }) - require.NoError(t, err) + // nolint:gocritic // This is a test + provisionerCtx := dbauthz.AsProvisionerd(ctx) - var presetParameterNames []string - var presetParameterValues []string - for _, presetParameter := range givenPreset.Parameters { - presetParameterNames = append(presetParameterNames, presetParameter.Name) - presetParameterValues = append(presetParameterValues, presetParameter.Value) - } - _, err = db.InsertPresetParameters(provisionerCtx, database.InsertPresetParametersParams{ - TemplateVersionPresetID: dbPreset.ID, - Names: presetParameterNames, - Values: presetParameterValues, - }) - require.NoError(t, err) + // Insert all presets for this test case + for _, givenPreset := range tc.presets { + dbPreset, err := db.InsertPreset(provisionerCtx, database.InsertPresetParams{ + Name: givenPreset.Name, + TemplateVersionID: version.ID, + }) + require.NoError(t, err) - userSubject, _, err := httpmw.UserRBACSubject(ctx, db, user.UserID, rbac.ScopeAll) - require.NoError(t, err) - userCtx := dbauthz.As(ctx, userSubject) + if len(givenPreset.Parameters) > 0 { + var presetParameterNames []string + var presetParameterValues []string + for _, presetParameter := range givenPreset.Parameters { + presetParameterNames = append(presetParameterNames, presetParameter.Name) + presetParameterValues = append(presetParameterValues, presetParameter.Value) + } + _, err = db.InsertPresetParameters(provisionerCtx, database.InsertPresetParametersParams{ + TemplateVersionPresetID: dbPreset.ID, + Names: presetParameterNames, + Values: presetParameterValues, + }) + require.NoError(t, err) + } + } - gotPresets, err := client.TemplateVersionPresets(userCtx, version.ID) - require.NoError(t, err) + userSubject, _, err := httpmw.UserRBACSubject(ctx, db, user.UserID, rbac.ScopeAll) + require.NoError(t, err) + userCtx := dbauthz.As(ctx, userSubject) - require.Equal(t, 1, len(gotPresets)) - require.Equal(t, givenPreset.Name, gotPresets[0].Name) + gotPresets, err := client.TemplateVersionPresets(userCtx, version.ID) + require.NoError(t, err) - for _, presetParameter := range givenPreset.Parameters { - require.Contains(t, gotPresets[0].Parameters, presetParameter) + require.Equal(t, len(tc.presets), len(gotPresets)) + + for _, expectedPreset := range tc.presets { + found := false + for _, gotPreset := range gotPresets { + if gotPreset.Name == expectedPreset.Name { + found = true + + // verify not only that we get the right number of parameters, but that we get the right parameters + // This ensures that we don't get extra parameters from other presets + require.Equal(t, len(expectedPreset.Parameters), len(gotPreset.Parameters)) + for _, expectedParam := range expectedPreset.Parameters { + require.Contains(t, gotPreset.Parameters, expectedParam) + } + break + } + } + require.True(t, found, "Expected preset %s not found in results", expectedPreset.Name) + } + }) } } diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 16e7132e53..b1ae055324 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1932,10 +1932,25 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. appSlugs = make(map[string]struct{}) ) for _, prAgent := range protoResource.Agents { - if _, ok := agentNames[prAgent.Name]; ok { + // Similar logic is duplicated in terraform/resources.go. + if prAgent.Name == "" { + return xerrors.Errorf("agent name cannot be empty") + } + // In 2025-02 we removed support for underscores in agent names. To + // provide a nicer error message, we check the regex first and check + // for underscores if it fails. + if !provisioner.AgentNameRegex.MatchString(prAgent.Name) { + if strings.Contains(prAgent.Name, "_") { + return xerrors.Errorf("agent name %q contains underscores which are no longer supported, please use hyphens instead (regex: %q)", prAgent.Name, provisioner.AgentNameRegex.String()) + } + return xerrors.Errorf("agent name %q does not match regex %q", prAgent.Name, provisioner.AgentNameRegex.String()) + } + // Agent names must be case-insensitive-unique, to be unambiguous in + // `coder_app`s and CoderVPN DNS names. + if _, ok := agentNames[strings.ToLower(prAgent.Name)]; ok { return xerrors.Errorf("duplicate agent name %q", prAgent.Name) } - agentNames[prAgent.Name] = struct{}{} + agentNames[strings.ToLower(prAgent.Name)] = struct{}{} var instanceID sql.NullString if prAgent.GetInstanceId() != "" { @@ -2109,10 +2124,13 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. } for _, app := range prAgent.Apps { + // Similar logic is duplicated in terraform/resources.go. slug := app.Slug if slug == "" { return xerrors.Errorf("app must have a slug or name set") } + // Contrary to agent names above, app slugs were never permitted to + // contain uppercase letters or underscores. if !provisioner.AppSlugRegex.MatchString(slug) { return xerrors.Errorf("app slug %q does not match regex %q", slug, provisioner.AppSlugRegex.String()) } diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 21ba8c6fad..cc73089e82 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -1883,6 +1883,7 @@ func TestInsertWorkspaceResource(t *testing.T) { Name: "something", Type: "aws_instance", Agents: []*sdkproto.Agent{{ + Name: "dev", Auth: &sdkproto.Agent_Token{ Token: "bananas", }, @@ -1896,6 +1897,7 @@ func TestInsertWorkspaceResource(t *testing.T) { Name: "something", Type: "aws_instance", Agents: []*sdkproto.Agent{{ + Name: "dev", Apps: []*sdkproto.App{{ Slug: "a", }, { @@ -1903,7 +1905,116 @@ func TestInsertWorkspaceResource(t *testing.T) { }}, }}, }) - require.ErrorContains(t, err, "duplicate app slug") + require.ErrorContains(t, err, `duplicate app slug, must be unique per template: "a"`) + err = insert(dbmem.New(), uuid.New(), &sdkproto.Resource{ + Name: "something", + Type: "aws_instance", + Agents: []*sdkproto.Agent{{ + Name: "dev1", + Apps: []*sdkproto.App{{ + Slug: "a", + }}, + }, { + Name: "dev2", + Apps: []*sdkproto.App{{ + Slug: "a", + }}, + }}, + }) + require.ErrorContains(t, err, `duplicate app slug, must be unique per template: "a"`) + }) + t.Run("AppSlugInvalid", func(t *testing.T) { + t.Parallel() + db := dbmem.New() + job := uuid.New() + err := insert(db, job, &sdkproto.Resource{ + Name: "something", + Type: "aws_instance", + Agents: []*sdkproto.Agent{{ + Name: "dev", + Apps: []*sdkproto.App{{ + Slug: "dev_1", + }}, + }}, + }) + require.ErrorContains(t, err, `app slug "dev_1" does not match regex`) + err = insert(db, job, &sdkproto.Resource{ + Name: "something", + Type: "aws_instance", + Agents: []*sdkproto.Agent{{ + Name: "dev", + Apps: []*sdkproto.App{{ + Slug: "dev--1", + }}, + }}, + }) + require.ErrorContains(t, err, `app slug "dev--1" does not match regex`) + err = insert(db, job, &sdkproto.Resource{ + Name: "something", + Type: "aws_instance", + Agents: []*sdkproto.Agent{{ + Name: "dev", + Apps: []*sdkproto.App{{ + Slug: "Dev", + }}, + }}, + }) + require.ErrorContains(t, err, `app slug "Dev" does not match regex`) + }) + t.Run("DuplicateAgentNames", func(t *testing.T) { + t.Parallel() + db := dbmem.New() + job := uuid.New() + // case-insensitive-unique + err := insert(db, job, &sdkproto.Resource{ + Name: "something", + Type: "aws_instance", + Agents: []*sdkproto.Agent{{ + Name: "dev", + }, { + Name: "Dev", + }}, + }) + require.ErrorContains(t, err, "duplicate agent name") + err = insert(db, job, &sdkproto.Resource{ + Name: "something", + Type: "aws_instance", + Agents: []*sdkproto.Agent{{ + Name: "dev", + }, { + Name: "dev", + }}, + }) + require.ErrorContains(t, err, "duplicate agent name") + }) + t.Run("AgentNameInvalid", func(t *testing.T) { + t.Parallel() + db := dbmem.New() + job := uuid.New() + err := insert(db, job, &sdkproto.Resource{ + Name: "something", + Type: "aws_instance", + Agents: []*sdkproto.Agent{{ + Name: "Dev", + }}, + }) + require.NoError(t, err) // uppercase is still allowed + err = insert(db, job, &sdkproto.Resource{ + Name: "something", + Type: "aws_instance", + Agents: []*sdkproto.Agent{{ + Name: "dev_1", + }}, + }) + require.ErrorContains(t, err, `agent name "dev_1" contains underscores`) // custom error for underscores + err = insert(db, job, &sdkproto.Resource{ + Name: "something", + Type: "aws_instance", + Agents: []*sdkproto.Agent{{ + Name: "dev--1", + }}, + }) + require.ErrorContains(t, err, `agent name "dev--1" does not match regex`) }) t.Run("Success", func(t *testing.T) { t.Parallel() @@ -1981,6 +2092,7 @@ func TestInsertWorkspaceResource(t *testing.T) { Name: "something", Type: "aws_instance", Agents: []*sdkproto.Agent{{ + Name: "dev", DisplayApps: &sdkproto.DisplayApps{ Vscode: true, VscodeInsiders: true, @@ -2009,6 +2121,7 @@ func TestInsertWorkspaceResource(t *testing.T) { Name: "something", Type: "aws_instance", Agents: []*sdkproto.Agent{{ + Name: "dev", DisplayApps: &sdkproto.DisplayApps{}, }}, }) @@ -2033,6 +2146,7 @@ func TestInsertWorkspaceResource(t *testing.T) { Name: "something", Type: "aws_instance", Agents: []*sdkproto.Agent{{ + Name: "dev", DisplayApps: &sdkproto.DisplayApps{}, ResourcesMonitoring: &sdkproto.ResourcesMonitoring{ Memory: &sdkproto.MemoryResourceMonitor{ diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index 7fb141e557..e1399aded9 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -297,18 +297,17 @@ func ReloadBuiltinRoles(opts *RoleOptions) { Identifier: RoleAuditor(), DisplayName: "Auditor", Site: Permissions(map[string][]policy.Action{ - // Should be able to read all template details, even in orgs they - // are not in. - ResourceTemplate.Type: {policy.ActionRead, policy.ActionViewInsights}, - ResourceAuditLog.Type: {policy.ActionRead}, - ResourceUser.Type: {policy.ActionRead}, - ResourceGroup.Type: {policy.ActionRead}, - ResourceGroupMember.Type: {policy.ActionRead}, + ResourceAuditLog.Type: {policy.ActionRead}, + // Allow auditors to see the resources that audit logs reflect. + ResourceTemplate.Type: {policy.ActionRead, policy.ActionViewInsights}, + ResourceUser.Type: {policy.ActionRead}, + ResourceGroup.Type: {policy.ActionRead}, + ResourceGroupMember.Type: {policy.ActionRead}, + ResourceOrganization.Type: {policy.ActionRead}, + ResourceOrganizationMember.Type: {policy.ActionRead}, // Allow auditors to query deployment stats and insights. ResourceDeploymentStats.Type: {policy.ActionRead}, ResourceDeploymentConfig.Type: {policy.ActionRead}, - // Org roles are not really used yet, so grant the perm at the site level. - ResourceOrganizationMember.Type: {policy.ActionRead}, }), Org: map[string][]Permission{}, User: []Permission{}, @@ -325,11 +324,10 @@ func ReloadBuiltinRoles(opts *RoleOptions) { // CRUD to provisioner daemons for now. ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, // Needs to read all organizations since - ResourceOrganization.Type: {policy.ActionRead}, - ResourceUser.Type: {policy.ActionRead}, - ResourceGroup.Type: {policy.ActionRead}, - ResourceGroupMember.Type: {policy.ActionRead}, - // Org roles are not really used yet, so grant the perm at the site level. + ResourceUser.Type: {policy.ActionRead}, + ResourceGroup.Type: {policy.ActionRead}, + ResourceGroupMember.Type: {policy.ActionRead}, + ResourceOrganization.Type: {policy.ActionRead}, ResourceOrganizationMember.Type: {policy.ActionRead}, }), Org: map[string][]Permission{}, @@ -348,10 +346,11 @@ func ReloadBuiltinRoles(opts *RoleOptions) { policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete, policy.ActionUpdatePersonal, policy.ActionReadPersonal, }, + ResourceGroup.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, + ResourceGroupMember.Type: {policy.ActionRead}, + ResourceOrganization.Type: {policy.ActionRead}, // Full perms to manage org members ResourceOrganizationMember.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, - ResourceGroup.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, - ResourceGroupMember.Type: {policy.ActionRead}, // Manage org membership based on OIDC claims ResourceIdpsyncSettings.Type: {policy.ActionRead, policy.ActionUpdate}, }), diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index db0d983257..cb43b1b175 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -117,6 +117,7 @@ func TestRolePermissions(t *testing.T) { owner := authSubject{Name: "owner", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleOwner()}}} templateAdmin := authSubject{Name: "template-admin", Actor: rbac.Subject{ID: templateAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleTemplateAdmin()}}} userAdmin := authSubject{Name: "user-admin", Actor: rbac.Subject{ID: userAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleUserAdmin()}}} + auditor := authSubject{Name: "auditor", Actor: rbac.Subject{ID: auditorID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleAuditor()}}} orgAdmin := authSubject{Name: "org_admin", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgMember(orgID), rbac.ScopedRoleOrgAdmin(orgID)}}} orgAuditor := authSubject{Name: "org_auditor", Actor: rbac.Subject{ID: auditorID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgMember(orgID), rbac.ScopedRoleOrgAuditor(orgID)}}} @@ -286,8 +287,8 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceOrganization.WithID(orgID).InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgAdmin, orgMemberMe, templateAdmin, orgTemplateAdmin, orgAuditor, orgUserAdmin}, - false: {setOtherOrg, memberMe, userAdmin}, + true: {owner, orgAdmin, orgMemberMe, templateAdmin, orgTemplateAdmin, auditor, orgAuditor, userAdmin, orgUserAdmin}, + false: {setOtherOrg, memberMe}, }, }, { diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index 9fd1bf6e2d..4e3e3d2f7f 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -829,6 +829,7 @@ func TestTemplateVersionResources(t *testing.T) { Type: "example", Agents: []*proto.Agent{{ Id: "something", + Name: "dev", Auth: &proto.Agent_Token{}, }}, }, { @@ -875,7 +876,8 @@ func TestTemplateVersionLogs(t *testing.T) { Name: "some", Type: "example", Agents: []*proto.Agent{{ - Id: "something", + Id: "something", + Name: "dev", Auth: &proto.Agent_Token{ Token: uuid.NewString(), }, diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index cdb33e08a5..69bba9d8ba 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -393,7 +393,8 @@ func TestWorkspaceAgentConnectRPC(t *testing.T) { Name: "example", Type: "aws_instance", Agents: []*proto.Agent{{ - Id: uuid.NewString(), + Id: uuid.NewString(), + Name: "dev", Auth: &proto.Agent_Token{ Token: uuid.NewString(), }, diff --git a/coderd/workspaceagentsrpc.go b/coderd/workspaceagentsrpc.go index c794c9c143..43da35410f 100644 --- a/coderd/workspaceagentsrpc.go +++ b/coderd/workspaceagentsrpc.go @@ -147,6 +147,7 @@ func (api *API) workspaceAgentRPC(rw http.ResponseWriter, r *http.Request) { Database: api.Database, NotificationsEnqueuer: api.NotificationsEnqueuer, Pubsub: api.Pubsub, + Auditor: &api.Auditor, DerpMapFn: api.DERPMap, TailnetCoordinator: &api.TailnetCoordinator, AppearanceFetcher: &api.AppearanceFetcher, diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index fc8961a8c7..f6bfcfd2ea 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -720,6 +720,7 @@ func TestWorkspaceBuildLogs(t *testing.T) { Type: "example", Agents: []*proto.Agent{{ Id: "something", + Name: "dev", Auth: &proto.Agent_Token{}, }}, }, { diff --git a/coderd/workspaceresourceauth_test.go b/coderd/workspaceresourceauth_test.go index d653231ab9..8c1b64feaf 100644 --- a/coderd/workspaceresourceauth_test.go +++ b/coderd/workspaceresourceauth_test.go @@ -33,6 +33,7 @@ func TestPostWorkspaceAuthAzureInstanceIdentity(t *testing.T) { Name: "somename", Type: "someinstance", Agents: []*proto.Agent{{ + Name: "dev", Auth: &proto.Agent_InstanceId{ InstanceId: instanceID, }, @@ -78,6 +79,7 @@ func TestPostWorkspaceAuthAWSInstanceIdentity(t *testing.T) { Name: "somename", Type: "someinstance", Agents: []*proto.Agent{{ + Name: "dev", Auth: &proto.Agent_InstanceId{ InstanceId: instanceID, }, @@ -164,6 +166,7 @@ func TestPostWorkspaceAuthGoogleInstanceIdentity(t *testing.T) { Name: "somename", Type: "someinstance", Agents: []*proto.Agent{{ + Name: "dev", Auth: &proto.Agent_InstanceId{ InstanceId: instanceID, }, diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index b8bf71c3eb..7a81d51926 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -219,6 +219,7 @@ func TestWorkspace(t *testing.T) { Type: "example", Agents: []*proto.Agent{{ Id: uuid.NewString(), + Name: "dev", Auth: &proto.Agent_Token{}, }}, }}, @@ -259,6 +260,7 @@ func TestWorkspace(t *testing.T) { Type: "example", Agents: []*proto.Agent{{ Id: uuid.NewString(), + Name: "dev", Auth: &proto.Agent_Token{}, ConnectionTimeoutSeconds: 1, }}, @@ -1722,7 +1724,8 @@ func TestWorkspaceFilterManual(t *testing.T) { Name: "example", Type: "aws_instance", Agents: []*proto.Agent{{ - Id: uuid.NewString(), + Id: uuid.NewString(), + Name: "dev", Auth: &proto.Agent_Token{ Token: authToken, }, @@ -2729,7 +2732,8 @@ func TestWorkspaceWatcher(t *testing.T) { Name: "example", Type: "aws_instance", Agents: []*proto.Agent{{ - Id: uuid.NewString(), + Id: uuid.NewString(), + Name: "dev", Auth: &proto.Agent_Token{ Token: authToken, }, @@ -2951,6 +2955,7 @@ func TestWorkspaceResource(t *testing.T) { Type: "example", Agents: []*proto.Agent{{ Id: "something", + Name: "dev", Auth: &proto.Agent_Token{}, Apps: apps, }}, @@ -3025,6 +3030,7 @@ func TestWorkspaceResource(t *testing.T) { Type: "example", Agents: []*proto.Agent{{ Id: "something", + Name: "dev", Auth: &proto.Agent_Token{}, Apps: apps, }}, @@ -3068,6 +3074,7 @@ func TestWorkspaceResource(t *testing.T) { Type: "example", Agents: []*proto.Agent{{ Id: "something", + Name: "dev", Auth: &proto.Agent_Token{}, }}, Metadata: []*proto.Resource_Metadata{{ diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index b5f86c2220..5ecd8fee8e 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -35,6 +35,18 @@ import ( // log-source. This should be removed in the future. var ExternalLogSourceID = uuid.MustParse("3b579bf4-1ed8-4b99-87a8-e9a1e3410410") +// ConnectionType is the type of connection that the agent is receiving. +type ConnectionType string + +// Connection type enums. +const ( + ConnectionTypeUnspecified ConnectionType = "Unspecified" + ConnectionTypeSSH ConnectionType = "SSH" + ConnectionTypeVSCode ConnectionType = "VS Code" + ConnectionTypeJetBrains ConnectionType = "JetBrains" + ConnectionTypeReconnectingPTY ConnectionType = "Web Terminal" +) + // New returns a client that is used to interact with the // Coder API from a workspace agent. func New(serverURL *url.URL) *Client { diff --git a/codersdk/agentsdk/convert.go b/codersdk/agentsdk/convert.go index 002d96a50a..7e8ea08c74 100644 --- a/codersdk/agentsdk/convert.go +++ b/codersdk/agentsdk/convert.go @@ -390,3 +390,37 @@ func ProtoFromLifecycleState(s codersdk.WorkspaceAgentLifecycle) (proto.Lifecycl } return proto.Lifecycle_State(caps), nil } + +func ConnectionTypeFromProto(typ proto.Connection_Type) (ConnectionType, error) { + switch typ { + case proto.Connection_TYPE_UNSPECIFIED: + return ConnectionTypeUnspecified, nil + case proto.Connection_SSH: + return ConnectionTypeSSH, nil + case proto.Connection_VSCODE: + return ConnectionTypeVSCode, nil + case proto.Connection_JETBRAINS: + return ConnectionTypeJetBrains, nil + case proto.Connection_RECONNECTING_PTY: + return ConnectionTypeReconnectingPTY, nil + default: + return "", xerrors.Errorf("unknown connection type %q", typ) + } +} + +func ProtoFromConnectionType(typ ConnectionType) (proto.Connection_Type, error) { + switch typ { + case ConnectionTypeUnspecified: + return proto.Connection_TYPE_UNSPECIFIED, nil + case ConnectionTypeSSH: + return proto.Connection_SSH, nil + case ConnectionTypeVSCode: + return proto.Connection_VSCODE, nil + case ConnectionTypeJetBrains: + return proto.Connection_JETBRAINS, nil + case ConnectionTypeReconnectingPTY: + return proto.Connection_RECONNECTING_PTY, nil + default: + return 0, xerrors.Errorf("unknown connection type %q", typ) + } +} diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go index 30225ced30..a40ed7b64a 100644 --- a/enterprise/coderd/templates_test.go +++ b/enterprise/coderd/templates_test.go @@ -161,11 +161,11 @@ func TestTemplates(t *testing.T) { Name: "some", Type: "example", Agents: []*proto.Agent{{ - Id: "something", + Id: "something", + Name: "test", Auth: &proto.Agent_Token{ Token: uuid.NewString(), }, - Name: "test", }}, }, { Name: "another", diff --git a/provisioner/appslug.go b/provisioner/appslug.go deleted file mode 100644 index a13fa4eb2d..0000000000 --- a/provisioner/appslug.go +++ /dev/null @@ -1,13 +0,0 @@ -package provisioner - -import "regexp" - -// AppSlugRegex is the regex used to validate the slug of a coder_app -// resource. It must be a valid hostname and cannot contain two consecutive -// hyphens or start/end with a hyphen. -// -// This regex is duplicated in the terraform provider code, so make sure to -// update it there as well. -// -// There are test cases for this regex in appslug_test.go. -var AppSlugRegex = regexp.MustCompile(`^[a-z0-9](-?[a-z0-9])*$`) diff --git a/provisioner/appslug_test.go b/provisioner/appslug_test.go deleted file mode 100644 index f13f220e9c..0000000000 --- a/provisioner/appslug_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package provisioner_test - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/coder/coder/v2/provisioner" -) - -func TestValidAppSlugRegex(t *testing.T) { - t.Parallel() - - t.Run("Valid", func(t *testing.T) { - t.Parallel() - - validStrings := []string{ - "a", - "1", - "a1", - "1a", - "1a1", - "1-1", - "a-a", - "ab-cd", - "ab-cd-ef", - "abc-123", - "a-123", - "abc-1", - "ab-c", - "a-bc", - } - - for _, s := range validStrings { - require.True(t, provisioner.AppSlugRegex.MatchString(s), s) - } - }) - - t.Run("Invalid", func(t *testing.T) { - t.Parallel() - - invalidStrings := []string{ - "", - "-", - "-abc", - "abc-", - "ab--cd", - "a--bc", - "ab--c", - "_", - "ab_cd", - "_abc", - "abc_", - " ", - "abc ", - " abc", - "ab cd", - } - - for _, s := range invalidStrings { - require.False(t, provisioner.AppSlugRegex.MatchString(s), s) - } - }) -} diff --git a/provisioner/regexes.go b/provisioner/regexes.go new file mode 100644 index 0000000000..fe4db3e9e9 --- /dev/null +++ b/provisioner/regexes.go @@ -0,0 +1,31 @@ +package provisioner + +import "regexp" + +var ( + // AgentNameRegex is the regex used to validate the name of a coder_agent + // resource. It must be a valid hostname and cannot contain two consecutive + // hyphens or start/end with a hyphen. Uppercase characters ARE permitted, + // although duplicate agent names with different casing will be rejected. + // + // Previously, underscores were permitted, but this was changed in 2025-02. + // App URLs never supported underscores, and proxy requests to apps on + // agents with underscores in the name always failed. + // + // Due to terraform limitations, this cannot be validated at the provider + // level as resource names cannot be read from the provider API, so this is + // not duplicated in the terraform provider code. + // + // There are test cases for this regex in regexes_test.go. + AgentNameRegex = regexp.MustCompile(`(?i)^[a-z0-9](-?[a-z0-9])*$`) + + // AppSlugRegex is the regex used to validate the slug of a coder_app + // resource. It must be a valid hostname and cannot contain two consecutive + // hyphens or start/end with a hyphen. + // + // This regex is duplicated in the terraform provider code, so make sure to + // update it there as well. + // + // There are test cases for this regex in regexes_test.go. + AppSlugRegex = regexp.MustCompile(`^[a-z0-9](-?[a-z0-9])*$`) +) diff --git a/provisioner/regexes_test.go b/provisioner/regexes_test.go new file mode 100644 index 0000000000..d8c69f9b67 --- /dev/null +++ b/provisioner/regexes_test.go @@ -0,0 +1,88 @@ +package provisioner_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/provisioner" +) + +var ( + validStrings = []string{ + "a", + "1", + "a1", + "1a", + "1a1", + "1-1", + "a-a", + "ab-cd", + "ab-cd-ef", + "abc-123", + "a-123", + "abc-1", + "ab-c", + "a-bc", + } + + invalidStrings = []string{ + "", + "-", + "-abc", + "abc-", + "ab--cd", + "a--bc", + "ab--c", + "_", + "ab_cd", + "_abc", + "abc_", + " ", + "abc ", + " abc", + "ab cd", + } + + uppercaseStrings = []string{ + "A", + "A1", + "1A", + } +) + +func TestAgentNameRegex(t *testing.T) { + t.Parallel() + + t.Run("Valid", func(t *testing.T) { + t.Parallel() + for _, s := range append(validStrings, uppercaseStrings...) { + require.True(t, provisioner.AgentNameRegex.MatchString(s), s) + } + }) + + t.Run("Invalid", func(t *testing.T) { + t.Parallel() + for _, s := range invalidStrings { + require.False(t, provisioner.AgentNameRegex.MatchString(s), s) + } + }) +} + +func TestAppSlugRegex(t *testing.T) { + t.Parallel() + + t.Run("Valid", func(t *testing.T) { + t.Parallel() + for _, s := range validStrings { + require.True(t, provisioner.AppSlugRegex.MatchString(s), s) + } + }) + + t.Run("Invalid", func(t *testing.T) { + t.Parallel() + for _, s := range append(invalidStrings, uppercaseStrings...) { + require.False(t, provisioner.AppSlugRegex.MatchString(s), s) + } + }) +} diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index 5c96863728..001d88cab4 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -215,10 +215,25 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s return nil, xerrors.Errorf("decode agent attributes: %w", err) } - if _, ok := agentNames[tfResource.Name]; ok { + // Similar logic is duplicated in terraform/resources.go. + if tfResource.Name == "" { + return nil, xerrors.Errorf("agent name cannot be empty") + } + // In 2025-02 we removed support for underscores in agent names. To + // provide a nicer error message, we check the regex first and check + // for underscores if it fails. + if !provisioner.AgentNameRegex.MatchString(tfResource.Name) { + if strings.Contains(tfResource.Name, "_") { + return nil, xerrors.Errorf("agent name %q contains underscores which are no longer supported, please use hyphens instead (regex: %q)", tfResource.Name, provisioner.AgentNameRegex.String()) + } + return nil, xerrors.Errorf("agent name %q does not match regex %q", tfResource.Name, provisioner.AgentNameRegex.String()) + } + // Agent names must be case-insensitive-unique, to be unambiguous in + // `coder_app`s and CoderVPN DNS names. + if _, ok := agentNames[strings.ToLower(tfResource.Name)]; ok { return nil, xerrors.Errorf("duplicate agent name: %s", tfResource.Name) } - agentNames[tfResource.Name] = struct{}{} + agentNames[strings.ToLower(tfResource.Name)] = struct{}{} // Handling for deprecated attributes. login_before_ready was replaced // by startup_script_behavior, but we still need to support it for @@ -441,6 +456,7 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s if attrs.Slug == "" { attrs.Slug = resource.Name } + // Similar logic is duplicated in terraform/resources.go. if attrs.DisplayName == "" { if attrs.Name != "" { // Name is deprecated but still accepted. @@ -450,8 +466,10 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s } } + // Contrary to agent names above, app slugs were never permitted to + // contain uppercase letters or underscores. if !provisioner.AppSlugRegex.MatchString(attrs.Slug) { - return nil, xerrors.Errorf("invalid app slug %q, please update your coder/coder provider to the latest version and specify the slug property on each coder_app", attrs.Slug) + return nil, xerrors.Errorf("app slug %q does not match regex %q", attrs.Slug, provisioner.AppSlugRegex.String()) } if _, exists := appSlugs[attrs.Slug]; exists { diff --git a/provisioner/terraform/resources_test.go b/provisioner/terraform/resources_test.go index 1f1a03dfae..46ad49d01d 100644 --- a/provisioner/terraform/resources_test.go +++ b/provisioner/terraform/resources_test.go @@ -984,6 +984,7 @@ func TestInvalidTerraformAddress(t *testing.T) { require.Equal(t, state.Resources[0].ModulePath, "invalid terraform address") } +//nolint:tparallel func TestAppSlugValidation(t *testing.T) { t.Parallel() ctx, logger := ctxAndLogger(t) @@ -1001,29 +1002,147 @@ func TestAppSlugValidation(t *testing.T) { tfPlanGraph, err := os.ReadFile(filepath.Join(dir, "multiple-apps.tfplan.dot")) require.NoError(t, err) - // Change all slugs to be invalid. + cases := []struct { + slug string + errContains string + }{ + {slug: "$$$ invalid slug $$$", errContains: "does not match regex"}, + {slug: "invalid--slug", errContains: "does not match regex"}, + {slug: "invalid_slug", errContains: "does not match regex"}, + {slug: "Invalid-slug", errContains: "does not match regex"}, + {slug: "valid", errContains: ""}, + } + + //nolint:paralleltest + for i, c := range cases { + c := c + t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) { + // Change the first app slug to match the current case. + for _, resource := range tfPlan.PlannedValues.RootModule.Resources { + if resource.Type == "coder_app" { + resource.AttributeValues["slug"] = c.slug + break + } + } + + _, err := terraform.ConvertState(ctx, []*tfjson.StateModule{tfPlan.PlannedValues.RootModule}, string(tfPlanGraph), logger) + if c.errContains != "" { + require.ErrorContains(t, err, c.errContains) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestAppSlugDuplicate(t *testing.T) { + t.Parallel() + ctx, logger := ctxAndLogger(t) + + // nolint:dogsled + _, filename, _, _ := runtime.Caller(0) + + dir := filepath.Join(filepath.Dir(filename), "testdata", "multiple-apps") + tfPlanRaw, err := os.ReadFile(filepath.Join(dir, "multiple-apps.tfplan.json")) + require.NoError(t, err) + var tfPlan tfjson.Plan + err = json.Unmarshal(tfPlanRaw, &tfPlan) + require.NoError(t, err) + tfPlanGraph, err := os.ReadFile(filepath.Join(dir, "multiple-apps.tfplan.dot")) + require.NoError(t, err) + for _, resource := range tfPlan.PlannedValues.RootModule.Resources { if resource.Type == "coder_app" { - resource.AttributeValues["slug"] = "$$$ invalid slug $$$" + resource.AttributeValues["slug"] = "dev" + } + } + + _, err = terraform.ConvertState(ctx, []*tfjson.StateModule{tfPlan.PlannedValues.RootModule}, string(tfPlanGraph), logger) + require.Error(t, err) + require.ErrorContains(t, err, "duplicate app slug") +} + +//nolint:tparallel +func TestAgentNameInvalid(t *testing.T) { + t.Parallel() + ctx, logger := ctxAndLogger(t) + + // nolint:dogsled + _, filename, _, _ := runtime.Caller(0) + + dir := filepath.Join(filepath.Dir(filename), "testdata", "multiple-agents") + tfPlanRaw, err := os.ReadFile(filepath.Join(dir, "multiple-agents.tfplan.json")) + require.NoError(t, err) + var tfPlan tfjson.Plan + err = json.Unmarshal(tfPlanRaw, &tfPlan) + require.NoError(t, err) + tfPlanGraph, err := os.ReadFile(filepath.Join(dir, "multiple-agents.tfplan.dot")) + require.NoError(t, err) + + cases := []struct { + name string + errContains string + }{ + {name: "bad--name", errContains: "does not match regex"}, + {name: "bad_name", errContains: "contains underscores"}, // custom error for underscores + {name: "valid-name-123", errContains: ""}, + {name: "valid", errContains: ""}, + {name: "UppercaseValid", errContains: ""}, + } + + //nolint:paralleltest + for i, c := range cases { + c := c + t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) { + // Change the first agent name to match the current case. + for _, resource := range tfPlan.PlannedValues.RootModule.Resources { + if resource.Type == "coder_agent" { + resource.Name = c.name + break + } + } + + _, err := terraform.ConvertState(ctx, []*tfjson.StateModule{tfPlan.PlannedValues.RootModule}, string(tfPlanGraph), logger) + if c.errContains != "" { + require.ErrorContains(t, err, c.errContains) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestAgentNameDuplicate(t *testing.T) { + t.Parallel() + ctx, logger := ctxAndLogger(t) + + // nolint:dogsled + _, filename, _, _ := runtime.Caller(0) + + dir := filepath.Join(filepath.Dir(filename), "testdata", "multiple-agents") + tfPlanRaw, err := os.ReadFile(filepath.Join(dir, "multiple-agents.tfplan.json")) + require.NoError(t, err) + var tfPlan tfjson.Plan + err = json.Unmarshal(tfPlanRaw, &tfPlan) + require.NoError(t, err) + tfPlanGraph, err := os.ReadFile(filepath.Join(dir, "multiple-agents.tfplan.dot")) + require.NoError(t, err) + + for _, resource := range tfPlan.PlannedValues.RootModule.Resources { + if resource.Type == "coder_agent" { + switch resource.Name { + case "dev1": + resource.Name = "dev" + case "dev2": + resource.Name = "Dev" + } } } state, err := terraform.ConvertState(ctx, []*tfjson.StateModule{tfPlan.PlannedValues.RootModule}, string(tfPlanGraph), logger) require.Nil(t, state) require.Error(t, err) - require.ErrorContains(t, err, "invalid app slug") - - // Change all slugs to be identical and valid. - for _, resource := range tfPlan.PlannedValues.RootModule.Resources { - if resource.Type == "coder_app" { - resource.AttributeValues["slug"] = "valid" - } - } - - state, err = terraform.ConvertState(ctx, []*tfjson.StateModule{tfPlan.PlannedValues.RootModule}, string(tfPlanGraph), logger) - require.Nil(t, state) - require.Error(t, err) - require.ErrorContains(t, err, "duplicate app slug") + require.ErrorContains(t, err, "duplicate agent name") } func TestMetadataResourceDuplicate(t *testing.T) { diff --git a/site/site.go b/site/site.go index 3a85f7b396..e2209b4052 100644 --- a/site/site.go +++ b/site/site.go @@ -166,7 +166,7 @@ func New(opts *Options) *Handler { handler.installScript, err = parseInstallScript(opts.SiteFS, opts.BuildInfo) if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "install.sh will be unavailable: %v", err.Error()) + opts.Logger.Warn(context.Background(), "could not parse install.sh, it will be unavailable", slog.Error(err)) } return handler diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index 70cd57628f..a27514a03c 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -1,11 +1,17 @@ import { API } from "api/api"; import type { - AuthorizationResponse, CreateOrganizationRequest, GroupSyncSettings, RoleSyncSettings, UpdateOrganizationRequest, } from "api/typesGenerated"; +import { + type AnyOrganizationPermissions, + type OrganizationPermissionName, + type OrganizationPermissions, + anyOrganizationPermissionChecks, + organizationPermissionChecks, +} from "modules/management/organizationPermissions"; import type { QueryClient } from "react-query"; import { meKey } from "./users"; @@ -197,53 +203,6 @@ export const patchRoleSyncSettings = ( }; }; -/** - * Fetch permissions for a single organization. - * - * If the ID is undefined, return a disabled query. - */ -export const organizationPermissions = (organizationId: string | undefined) => { - if (!organizationId) { - return { enabled: false }; - } - return { - queryKey: ["organization", organizationId, "permissions"], - queryFn: () => - // Only request what we use on individual org settings, members, and group - // pages, which at the moment is whether you can edit the members on the - // members page, create roles on the roles page, and create groups on the - // groups page. The edit organization check for the settings page is - // covered by the multi-org query at the moment, and the edit group check - // on the group page is done on the group itself, not the org, so neither - // show up here. - API.checkAuthorization({ - checks: { - editMembers: { - object: { - resource_type: "organization_member", - organization_id: organizationId, - }, - action: "update", - }, - createGroup: { - object: { - resource_type: "group", - organization_id: organizationId, - }, - action: "create", - }, - assignOrgRole: { - object: { - resource_type: "assign_org_role", - organization_id: organizationId, - }, - action: "create", - }, - }, - }), - }; -}; - export const provisionerJobQueryKey = (orgId: string) => [ "organization", orgId, @@ -276,58 +235,13 @@ export const organizationsPermissions = ( // per sub-link (settings, groups, roles, and members pages) that tells us // whether to show that page, since we only show them if you can edit (and // not, at the moment if you can only view). - const checks = (organizationId: string) => ({ - editMembers: { - object: { - resource_type: "organization_member", - organization_id: organizationId, - }, - action: "update", - }, - editGroups: { - object: { - resource_type: "group", - organization_id: organizationId, - }, - action: "update", - }, - editOrganization: { - object: { - resource_type: "organization", - organization_id: organizationId, - }, - action: "update", - }, - assignOrgRole: { - object: { - resource_type: "assign_org_role", - organization_id: organizationId, - }, - action: "create", - }, - viewProvisioners: { - object: { - resource_type: "provisioner_daemon", - organization_id: organizationId, - }, - action: "read", - }, - viewIdpSyncSettings: { - object: { - resource_type: "idpsync_settings", - organization_id: organizationId, - }, - action: "read", - }, - }); // The endpoint takes a flat array, so to avoid collisions prepend each // check with the org ID (the key can be anything we want). const prefixedChecks = organizationIds.flatMap((orgId) => - Object.entries(checks(orgId)).map(([key, val]) => [ - `${orgId}.${key}`, - val, - ]), + Object.entries(organizationPermissionChecks(orgId)).map( + ([key, val]) => [`${orgId}.${key}`, val], + ), ); const response = await API.checkAuthorization({ @@ -343,15 +257,30 @@ export const organizationsPermissions = ( if (!acc[orgId]) { acc[orgId] = {}; } - acc[orgId][perm] = value; + acc[orgId][perm as OrganizationPermissionName] = value; return acc; }, - {} as Record, - ); + {} as Record>, + ) as Record; }, }; }; +export const anyOrganizationPermissionsKey = [ + "authorization", + "anyOrganization", +]; + +export const anyOrganizationPermissions = () => { + return { + queryKey: anyOrganizationPermissionsKey, + queryFn: () => + API.checkAuthorization({ + checks: anyOrganizationPermissionChecks, + }) as Promise, + }; +}; + export const getOrganizationIdpSyncClaimFieldValuesKey = ( organization: string, field: string, diff --git a/site/src/components/Latency/Latency.tsx b/site/src/components/Latency/Latency.tsx index 16e3199b33..706bf10687 100644 --- a/site/src/components/Latency/Latency.tsx +++ b/site/src/components/Latency/Latency.tsx @@ -10,9 +10,14 @@ import { getLatencyColor } from "utils/latency"; interface LatencyProps { latency?: number; isLoading?: boolean; + size?: number; } -export const Latency: FC = ({ latency, isLoading }) => { +export const Latency: FC = ({ + latency, + isLoading, + size = 14, +}) => { const theme = useTheme(); // Always use the no latency color for loading. const color = getLatencyColor(theme, isLoading ? undefined : latency); @@ -21,7 +26,7 @@ export const Latency: FC = ({ latency, isLoading }) => { return ( diff --git a/site/src/contexts/auth/permissions.tsx b/site/src/contexts/auth/permissions.tsx index b44d85e963..1043862942 100644 --- a/site/src/contexts/auth/permissions.tsx +++ b/site/src/contexts/auth/permissions.tsx @@ -16,7 +16,6 @@ export const checks = { readWorkspaceProxies: "readWorkspaceProxies", editWorkspaceProxies: "editWorkspaceProxies", createOrganization: "createOrganization", - editAnyOrganization: "editAnyOrganization", viewAnyGroup: "viewAnyGroup", createGroup: "createGroup", viewAllLicenses: "viewAllLicenses", @@ -122,13 +121,6 @@ export const permissionsToCheck = { }, action: "create", }, - [checks.editAnyOrganization]: { - object: { - resource_type: "organization", - any_org: true, - }, - action: "update", - }, [checks.viewAnyGroup]: { object: { resource_type: "group", diff --git a/site/src/modules/dashboard/DashboardProvider.tsx b/site/src/modules/dashboard/DashboardProvider.tsx index d8fa339dec..bf8e307206 100644 --- a/site/src/modules/dashboard/DashboardProvider.tsx +++ b/site/src/modules/dashboard/DashboardProvider.tsx @@ -1,7 +1,10 @@ import { appearance } from "api/queries/appearance"; import { entitlements } from "api/queries/entitlements"; import { experiments } from "api/queries/experiments"; -import { organizations } from "api/queries/organizations"; +import { + anyOrganizationPermissions, + organizations, +} from "api/queries/organizations"; import type { AppearanceConfig, Entitlements, @@ -11,6 +14,7 @@ import type { import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Loader } from "components/Loader/Loader"; import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; +import { canViewAnyOrganization } from "modules/management/organizationPermissions"; import { type FC, type PropsWithChildren, createContext } from "react"; import { useQuery } from "react-query"; import { selectFeatureVisibility } from "./entitlements"; @@ -21,6 +25,7 @@ export interface DashboardValue { appearance: AppearanceConfig; organizations: readonly Organization[]; showOrganizations: boolean; + canViewOrganizationSettings: boolean; } export const DashboardContext = createContext( @@ -33,12 +38,16 @@ export const DashboardProvider: FC = ({ children }) => { const experimentsQuery = useQuery(experiments(metadata.experiments)); const appearanceQuery = useQuery(appearance(metadata.appearance)); const organizationsQuery = useQuery(organizations()); + const anyOrganizationPermissionsQuery = useQuery( + anyOrganizationPermissions(), + ); const error = entitlementsQuery.error || appearanceQuery.error || experimentsQuery.error || - organizationsQuery.error; + organizationsQuery.error || + anyOrganizationPermissionsQuery.error; if (error) { return ; @@ -48,7 +57,8 @@ export const DashboardProvider: FC = ({ children }) => { !entitlementsQuery.data || !appearanceQuery.data || !experimentsQuery.data || - !organizationsQuery.data; + !organizationsQuery.data || + !anyOrganizationPermissionsQuery.data; if (isLoading) { return ; @@ -58,6 +68,7 @@ export const DashboardProvider: FC = ({ children }) => { const organizationsEnabled = selectFeatureVisibility( entitlementsQuery.data, ).multiple_organizations; + const showOrganizations = hasMultipleOrganizations || organizationsEnabled; return ( = ({ children }) => { experiments: experimentsQuery.data, appearance: appearanceQuery.data, organizations: organizationsQuery.data, - showOrganizations: hasMultipleOrganizations || organizationsEnabled, + showOrganizations, + canViewOrganizationSettings: + showOrganizations && + canViewAnyOrganization(anyOrganizationPermissionsQuery.data), }} > {children} diff --git a/site/src/modules/dashboard/Navbar/Navbar.tsx b/site/src/modules/dashboard/Navbar/Navbar.tsx index fa249f3a7f..f80887e1f1 100644 --- a/site/src/modules/dashboard/Navbar/Navbar.tsx +++ b/site/src/modules/dashboard/Navbar/Navbar.tsx @@ -12,14 +12,13 @@ export const Navbar: FC = () => { const { metadata } = useEmbeddedMetadata(); const buildInfoQuery = useQuery(buildInfo(metadata["build-info"])); - const { appearance, showOrganizations } = useDashboard(); + const { appearance, canViewOrganizationSettings } = useDashboard(); const { user: me, permissions, signOut } = useAuthenticated(); const featureVisibility = useFeatureVisibility(); const canViewAuditLog = - featureVisibility.audit_log && Boolean(permissions.viewAnyAuditLog); - const canViewDeployment = Boolean(permissions.viewDeploymentValues); - const canViewOrganizations = - Boolean(permissions.editAnyOrganization) && showOrganizations; + featureVisibility.audit_log && permissions.viewAnyAuditLog; + const canViewDeployment = permissions.viewDeploymentValues; + const canViewOrganizations = canViewOrganizationSettings; const proxyContextValue = useProxy(); const canViewHealth = canViewDeployment; diff --git a/site/src/modules/dashboard/Navbar/ProxyMenu.tsx b/site/src/modules/dashboard/Navbar/ProxyMenu.tsx index 5345d3db9c..abbfbd5fd8 100644 --- a/site/src/modules/dashboard/Navbar/ProxyMenu.tsx +++ b/site/src/modules/dashboard/Navbar/ProxyMenu.tsx @@ -99,6 +99,7 @@ export const ProxyMenu: FC = ({ proxyContextValue }) => { ) : ( diff --git a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx index 90ea1dab74..9eb89407de 100644 --- a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx +++ b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx @@ -22,6 +22,7 @@ import { Stack } from "components/Stack/Stack"; import { usePopover } from "components/deprecated/Popover/Popover"; import type { FC } from "react"; import { Link } from "react-router-dom"; + export const Language = { accountLabel: "Account", signOutLabel: "Sign Out", @@ -129,7 +130,7 @@ export const UserDropdownContent: FC = ({ - {Boolean(buildInfo?.deployment_id) && ( + {buildInfo?.deployment_id && (
= ({ text-overflow: ellipsis; `} > - {buildInfo?.deployment_id} + {buildInfo.deployment_id}
; organization?: Organization; + organizationPermissions?: OrganizationPermissions; }>; export const useOrganizationSettings = (): OrganizationSettingsValue => { @@ -36,81 +45,89 @@ export const useOrganizationSettings = (): OrganizationSettingsValue => { return context; }; -/** - * Return true if the user can edit the organization settings or its members. - */ -export const canEditOrganization = ( - permissions: AuthorizationResponse | undefined, -) => { - return ( - permissions !== undefined && - (permissions.editOrganization || - permissions.editMembers || - permissions.editGroups) - ); -}; - const OrganizationSettingsLayout: FC = () => { - const { permissions } = useAuthenticated(); - const { organizations } = useDashboard(); + const { organizations, showOrganizations } = useDashboard(); const { organization: orgName } = useParams() as { organization?: string; }; - const canViewOrganizationSettingsPage = - permissions.viewDeploymentValues || permissions.editAnyOrganization; - const organization = orgName ? organizations.find((org) => org.name === orgName) : undefined; + const orgPermissionsQuery = useQuery( + organizationsPermissions(organizations?.map((o) => o.id)), + ); + + if (orgPermissionsQuery.isError) { + return ; + } + + if (!orgPermissionsQuery.data) { + return ; + } + + const viewableOrganizations = organizations.filter((org) => + canViewOrganization(orgPermissionsQuery.data?.[org.id]), + ); + + // It's currently up to each individual page to show an empty state if there + // is no matching organization. This is weird and we should probably fix it + // eventually, but if we handled it here it would break the /new route, and + // refactoring to fix _that_ is a non-trivial amount of work. + const organizationPermissions = + organization && orgPermissionsQuery.data?.[organization.id]; + if (organization && !canViewOrganization(organizationPermissions)) { + return ; + } + return ( - - -
- - - - Admin Settings - - - - - Organizations - - - {organization && ( - <> - - - - - {organization?.name} - - - - )} - - -
-
- }> - - -
+ +
+ + + + Admin Settings + + + + + Organizations + + + {organization && ( + <> + + + + + {organization.display_name} + + + + )} + + +
+
+ }> + +
- - +
+
); }; diff --git a/site/src/modules/management/OrganizationSidebar.tsx b/site/src/modules/management/OrganizationSidebar.tsx index 8ef14f9baf..3b6451b025 100644 --- a/site/src/modules/management/OrganizationSidebar.tsx +++ b/site/src/modules/management/OrganizationSidebar.tsx @@ -1,59 +1,25 @@ -import { organizationsPermissions } from "api/queries/organizations"; +import { Sidebar as BaseSidebar } from "components/Sidebar/Sidebar"; import { useAuthenticated } from "contexts/auth/RequireAuth"; -import { - canEditOrganization, - useOrganizationSettings, -} from "modules/management/OrganizationSettingsLayout"; +import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; import type { FC } from "react"; -import { useQuery } from "react-query"; -import { useParams } from "react-router-dom"; -import { - OrganizationSidebarView, - type OrganizationWithPermissions, -} from "./OrganizationSidebarView"; +import { OrganizationSidebarView } from "./OrganizationSidebarView"; /** - * A combined deployment settings and organization menu. - * - * This should only be used with multi-org support. If multi-org support is - * disabled or not licensed, this is the wrong sidebar to use. See - * DeploySettingsPage/Sidebar instead. + * Sidebar for the OrganizationSettingsLayout */ export const OrganizationSidebar: FC = () => { const { permissions } = useAuthenticated(); - const { organizations } = useOrganizationSettings(); - const { organization: organizationName } = useParams() as { - organization?: string; - }; - - const orgPermissionsQuery = useQuery( - organizationsPermissions(organizations?.map((o) => o.id)), - ); - - // Sometimes a user can read an organization but cannot actually do anything - // with it. For now, these are filtered out so you only see organizations you - // can manage in some way. - const editableOrgs = organizations - ?.map((org) => { - return { - ...org, - permissions: orgPermissionsQuery.data?.[org.id], - }; - }) - // TypeScript is not able to infer whether permissions are defined on the - // object even if we explicitly check org.permissions here, so add the `is` - // here to help out (canEditOrganization does the actual check). - .filter((org): org is OrganizationWithPermissions => { - return canEditOrganization(org.permissions); - }); - - const organization = editableOrgs?.find((o) => o.name === organizationName); + const { organizations, organization, organizationPermissions } = + useOrganizationSettings(); return ( - + + + ); }; diff --git a/site/src/modules/management/OrganizationSidebarView.stories.tsx b/site/src/modules/management/OrganizationSidebarView.stories.tsx index 6533a5e004..0a3ebef493 100644 --- a/site/src/modules/management/OrganizationSidebarView.stories.tsx +++ b/site/src/modules/management/OrganizationSidebarView.stories.tsx @@ -1,17 +1,16 @@ import type { Meta, StoryObj } from "@storybook/react"; import { expect, userEvent, waitFor, within } from "@storybook/test"; -import type { AuthorizationResponse } from "api/typesGenerated"; +import type { Organization } from "api/typesGenerated"; import { + MockNoOrganizationPermissions, MockNoPermissions, MockOrganization, MockOrganization2, + MockOrganizationPermissions, MockPermissions, } from "testHelpers/entities"; import { withDashboardProvider } from "testHelpers/storybook"; -import { - OrganizationSidebarView, - type OrganizationWithPermissions, -} from "./OrganizationSidebarView"; +import { OrganizationSidebarView } from "./OrganizationSidebarView"; const meta: Meta = { title: "modules/management/OrganizationSidebarView", @@ -20,26 +19,7 @@ const meta: Meta = { parameters: { showOrganizations: true }, args: { activeOrganization: undefined, - organizations: [ - { - ...MockOrganization, - permissions: { - editOrganization: true, - editMembers: true, - editGroups: true, - auditOrganization: true, - }, - }, - { - ...MockOrganization2, - permissions: { - editOrganization: true, - editMembers: true, - editGroups: true, - auditOrganization: true, - }, - }, - ], + organizations: [MockOrganization, MockOrganization2], permissions: MockPermissions, }, }; @@ -47,18 +27,10 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const LoadingOrganizations: Story = { - args: { - organizations: undefined, - }, -}; - export const NoCreateOrg: Story = { args: { - activeOrganization: { - ...MockOrganization, - permissions: { createOrganization: false }, - }, + activeOrganization: MockOrganization, + orgPermissions: MockNoOrganizationPermissions, permissions: { ...MockPermissions, createOrganization: false, @@ -77,23 +49,15 @@ export const NoCreateOrg: Story = { export const OverflowDropdown: Story = { args: { - activeOrganization: { - ...MockOrganization, - permissions: { createOrganization: true }, - }, + activeOrganization: MockOrganization, + orgPermissions: MockOrganizationPermissions, permissions: { ...MockPermissions, createOrganization: true, }, organizations: [ - { - ...MockOrganization, - permissions: {}, - }, - { - ...MockOrganization2, - permissions: {}, - }, + MockOrganization, + MockOrganization2, { id: "my-organization-3-id", name: "my-organization-3", @@ -103,7 +67,6 @@ export const OverflowDropdown: Story = { created_at: "", updated_at: "", is_default: false, - permissions: {}, }, { id: "my-organization-4-id", @@ -114,7 +77,6 @@ export const OverflowDropdown: Story = { created_at: "", updated_at: "", is_default: false, - permissions: {}, }, { id: "my-organization-5-id", @@ -125,7 +87,6 @@ export const OverflowDropdown: Story = { created_at: "", updated_at: "", is_default: false, - permissions: {}, }, { id: "my-organization-6-id", @@ -136,7 +97,6 @@ export const OverflowDropdown: Story = { created_at: "", updated_at: "", is_default: false, - permissions: {}, }, { id: "my-organization-7-id", @@ -147,7 +107,6 @@ export const OverflowDropdown: Story = { created_at: "", updated_at: "", is_default: false, - permissions: {}, }, ], }, @@ -159,129 +118,88 @@ export const OverflowDropdown: Story = { }, }; +export const NoOrganizations: Story = { + args: { + organizations: [], + activeOrganization: undefined, + orgPermissions: MockNoOrganizationPermissions, + permissions: MockNoPermissions, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click( + canvas.getByRole("button", { name: /No organization selected/i }), + ); + }, +}; + +export const NoOtherOrganizations: Story = { + args: { + organizations: [MockOrganization], + activeOrganization: MockOrganization, + orgPermissions: MockNoOrganizationPermissions, + permissions: MockNoPermissions, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click( + canvas.getByRole("button", { name: /My Organization/i }), + ); + }, +}; + export const NoPermissions: Story = { args: { - activeOrganization: { - ...MockOrganization, - permissions: MockNoPermissions, - }, + activeOrganization: MockOrganization, + orgPermissions: MockNoOrganizationPermissions, permissions: MockNoPermissions, }, }; export const AllPermissions: Story = { args: { - activeOrganization: { - ...MockOrganization, - permissions: { - editOrganization: true, - editMembers: true, - editGroups: true, - auditOrganization: true, - assignOrgRole: true, - viewProvisioners: true, - viewIdpSyncSettings: true, - }, - }, - organizations: [ - { - ...MockOrganization, - permissions: { - editOrganization: true, - editMembers: true, - editGroups: true, - auditOrganization: true, - assignOrgRole: true, - viewProvisioners: true, - viewIdpSyncSettings: true, - }, - }, - ], + activeOrganization: MockOrganization, + orgPermissions: MockOrganizationPermissions, + organizations: [MockOrganization], }, }; export const SelectedOrgAdmin: Story = { args: { - activeOrganization: { - ...MockOrganization, - permissions: { - editOrganization: true, - editMembers: true, - editGroups: true, - auditOrganization: true, - assignOrgRole: true, - }, - }, - organizations: [ - { - ...MockOrganization, - permissions: { - editOrganization: true, - editMembers: true, - editGroups: true, - auditOrganization: true, - assignOrgRole: true, - }, - }, - ], + activeOrganization: MockOrganization, + orgPermissions: MockOrganizationPermissions, + organizations: [MockOrganization], }, }; export const SelectedOrgAuditor: Story = { args: { - activeOrganization: { - ...MockOrganization, - permissions: { - editOrganization: false, - editMembers: false, - editGroups: false, - auditOrganization: true, - }, - }, + activeOrganization: MockOrganization, + orgPermissions: MockNoOrganizationPermissions, permissions: { ...MockPermissions, createOrganization: false, }, - organizations: [ - { - ...MockOrganization, - permissions: { - editOrganization: false, - editMembers: false, - editGroups: false, - auditOrganization: true, - }, - }, - ], + organizations: [MockOrganization], }, }; export const SelectedOrgUserAdmin: Story = { args: { - activeOrganization: { - ...MockOrganization, - permissions: { - editOrganization: false, - editMembers: true, - editGroups: true, - auditOrganization: false, - }, + activeOrganization: MockOrganization, + orgPermissions: { + ...MockNoOrganizationPermissions, + viewMembers: true, + viewGroups: true, + viewOrgRoles: true, + viewProvisioners: true, + viewIdpSyncSettings: true, }, permissions: { ...MockPermissions, createOrganization: false, }, - organizations: [ - { - ...MockOrganization, - permissions: { - editOrganization: false, - editMembers: true, - editGroups: true, - auditOrganization: false, - }, - }, - ], + organizations: [MockOrganization], }, }; @@ -291,26 +209,17 @@ export const OrgsDisabled: Story = { }, }; -const commonPerms: AuthorizationResponse = { - editOrganization: true, - editMembers: true, - editGroups: true, - auditOrganization: true, -}; - -const activeOrganization: OrganizationWithPermissions = { +const activeOrganization: Organization = { ...MockOrganization, display_name: "Omega org", name: "omega", id: "1", - permissions: { - ...commonPerms, - }, }; export const OrgsSortedAlphabetically: Story = { args: { activeOrganization, + orgPermissions: MockOrganizationPermissions, permissions: { ...MockPermissions, createOrganization: true, @@ -321,14 +230,12 @@ export const OrgsSortedAlphabetically: Story = { display_name: "Zeta Org", id: "2", name: "zeta", - permissions: commonPerms, }, { ...MockOrganization, display_name: "alpha Org", id: "3", name: "alpha", - permissions: commonPerms, }, activeOrganization, ], @@ -369,14 +276,12 @@ export const SearchForOrg: Story = { display_name: "Zeta Org", id: "2", name: "zeta", - permissions: commonPerms, }, { ...MockOrganization, display_name: "alpha Org", id: "3", name: "fish", - permissions: commonPerms, }, activeOrganization, ], @@ -388,7 +293,7 @@ export const SearchForOrg: Story = { // dropdown is not in #storybook-root so must query full document const globalScreen = within(document.body); const searchInput = - await globalScreen.getByPlaceholderText("Find organization"); + await globalScreen.findByPlaceholderText("Find organization"); await userEvent.type(searchInput, "ALPHA"); diff --git a/site/src/modules/management/OrganizationSidebarView.tsx b/site/src/modules/management/OrganizationSidebarView.tsx index b618c4f72b..7f3b697766 100644 --- a/site/src/modules/management/OrganizationSidebarView.tsx +++ b/site/src/modules/management/OrganizationSidebarView.tsx @@ -1,4 +1,4 @@ -import type { AuthorizationResponse, Organization } from "api/typesGenerated"; +import type { Organization } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; import { Button } from "components/Button/Button"; import { @@ -10,69 +10,25 @@ import { CommandList, CommandSeparator, } from "components/Command/Command"; -import { Loader } from "components/Loader/Loader"; import { Popover, PopoverContent, PopoverTrigger, } from "components/Popover/Popover"; -import { - Sidebar as BaseSidebar, - SettingsSidebarNavItem, -} from "components/Sidebar/Sidebar"; +import { SettingsSidebarNavItem } from "components/Sidebar/Sidebar"; import type { Permissions } from "contexts/auth/permissions"; import { Check, ChevronDown, Plus } from "lucide-react"; -import { useDashboard } from "modules/dashboard/useDashboard"; import { type FC, useState } from "react"; import { useNavigate } from "react-router-dom"; - -export interface OrganizationWithPermissions extends Organization { - permissions: AuthorizationResponse; -} - -interface SidebarProps { - /** The active org name, if any. Overrides activeSettings. */ - activeOrganization: OrganizationWithPermissions | undefined; - /** Organizations and their permissions or undefined if still fetching. */ - organizations: OrganizationWithPermissions[] | undefined; - /** Site-wide permissions. */ - permissions: Permissions; -} - -/** - * Organization settings left sidebar menu. - */ -export const OrganizationSidebarView: FC = ({ - activeOrganization, - organizations, - permissions, -}) => { - const { showOrganizations } = useDashboard(); - - return ( - - {showOrganizations && ( - - )} - - ); -}; - -function urlForSubpage(organizationName: string, subpage = ""): string { - return [`/organizations/${organizationName}`, subpage] - .filter(Boolean) - .join("/"); -} +import type { OrganizationPermissions } from "./organizationPermissions"; interface OrganizationsSettingsNavigationProps { - /** The active org name if an org is being viewed. */ - activeOrganization: OrganizationWithPermissions | undefined; + /** The organization selected from the dropdown */ + activeOrganization: Organization | undefined; + /** Permissions for the active organization */ + orgPermissions: OrganizationPermissions | undefined; /** Organizations and their permissions or undefined if still fetching. */ - organizations: OrganizationWithPermissions[] | undefined; + organizations: readonly Organization[]; /** Site-wide permissions. */ permissions: Permissions; } @@ -83,18 +39,13 @@ interface OrganizationsSettingsNavigationProps { * * If organizations or their permissions are still loading, show a loader. */ -const OrganizationsSettingsNavigation: FC< +export const OrganizationSidebarView: FC< OrganizationsSettingsNavigationProps -> = ({ activeOrganization, organizations, permissions }) => { - // Wait for organizations and their permissions to load - if (!organizations || !activeOrganization) { - return ; - } - +> = ({ activeOrganization, orgPermissions, organizations, permissions }) => { const sortedOrganizations = [...organizations].sort((a, b) => { // active org first - if (a.id === activeOrganization.id) return -1; - if (b.id === activeOrganization.id) return 1; + if (a.id === activeOrganization?.id) return -1; + if (b.id === activeOrganization?.id) return 1; return a.display_name .toLowerCase() @@ -114,16 +65,20 @@ const OrganizationsSettingsNavigation: FC< className="w-60 justify-between p-2 h-11" >
- {activeOrganization && ( - + {activeOrganization ? ( + <> + + + {activeOrganization.display_name || activeOrganization.name} + + + ) : ( + No organization selected )} - - {activeOrganization?.display_name || activeOrganization?.name} -
@@ -134,39 +89,33 @@ const OrganizationsSettingsNavigation: FC< No organization found. - {sortedOrganizations.length > 1 && ( -
- {sortedOrganizations.map((organization) => ( - { - setIsPopoverOpen(false); - navigate(urlForSubpage(organization.name)); - }} - // There is currently an issue with the cmdk component for keyboard navigation - // https://github.com/pacocoursey/cmdk/issues/322 - tabIndex={0} - > - - - {organization?.display_name || organization?.name} - - {activeOrganization.name === organization.name && ( - - )} - - ))} -
- )} +
+ {sortedOrganizations.map((organization) => ( + { + setIsPopoverOpen(false); + navigate(urlForSubpage(organization.name)); + }} + // There is currently an issue with the cmdk component for keyboard navigation + // https://github.com/pacocoursey/cmdk/issues/322 + tabIndex={0} + > + + + {organization?.display_name || organization?.name} + + {activeOrganization?.name === organization.name && ( + + )} + + ))} +
{permissions.createOrganization && ( <> @@ -190,58 +139,69 @@ const OrganizationsSettingsNavigation: FC< - + {activeOrganization && orgPermissions && ( + + )} ); }; +function urlForSubpage(organizationName: string, subpage = ""): string { + return [`/organizations/${organizationName}`, subpage] + .filter(Boolean) + .join("/"); +} + interface OrganizationSettingsNavigationProps { - organization: OrganizationWithPermissions; + organization: Organization; + orgPermissions: OrganizationPermissions; } const OrganizationSettingsNavigation: FC< OrganizationSettingsNavigationProps -> = ({ organization }) => { +> = ({ organization, orgPermissions }) => { return ( <>
- {organization.permissions.editMembers && ( + {orgPermissions.viewMembers && ( Members )} - {organization.permissions.editGroups && ( + {orgPermissions.viewGroups && ( Groups )} - {organization.permissions.assignOrgRole && ( + {orgPermissions.viewOrgRoles && ( Roles )} - {organization.permissions.viewProvisioners && ( - - Provisioners - - )} - {organization.permissions.viewIdpSyncSettings && ( + {orgPermissions.viewProvisioners && + orgPermissions.viewProvisionerJobs && ( + + Provisioners + + )} + {orgPermissions.viewIdpSyncSettings && ( IdP Sync )} - {organization.permissions.editOrganization && ( + {orgPermissions.editSettings && ( diff --git a/site/src/modules/management/organizationPermissions.tsx b/site/src/modules/management/organizationPermissions.tsx new file mode 100644 index 0000000000..2a41485610 --- /dev/null +++ b/site/src/modules/management/organizationPermissions.tsx @@ -0,0 +1,200 @@ +import type { AuthorizationCheck } from "api/typesGenerated"; + +export type OrganizationPermissions = { + [k in OrganizationPermissionName]: boolean; +}; + +export type OrganizationPermissionName = keyof ReturnType< + typeof organizationPermissionChecks +>; + +export const organizationPermissionChecks = (organizationId: string) => + ({ + viewMembers: { + object: { + resource_type: "organization_member", + organization_id: organizationId, + }, + action: "read", + }, + editMembers: { + object: { + resource_type: "organization_member", + organization_id: organizationId, + }, + action: "update", + }, + createGroup: { + object: { + resource_type: "group", + organization_id: organizationId, + }, + action: "create", + }, + viewGroups: { + object: { + resource_type: "group", + organization_id: organizationId, + }, + action: "read", + }, + editGroups: { + object: { + resource_type: "group", + organization_id: organizationId, + }, + action: "update", + }, + editSettings: { + object: { + resource_type: "organization", + organization_id: organizationId, + }, + action: "update", + }, + assignOrgRoles: { + object: { + resource_type: "assign_org_role", + organization_id: organizationId, + }, + action: "assign", + }, + viewOrgRoles: { + object: { + resource_type: "assign_org_role", + organization_id: organizationId, + }, + action: "read", + }, + createOrgRoles: { + object: { + resource_type: "assign_org_role", + organization_id: organizationId, + }, + action: "create", + }, + viewProvisioners: { + object: { + resource_type: "provisioner_daemon", + organization_id: organizationId, + }, + action: "read", + }, + viewProvisionerJobs: { + object: { + resource_type: "provisioner_jobs", + organization_id: organizationId, + }, + action: "read", + }, + viewIdpSyncSettings: { + object: { + resource_type: "idpsync_settings", + organization_id: organizationId, + }, + action: "read", + }, + editIdpSyncSettings: { + object: { + resource_type: "idpsync_settings", + organization_id: organizationId, + }, + action: "update", + }, + }) as const satisfies Record; + +/** + * Checks if the user can view or edit members or groups for the organization + * that produced the given OrganizationPermissions. + */ +export const canViewOrganization = ( + permissions: OrganizationPermissions | undefined, +): permissions is OrganizationPermissions => { + return ( + permissions !== undefined && + (permissions.viewMembers || + permissions.viewGroups || + permissions.viewOrgRoles || + permissions.viewProvisioners || + permissions.viewIdpSyncSettings) + ); +}; + +/** + * Return true if the user can edit the organization settings or its members. + */ +export const canEditOrganization = ( + permissions: OrganizationPermissions | undefined, +): permissions is OrganizationPermissions => { + return ( + permissions !== undefined && + (permissions.editMembers || + permissions.editGroups || + permissions.editSettings || + permissions.assignOrgRoles || + permissions.editIdpSyncSettings || + permissions.createOrgRoles) + ); +}; + +export type AnyOrganizationPermissions = { + [k in AnyOrganizationPermissionName]: boolean; +}; + +export type AnyOrganizationPermissionName = + keyof typeof anyOrganizationPermissionChecks; + +export const anyOrganizationPermissionChecks = { + viewAnyMembers: { + object: { + resource_type: "organization_member", + any_org: true, + }, + action: "read", + }, + editAnyGroups: { + object: { + resource_type: "group", + any_org: true, + }, + action: "update", + }, + assignAnyRoles: { + object: { + resource_type: "assign_org_role", + any_org: true, + }, + action: "assign", + }, + viewAnyIdpSyncSettings: { + object: { + resource_type: "idpsync_settings", + any_org: true, + }, + action: "read", + }, + editAnySettings: { + object: { + resource_type: "organization", + any_org: true, + }, + action: "update", + }, +} as const satisfies Record; + +/** + * Checks if the user can view or edit members or groups for the organization + * that produced the given OrganizationPermissions. + */ +export const canViewAnyOrganization = ( + permissions: AnyOrganizationPermissions | undefined, +): permissions is AnyOrganizationPermissions => { + return ( + permissions !== undefined && + (permissions.viewAnyMembers || + permissions.editAnyGroups || + permissions.assignAnyRoles || + permissions.viewAnyIdpSyncSettings || + permissions.editAnySettings) + ); +}; diff --git a/site/src/pages/AuditPage/AuditPage.tsx b/site/src/pages/AuditPage/AuditPage.tsx index 68f566b4bf..efcf2068f1 100644 --- a/site/src/pages/AuditPage/AuditPage.tsx +++ b/site/src/pages/AuditPage/AuditPage.tsx @@ -1,4 +1,5 @@ import { paginatedAudits } from "api/queries/audits"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; import { useFilter } from "components/Filter/Filter"; import { useUserFilterMenu } from "components/Filter/UserFilter"; import { isNonInitialPage } from "components/PaginationWidget/utils"; @@ -67,6 +68,14 @@ const AuditPage: FC = () => { }), }); + if (auditsQuery.error) { + return ( +
+ +
+ ); + } + return ( <> diff --git a/site/src/pages/DeploymentSettingsPage/NotificationsPage/storybookUtils.ts b/site/src/pages/DeploymentSettingsPage/NotificationsPage/storybookUtils.ts index 4906a5ab54..fc500efd84 100644 --- a/site/src/pages/DeploymentSettingsPage/NotificationsPage/storybookUtils.ts +++ b/site/src/pages/DeploymentSettingsPage/NotificationsPage/storybookUtils.ts @@ -13,7 +13,7 @@ import { withAuthProvider, withDashboardProvider, withGlobalSnackbar, - withManagementSettingsProvider, + withOrganizationSettingsProvider, } from "testHelpers/storybook"; import type { NotificationsPage } from "./NotificationsPage"; @@ -213,6 +213,6 @@ export const baseMeta = { withGlobalSnackbar, withAuthProvider, withDashboardProvider, - withManagementSettingsProvider, + withOrganizationSettingsProvider, ], } satisfies Meta; diff --git a/site/src/pages/GroupsPage/GroupsPage.tsx b/site/src/pages/GroupsPage/GroupsPage.tsx index 5e33e23222..a99ec44334 100644 --- a/site/src/pages/GroupsPage/GroupsPage.tsx +++ b/site/src/pages/GroupsPage/GroupsPage.tsx @@ -1,7 +1,8 @@ import GroupAdd from "@mui/icons-material/GroupAddOutlined"; import { getErrorMessage } from "api/errors"; import { groupsByOrganization } from "api/queries/groups"; -import { organizationPermissions } from "api/queries/organizations"; +import { organizationsPermissions } from "api/queries/organizations"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Button } from "components/Button/Button"; import { EmptyState } from "components/EmptyState/EmptyState"; import { displayError } from "components/GlobalSnackbar/utils"; @@ -23,7 +24,11 @@ export const GroupsPage: FC = () => { const groupsQuery = useQuery( organization ? groupsByOrganization(organization.name) : { enabled: false }, ); - const permissionsQuery = useQuery(organizationPermissions(organization?.id)); + const permissionsQuery = useQuery( + organization + ? organizationsPermissions([organization.id]) + : { enabled: false }, + ); useEffect(() => { if (groupsQuery.error) { @@ -45,11 +50,15 @@ export const GroupsPage: FC = () => { return ; } - const permissions = permissionsQuery.data; - if (!permissions) { + if (permissionsQuery.isLoading) { return ; } + const permissions = permissionsQuery.data?.[organization.id]; + if (!permissions) { + return ; + } + return ( <> diff --git a/site/src/pages/GroupsPage/GroupsPageProvider.tsx b/site/src/pages/GroupsPage/GroupsPageProvider.tsx index 85ccd763be..3697705aeb 100644 --- a/site/src/pages/GroupsPage/GroupsPageProvider.tsx +++ b/site/src/pages/GroupsPage/GroupsPageProvider.tsx @@ -1,13 +1,6 @@ -import type { AuthorizationResponse, Organization } from "api/typesGenerated"; -import { useAuthenticated } from "contexts/auth/RequireAuth"; -import { RequirePermission } from "contexts/auth/RequirePermission"; +import type { Organization } from "api/typesGenerated"; import { useDashboard } from "modules/dashboard/useDashboard"; -import { - type FC, - type PropsWithChildren, - createContext, - useContext, -} from "react"; +import { type FC, createContext, useContext } from "react"; import { Navigate, Outlet, useParams } from "react-router-dom"; export const GroupsPageContext = createContext< diff --git a/site/src/pages/HealthPage/HealthLayout.tsx b/site/src/pages/HealthPage/HealthLayout.tsx index 2c566500d8..c520fd764f 100644 --- a/site/src/pages/HealthPage/HealthLayout.tsx +++ b/site/src/pages/HealthPage/HealthLayout.tsx @@ -7,6 +7,7 @@ import IconButton from "@mui/material/IconButton"; import Tooltip from "@mui/material/Tooltip"; import { health, refreshHealth } from "api/queries/debug"; import type { HealthSeverity } from "api/typesGenerated"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Loader } from "components/Loader/Loader"; import { type ClassName, useClassName } from "hooks/useClassName"; import kebabCase from "lodash/fp/kebabCase"; @@ -22,7 +23,11 @@ import { HealthIcon } from "./Content"; export const HealthLayout: FC = () => { const theme = useTheme(); const queryClient = useQueryClient(); - const { data: healthStatus } = useQuery({ + const { + data: healthStatus, + isLoading, + error, + } = useQuery({ ...health(), refetchInterval: 30_000, }); @@ -42,161 +47,171 @@ export const HealthLayout: FC = () => { const link = useClassName(classNames.link, []); const activeLink = useClassName(classNames.activeLink, []); + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error || !healthStatus) { + return ( +
+ +
+ ); + } + return ( <> {pageTitle("Health")} - {healthStatus ? ( - + +
-
-
-
- +
+
+ - - { - forceRefresh(); - }} - > - {isRefreshing ? ( - - ) : ( - - )} - - -
-
- {healthStatus.healthy ? "Healthy" : "Unhealthy"} -
-
- {healthStatus.healthy - ? Object.keys(visibleSections).some((key) => { - const section = - healthStatus[key as keyof typeof visibleSections]; - return ( - section.warnings && section.warnings.length > 0 - ); - }) - ? "All systems operational, but performance might be degraded" - : "All systems operational" - : "Some issues have been detected"} -
+ + { + forceRefresh(); + }} + > + {isRefreshing ? ( + + ) : ( + + )} + +
- -
- Last check - - {createDayString(healthStatus.time)} - +
+ {healthStatus.healthy ? "Healthy" : "Unhealthy"}
- -
- Version - - {healthStatus.coder_version} - +
+ {healthStatus.healthy + ? Object.keys(visibleSections).some((key) => { + const section = + healthStatus[key as keyof typeof visibleSections]; + return section.warnings && section.warnings.length > 0; + }) + ? "All systems operational, but performance might be degraded" + : "All systems operational" + : "Some issues have been detected"}
-
+
+ + -
- -
- }> - - -
+ )} + + ); + })} +
- - ) : ( - - )} + +
+ }> + + +
+
+ ); }; diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx index 9bb2767968..b9adbb44fe 100644 --- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx @@ -1,11 +1,11 @@ import { getErrorMessage } from "api/errors"; -import { organizationPermissions } from "api/queries/organizations"; import { createOrganizationRole, organizationRoles, updateOrganizationRole, } from "api/queries/roles"; import type { CustomRoleRequest } from "api/typesGenerated"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; import { displayError } from "components/GlobalSnackbar/utils"; import { Loader } from "components/Loader/Loader"; import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; @@ -24,9 +24,7 @@ export const CreateEditRolePage: FC = () => { organization: string; roleName: string; }; - const { organizations } = useOrganizationSettings(); - const organization = organizations?.find((o) => o.name === organizationName); - const permissionsQuery = useQuery(organizationPermissions(organization?.id)); + const { organizationPermissions } = useOrganizationSettings(); const createOrganizationRoleMutation = useMutation( createOrganizationRole(queryClient, organizationName), ); @@ -37,12 +35,15 @@ export const CreateEditRolePage: FC = () => { organizationRoles(organizationName), ); const role = roleData?.find((role) => role.name === roleName); - const permissions = permissionsQuery.data; - if (isLoading || !permissions) { + if (isLoading) { return ; } + if (!organizationPermissions) { + return ; + } + return ( <> @@ -80,7 +81,7 @@ export const CreateEditRolePage: FC = () => { : createOrganizationRoleMutation.isLoading } organizationName={organizationName} - canAssignOrgRole={permissions.assignOrgRole} + canAssignOrgRole={organizationPermissions.assignOrgRoles} /> ); diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx index 905e67ebd2..362448368d 100644 --- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx @@ -1,5 +1,4 @@ import { getErrorMessage } from "api/errors"; -import { organizationPermissions } from "api/queries/organizations"; import { deleteOrganizationRole, organizationRoles } from "api/queries/roles"; import type { Role } from "api/typesGenerated"; import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"; @@ -22,13 +21,10 @@ export const CustomRolesPage: FC = () => { const { organization: organizationName } = useParams() as { organization: string; }; - const { organizations } = useOrganizationSettings(); - const organization = organizations?.find((o) => o.name === organizationName); - const permissionsQuery = useQuery(organizationPermissions(organization?.id)); - const deleteRoleMutation = useMutation( - deleteOrganizationRole(queryClient, organizationName), - ); + const { organizationPermissions } = useOrganizationSettings(); + const [roleToDelete, setRoleToDelete] = useState(); + const organizationRolesQuery = useQuery(organizationRoles(organizationName)); const builtInRoles = organizationRolesQuery.data?.filter( (role) => role.built_in, @@ -36,7 +32,10 @@ export const CustomRolesPage: FC = () => { const customRoles = organizationRolesQuery.data?.filter( (role) => !role.built_in, ); - const permissions = permissionsQuery.data; + + const deleteRoleMutation = useMutation( + deleteOrganizationRole(queryClient, organizationName), + ); useEffect(() => { if (organizationRolesQuery.error) { @@ -49,7 +48,7 @@ export const CustomRolesPage: FC = () => { } }, [organizationRolesQuery.error]); - if (!permissions) { + if (!organizationPermissions) { return ; } @@ -74,7 +73,8 @@ export const CustomRolesPage: FC = () => { builtInRoles={builtInRoles} customRoles={customRoles} onDeleteRole={setRoleToDelete} - canAssignOrgRole={permissions.assignOrgRole} + canAssignOrgRole={organizationPermissions.assignOrgRoles} + canCreateOrgRole={organizationPermissions.createOrgRoles} isCustomRolesEnabled={isCustomRolesEnabled} /> diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx index f37e23a1e9..79319c8886 100644 --- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx @@ -8,44 +8,38 @@ import { CustomRolesPageView } from "./CustomRolesPageView"; const meta: Meta = { title: "pages/OrganizationCustomRolesPage", component: CustomRolesPageView, + args: { + builtInRoles: [MockRoleWithOrgPermissions], + customRoles: [MockRoleWithOrgPermissions], + canAssignOrgRole: true, + canCreateOrgRole: true, + isCustomRolesEnabled: true, + }, }; export default meta; type Story = StoryObj; +export const Enabled: Story = {}; + export const NotEnabled: Story = { args: { - builtInRoles: [MockRoleWithOrgPermissions], - customRoles: [MockRoleWithOrgPermissions], - canAssignOrgRole: true, isCustomRolesEnabled: false, }, }; export const NotEnabledEmptyTable: Story = { args: { - builtInRoles: [MockRoleWithOrgPermissions], customRoles: [], canAssignOrgRole: true, isCustomRolesEnabled: false, }, }; -export const Enabled: Story = { - args: { - builtInRoles: [MockRoleWithOrgPermissions], - customRoles: [MockRoleWithOrgPermissions], - canAssignOrgRole: true, - isCustomRolesEnabled: true, - }, -}; - export const RoleWithoutPermissions: Story = { args: { builtInRoles: [MockOrganizationAuditorRole], customRoles: [MockOrganizationAuditorRole], - canAssignOrgRole: true, - isCustomRolesEnabled: true, }, }; @@ -58,26 +52,19 @@ export const EmptyDisplayName: Story = { display_name: "", }, ], - builtInRoles: [MockRoleWithOrgPermissions], - canAssignOrgRole: true, - isCustomRolesEnabled: true, }, }; export const EmptyTableUserWithoutPermission: Story = { args: { - builtInRoles: [MockRoleWithOrgPermissions], customRoles: [], canAssignOrgRole: false, - isCustomRolesEnabled: true, + canCreateOrgRole: false, }, }; export const EmptyTableUserWithPermission: Story = { args: { - builtInRoles: [MockRoleWithOrgPermissions], customRoles: [], - canAssignOrgRole: true, - isCustomRolesEnabled: true, }, }; diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx index c1aa222370..1bb1f049aa 100644 --- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx @@ -35,6 +35,7 @@ interface CustomRolesPageViewProps { customRoles: AssignableRoles[] | undefined; onDeleteRole: (role: Role) => void; canAssignOrgRole: boolean; + canCreateOrgRole: boolean; isCustomRolesEnabled: boolean; } @@ -43,6 +44,7 @@ export const CustomRolesPageView: FC = ({ customRoles, onDeleteRole, canAssignOrgRole, + canCreateOrgRole, isCustomRolesEnabled, }) => { return ( @@ -66,7 +68,7 @@ export const CustomRolesPageView: FC = ({ permissions. - {canAssignOrgRole && isCustomRolesEnabled && ( + {canCreateOrgRole && isCustomRolesEnabled && ( diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.test.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.test.tsx index 0c9c7d44bd..1270f78484 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.test.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.test.tsx @@ -6,6 +6,7 @@ import { MockEntitlementsWithMultiOrg, MockOrganization, MockOrganizationAuditorRole, + MockOrganizationPermissions, MockUser, } from "testHelpers/entities"; import { @@ -23,10 +24,14 @@ beforeEach(() => { return HttpResponse.json(MockEntitlementsWithMultiOrg); }), http.post("/api/v2/authcheck", async () => { - return HttpResponse.json({ - editMembers: true, - viewDeploymentValues: true, - }); + return HttpResponse.json( + Object.fromEntries( + Object.entries(MockOrganizationPermissions).map(([key, value]) => [ + `${MockOrganization.id}.${key}`, + value, + ]), + ), + ); }), ); }); diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx index ac90365ea4..078ae1a0cb 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx @@ -4,15 +4,14 @@ import { groupsByUserIdInOrganization } from "api/queries/groups"; import { addOrganizationMember, organizationMembers, - organizationPermissions, removeOrganizationMember, updateOrganizationMemberRoles, } from "api/queries/organizations"; import { organizationRoles } from "api/queries/roles"; import type { OrganizationMemberWithUserData, User } from "api/typesGenerated"; import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; +import { EmptyState } from "components/EmptyState/EmptyState"; import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; -import { Loader } from "components/Loader/Loader"; import { Stack } from "components/Stack/Stack"; import { useAuthenticated } from "contexts/auth/RequireAuth"; import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; @@ -25,17 +24,17 @@ import { OrganizationMembersPageView } from "./OrganizationMembersPageView"; const OrganizationMembersPage: FC = () => { const queryClient = useQueryClient(); + const { user: me } = useAuthenticated(); const { organization: organizationName } = useParams() as { organization: string; }; - const { user: me } = useAuthenticated(); - - const groupsByUserIdQuery = useQuery( - groupsByUserIdInOrganization(organizationName), - ); + const { organization, organizationPermissions } = useOrganizationSettings(); const membersQuery = useQuery(organizationMembers(organizationName)); const organizationRolesQuery = useQuery(organizationRoles(organizationName)); + const groupsByUserIdQuery = useQuery( + groupsByUserIdInOrganization(organizationName), + ); const members = membersQuery.data?.map((member) => { const groups = groupsByUserIdQuery.data?.get(member.user_id) ?? []; @@ -52,19 +51,14 @@ const OrganizationMembersPage: FC = () => { updateOrganizationMemberRoles(queryClient, organizationName), ); - const { organizations } = useOrganizationSettings(); - const organization = organizations?.find((o) => o.name === organizationName); - const permissionsQuery = useQuery(organizationPermissions(organization?.id)); - const [memberToDelete, setMemberToDelete] = useState(); - const permissions = permissionsQuery.data; - if (!permissions) { - return ; + if (!organization || !organizationPermissions) { + return ; } - const helmet = organization && ( + const helmet = ( {pageTitle("Members", organization.display_name || organization.name)} @@ -77,9 +71,11 @@ const OrganizationMembersPage: FC = () => { {helmet} <OrganizationMembersPageView allAvailableRoles={organizationRolesQuery.data} - canEditMembers={permissions.editMembers} + canEditMembers={organizationPermissions.editMembers} error={ membersQuery.error ?? + organizationRolesQuery.error ?? + groupsByUserIdQuery.error ?? addMemberMutation.error ?? removeMemberMutation.error ?? updateMemberRolesMutation.error diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx index 72737a92c3..f6c791484e 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx @@ -79,6 +79,7 @@ export const OrganizationMembersPageView: FC< onSubmit={addMember} /> )} + <Table> <TableHeader> <TableRow> diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.test.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationRedirect.test.tsx similarity index 58% rename from site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.test.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationRedirect.test.tsx index 2978702ab9..96e0110d21 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.test.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationRedirect.test.tsx @@ -10,19 +10,29 @@ import { waitForLoaderToBeRemoved, } from "testHelpers/renderHelpers"; import { server } from "testHelpers/server"; -import OrganizationSettingsPage from "./OrganizationSettingsPage"; +import OrganizationRedirect from "./OrganizationRedirect"; jest.spyOn(console, "error").mockImplementation(() => {}); const renderPage = async () => { - renderWithOrganizationSettingsLayout(<OrganizationSettingsPage />, { - route: "/organizations", - path: "/organizations/:organization?", - }); + const { router } = renderWithOrganizationSettingsLayout( + <OrganizationRedirect />, + { + route: "/organizations", + path: "/organizations", + extraRoutes: [ + { + path: "/organizations/:organization", + element: <h1>Organization Settings</h1>, + }, + ], + }, + ); await waitForLoaderToBeRemoved(); + return router; }; -describe("OrganizationSettingsPage", () => { +describe("OrganizationRedirect", () => { it("has no editable organizations", async () => { server.use( http.get("/api/v2/entitlements", () => { @@ -32,9 +42,7 @@ describe("OrganizationSettingsPage", () => { return HttpResponse.json([MockDefaultOrganization, MockOrganization2]); }), http.post("/api/v2/authcheck", async () => { - return HttpResponse.json({ - viewDeploymentValues: true, - }); + return HttpResponse.json({}); }), ); await renderPage(); @@ -52,16 +60,19 @@ describe("OrganizationSettingsPage", () => { }), http.post("/api/v2/authcheck", async () => { return HttpResponse.json({ - [`${MockDefaultOrganization.id}.editOrganization`]: true, - [`${MockOrganization2.id}.editOrganization`]: true, - viewDeploymentValues: true, + viewAnyMembers: true, + [`${MockDefaultOrganization.id}.viewMembers`]: true, + [`${MockDefaultOrganization.id}.editMembers`]: true, + [`${MockOrganization2.id}.viewMembers`]: true, + [`${MockOrganization2.id}.editMembers`]: true, }); }), ); - await renderPage(); - const form = screen.getByTestId("org-settings-form"); - expect(within(form).getByRole("textbox", { name: "Slug" })).toHaveValue( - MockDefaultOrganization.name, + const router = await renderPage(); + const form = screen.getByText("Organization Settings"); + expect(form).toBeInTheDocument(); + expect(router.state.location.pathname).toBe( + `/organizations/${MockDefaultOrganization.name}`, ); }); @@ -75,15 +86,18 @@ describe("OrganizationSettingsPage", () => { }), http.post("/api/v2/authcheck", async () => { return HttpResponse.json({ - [`${MockOrganization2.id}.editOrganization`]: true, - viewDeploymentValues: true, + viewAnyMembers: true, + [`${MockDefaultOrganization.id}.viewMembers`]: true, + [`${MockOrganization2.id}.viewMembers`]: true, + [`${MockOrganization2.id}.editMembers`]: true, }); }), ); - await renderPage(); - const form = screen.getByTestId("org-settings-form"); - expect(within(form).getByRole("textbox", { name: "Slug" })).toHaveValue( - MockOrganization2.name, + const router = await renderPage(); + const form = screen.getByText("Organization Settings"); + expect(form).toBeInTheDocument(); + expect(router.state.location.pathname).toBe( + `/organizations/${MockOrganization2.name}`, ); }); }); diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationRedirect.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationRedirect.tsx new file mode 100644 index 0000000000..b862ad41dc --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/OrganizationRedirect.tsx @@ -0,0 +1,30 @@ +import { EmptyState } from "components/EmptyState/EmptyState"; +import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; +import { canEditOrganization } from "modules/management/organizationPermissions"; +import type { FC } from "react"; +import { Navigate } from "react-router-dom"; + +const OrganizationRedirect: FC = () => { + const { + organizations, + organizationPermissionsByOrganizationId: organizationPermissions, + } = useOrganizationSettings(); + + // Redirect /organizations => /organizations/some-organization-name + // If they can edit the default org, we should redirect to the default. + // If they cannot edit the default, we should redirect to the first org that + // they can edit. + const editableOrg = [...organizations] + .sort((a, b) => (b.is_default ? 1 : 0) - (a.is_default ? 1 : 0)) + .find((org) => canEditOrganization(organizationPermissions[org.id])); + if (editableOrg) { + return <Navigate to={`/organizations/${editableOrg.name}`} replace />; + } + // If they cannot edit any org, just redirect to an org they can read. + if (organizations.length > 0) { + return <Navigate to={`/organizations/${organizations[0].name}`} replace />; + } + return <EmptyState message="No organizations found" />; +}; + +export default OrganizationRedirect; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.stories.tsx deleted file mode 100644 index f6b6b49c88..0000000000 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.stories.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { reactRouterParameters } from "storybook-addon-remix-react-router"; -import { - MockDefaultOrganization, - MockOrganization, - MockOrganization2, - MockUser, -} from "testHelpers/entities"; -import { - withAuthProvider, - withDashboardProvider, - withManagementSettingsProvider, -} from "testHelpers/storybook"; -import OrganizationSettingsPage from "./OrganizationSettingsPage"; - -const meta: Meta<typeof OrganizationSettingsPage> = { - title: "pages/OrganizationSettingsPage", - component: OrganizationSettingsPage, - decorators: [ - withAuthProvider, - withDashboardProvider, - withManagementSettingsProvider, - ], - parameters: { - showOrganizations: true, - user: MockUser, - features: ["multiple_organizations"], - permissions: { viewDeploymentValues: true }, - queries: [ - { - key: ["organizations", [MockDefaultOrganization.id], "permissions"], - data: {}, - }, - ], - }, -}; - -export default meta; -type Story = StoryObj<typeof OrganizationSettingsPage>; - -export const NoRedirectableOrganizations: Story = {}; - -export const OrganizationDoesNotExist: Story = { - parameters: { - reactRouter: reactRouterParameters({ - location: { pathParams: { organization: "does-not-exist" } }, - routing: { path: "/organizations/:organization" }, - }), - }, -}; - -export const CannotEditOrganization: Story = { - parameters: { - reactRouter: reactRouterParameters({ - location: { pathParams: { organization: MockDefaultOrganization.name } }, - routing: { path: "/organizations/:organization" }, - }), - }, -}; - -export const CanEditOrganization: Story = { - parameters: { - reactRouter: reactRouterParameters({ - location: { pathParams: { organization: MockDefaultOrganization.name } }, - routing: { path: "/organizations/:organization" }, - }), - queries: [ - { - key: ["organizations", [MockDefaultOrganization.id], "permissions"], - data: { - [MockDefaultOrganization.id]: { - editOrganization: true, - }, - }, - }, - ], - }, -}; - -export const CanEditOrganizationNotEntitled: Story = { - parameters: { - reactRouter: reactRouterParameters({ - location: { pathParams: { organization: MockDefaultOrganization.name } }, - routing: { path: "/organizations/:organization" }, - }), - features: [], - queries: [ - { - key: ["organizations", [MockDefaultOrganization.id], "permissions"], - data: { - [MockDefaultOrganization.id]: { - editOrganization: true, - }, - }, - }, - ], - }, -}; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx index 698f2ee758..13c339dcc3 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx @@ -1,30 +1,20 @@ import { deleteOrganization, - organizationsPermissions, updateOrganization, } from "api/queries/organizations"; -import { ErrorAlert } from "components/Alert/ErrorAlert"; import { EmptyState } from "components/EmptyState/EmptyState"; import { displaySuccess } from "components/GlobalSnackbar/utils"; -import { Loader } from "components/Loader/Loader"; -import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; -import { canEditOrganization } from "modules/management/OrganizationSettingsLayout"; import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; import type { FC } from "react"; -import { useMutation, useQuery, useQueryClient } from "react-query"; -import { Navigate, useNavigate, useParams } from "react-router-dom"; +import { useMutation, useQueryClient } from "react-query"; +import { useNavigate } from "react-router-dom"; import { OrganizationSettingsPageView } from "./OrganizationSettingsPageView"; -import { OrganizationSummaryPageView } from "./OrganizationSummaryPageView"; const OrganizationSettingsPage: FC = () => { - const { organization: organizationName } = useParams() as { - organization?: string; - }; - const { organizations } = useOrganizationSettings(); - const feats = useFeatureVisibility(); - const navigate = useNavigate(); const queryClient = useQueryClient(); + const { organization, organizationPermissions } = useOrganizationSettings(); + const updateOrganizationMutation = useMutation( updateOrganization(queryClient), ); @@ -32,50 +22,10 @@ const OrganizationSettingsPage: FC = () => { deleteOrganization(queryClient), ); - const organization = organizations?.find((o) => o.name === organizationName); - const permissionsQuery = useQuery( - organizationsPermissions(organizations?.map((o) => o.id)), - ); - - if (permissionsQuery.isLoading) { - return <Loader />; - } - - const permissions = permissionsQuery.data; - if (permissionsQuery.error || !permissions) { - return <ErrorAlert error={permissionsQuery.error} />; - } - - // Redirect /organizations => /organizations/default-org, or if they cannot edit - // the default org, then the first org they can edit, if any. - if (!organizationName) { - // .find will stop at the first match found; make sure default - // organizations are placed first - const editableOrg = [...organizations] - .sort((a, b) => (b.is_default ? 1 : 0) - (a.is_default ? 1 : 0)) - .find((org) => canEditOrganization(permissions[org.id])); - if (editableOrg) { - return <Navigate to={`/organizations/${editableOrg.name}`} replace />; - } - return <EmptyState message="No organizations found" />; - } - - if (!organization) { + if (!organization || !organizationPermissions?.editSettings) { return <EmptyState message="Organization not found" />; } - // The user may not be able to edit this org but they can still see it because - // they can edit members, etc. In this case they will be shown a read-only - // summary page instead of the settings form. - // Similarly, if the feature is not entitled then the user will not be able to - // edit the organization. - if ( - !permissions[organization.id]?.editOrganization || - !feats.multiple_organizations - ) { - return <OrganizationSummaryPageView organization={organization} />; - } - const error = updateOrganizationMutation.error ?? deleteOrganizationMutation.error; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx index 16738ca7dd..08199c0d65 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx @@ -75,7 +75,6 @@ export const OrganizationSettingsPageView: FC< )} <HorizontalForm - data-testid="org-settings-form" onSubmit={form.handleSubmit} aria-label="Organization settings form" > diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSummaryPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSummaryPageView.stories.tsx deleted file mode 100644 index 92567ad99f..0000000000 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSummaryPageView.stories.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { - MockDefaultOrganization, - MockOrganization, -} from "testHelpers/entities"; -import { OrganizationSummaryPageView } from "./OrganizationSummaryPageView"; - -const meta: Meta<typeof OrganizationSummaryPageView> = { - title: "pages/OrganizationSummaryPageView", - component: OrganizationSummaryPageView, - args: { - organization: MockOrganization, - }, -}; - -export default meta; -type Story = StoryObj<typeof OrganizationSummaryPageView>; - -export const DefaultOrg: Story = { - args: { - organization: MockDefaultOrganization, - }, -}; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSummaryPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSummaryPageView.tsx deleted file mode 100644 index c12b3c13a4..0000000000 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSummaryPageView.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import type { Organization } from "api/typesGenerated"; -import { Avatar } from "components/Avatar/Avatar"; -import { - PageHeader, - PageHeaderSubtitle, - PageHeaderTitle, -} from "components/PageHeader/PageHeader"; -import { Stack } from "components/Stack/Stack"; -import type { FC } from "react"; - -interface OrganizationSummaryPageViewProps { - organization: Organization; -} - -export const OrganizationSummaryPageView: FC< - OrganizationSummaryPageViewProps -> = ({ organization }) => { - return ( - <div> - <PageHeader - css={{ - // The deployment settings layout already has padding. - paddingTop: 0, - }} - > - <Stack direction="row"> - <Avatar - size="lg" - variant="icon" - src={organization.icon} - fallback={organization.display_name || organization.name} - /> - - <div> - <PageHeaderTitle> - {organization.display_name || organization.name} - </PageHeaderTitle> - {organization.description && ( - <PageHeaderSubtitle> - {organization.description} - </PageHeaderSubtitle> - )} - </div> - </Stack> - </PageHeader> - You are a member of this organization. - </div> - ); -}; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx index 871eb7b91f..051f916c3a 100644 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx @@ -9,13 +9,13 @@ import { ProvisionerDaemonsPage } from "./ProvisionerDaemonsPage"; import { ProvisionerJobsPage } from "./ProvisionerJobsPage"; const ProvisionersPage: FC = () => { - const { organization } = useOrganizationSettings(); + const { organization, organizationPermissions } = useOrganizationSettings(); const tab = useSearchParamsKey({ key: "tab", defaultValue: "jobs", }); - if (!organization) { + if (!organization || !organizationPermissions?.viewProvisionerJobs) { return ( <> <Helmet> diff --git a/site/src/pages/TerminalPage/TerminalPage.stories.tsx b/site/src/pages/TerminalPage/TerminalPage.stories.tsx index 4fae86ff8b..b9dfeba1d8 100644 --- a/site/src/pages/TerminalPage/TerminalPage.stories.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import { getAuthorizationKey } from "api/queries/authCheck"; +import { anyOrganizationPermissionsKey } from "api/queries/organizations"; import { workspaceByOwnerAndNameKey } from "api/queries/workspaces"; import type { Workspace, WorkspaceAgentLifecycle } from "api/typesGenerated"; import { AuthProvider } from "contexts/auth/AuthProvider"; @@ -76,6 +77,7 @@ const meta = { key: getAuthorizationKey({ checks: permissionsToCheck }), data: { editWorkspaceProxies: true }, }, + { key: anyOrganizationPermissionsKey, data: {} }, ], chromatic: { delay: 300 }, }, diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 1c644f981d..50f47a4721 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -565,6 +565,7 @@ describe("WorkspacePage", () => { experiments: [], organizations: [MockOrganization], showOrganizations: true, + canViewOrganizationSettings: true, }} > {children} diff --git a/site/src/router.tsx b/site/src/router.tsx index 7e7776eeec..85133f7e6e 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -228,6 +228,10 @@ const AddNewLicensePage = lazy( "./pages/DeploymentSettingsPage/LicensesSettingsPage/AddNewLicensePage" ), ); +const OrganizationRedirect = lazy( + () => import("./pages/OrganizationSettingsPage/OrganizationRedirect"), +); + const CreateOrganizationPage = lazy( () => import("./pages/OrganizationSettingsPage/CreateOrganizationPage"), ); @@ -415,7 +419,7 @@ export const router = createBrowserRouter( <Route path="new" element={<CreateOrganizationPage />} /> {/* General settings for the default org can omit the organization name */} - <Route index element={<OrganizationSettingsPage />} /> + <Route index element={<OrganizationRedirect />} /> <Route path=":organization" element={<OrganizationSidebarLayout />}> <Route index element={<OrganizationMembersPage />} /> diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index c866c64f15..74d4de9121 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -8,6 +8,7 @@ import type * as TypesGen from "api/typesGenerated"; import type { Permissions } from "contexts/auth/permissions"; import type { ProxyLatencyReport } from "contexts/useProxyLatency"; import range from "lodash/range"; +import type { OrganizationPermissions } from "modules/management/organizationPermissions"; import type { FileTree } from "utils/filetree"; import type { TemplateVersionFiles } from "utils/templateVersion"; @@ -2836,7 +2837,6 @@ export const MockPermissions: Permissions = { readWorkspaceProxies: true, editWorkspaceProxies: true, createOrganization: true, - editAnyOrganization: true, viewAnyGroup: true, createGroup: true, viewAllLicenses: true, @@ -2844,6 +2844,38 @@ export const MockPermissions: Permissions = { viewOrganizationIDPSyncSettings: true, }; +export const MockOrganizationPermissions: OrganizationPermissions = { + viewMembers: true, + editMembers: true, + createGroup: true, + viewGroups: true, + editGroups: true, + editSettings: true, + viewOrgRoles: true, + createOrgRoles: true, + assignOrgRoles: true, + viewProvisioners: true, + viewProvisionerJobs: true, + viewIdpSyncSettings: true, + editIdpSyncSettings: true, +}; + +export const MockNoOrganizationPermissions: OrganizationPermissions = { + viewMembers: false, + editMembers: false, + createGroup: false, + viewGroups: false, + editGroups: false, + editSettings: false, + viewOrgRoles: false, + createOrgRoles: false, + assignOrgRoles: false, + viewProvisioners: false, + viewProvisionerJobs: false, + viewIdpSyncSettings: false, + editIdpSyncSettings: false, +}; + export const MockNoPermissions: Permissions = { createTemplates: false, createUser: false, @@ -2860,7 +2892,6 @@ export const MockNoPermissions: Permissions = { readWorkspaceProxies: false, editWorkspaceProxies: false, createOrganization: false, - editAnyOrganization: false, viewAnyGroup: false, createGroup: false, viewAllLicenses: false, diff --git a/site/src/testHelpers/storybook.tsx b/site/src/testHelpers/storybook.tsx index f1bdc8fadd..2b81bf16cd 100644 --- a/site/src/testHelpers/storybook.tsx +++ b/site/src/testHelpers/storybook.tsx @@ -17,6 +17,7 @@ import { MockDefaultOrganization, MockDeploymentConfig, MockEntitlements, + MockOrganizationPermissions, } from "./entities"; export const withDashboardProvider = ( @@ -28,6 +29,7 @@ export const withDashboardProvider = ( experiments = [], showOrganizations = false, organizations = [MockDefaultOrganization], + canViewOrganizationSettings = false, } = parameters; const entitlements: Entitlements = { @@ -48,9 +50,10 @@ export const withDashboardProvider = ( value={{ entitlements, experiments, + appearance: MockAppearanceConfig, organizations, showOrganizations, - appearance: MockAppearanceConfig, + canViewOrganizationSettings, }} > <Story /> @@ -153,12 +156,16 @@ export const withGlobalSnackbar = (Story: FC) => ( </> ); -export const withManagementSettingsProvider = (Story: FC) => { +export const withOrganizationSettingsProvider = (Story: FC) => { return ( <OrganizationSettingsContext.Provider value={{ organizations: [MockDefaultOrganization], + organizationPermissionsByOrganizationId: { + [MockDefaultOrganization.id]: MockOrganizationPermissions, + }, organization: MockDefaultOrganization, + organizationPermissions: MockOrganizationPermissions, }} > <DeploymentSettingsContext.Provider diff --git a/site/src/utils/formUtils.ts b/site/src/utils/formUtils.ts index 7ec2f04b23..558bb3eb47 100644 --- a/site/src/utils/formUtils.ts +++ b/site/src/utils/formUtils.ts @@ -12,8 +12,8 @@ const Language = { nameRequired: (name: string): string => { return name ? `Please enter a ${name.toLowerCase()}.` : "Required"; }, - nameInvalidChars: (name: string): string => { - return `${name} must start with a-Z or 0-9 and can contain a-Z, 0-9 or -`; + nameInvalidChars: (): string => { + return "Special characters (e.g.: !, @, #) are not supported"; }, nameTooLong: (name: string, len: number): string => { return `${name} cannot be longer than ${len} characters`; @@ -119,7 +119,7 @@ const displayNameRE = /^[^\s](.*[^\s])?$/; export const nameValidator = (name: string): Yup.StringSchema => Yup.string() .required(Language.nameRequired(name)) - .matches(usernameRE, Language.nameInvalidChars(name)) + .matches(usernameRE, Language.nameInvalidChars()) .max(maxLenName, Language.nameTooLong(name, maxLenName)); export const displayNameValidator = (displayName: string): Yup.StringSchema => diff --git a/tailnet/controllers.go b/tailnet/controllers.go index e0a5766062..832baf09cd 100644 --- a/tailnet/controllers.go +++ b/tailnet/controllers.go @@ -883,23 +883,30 @@ type Workspace struct { } // updateDNSNames updates the DNS names for all agents in the workspace. +// DNS hosts must be all lowercase, or the resolver won't be able to find them. +// Usernames are globally unique & case-insensitive. +// Workspace names are unique per-user & case-insensitive. +// Agent names are unique per-workspace & case-insensitive. func (w *Workspace) updateDNSNames() error { + wsName := strings.ToLower(w.Name) + username := strings.ToLower(w.ownerUsername) for id, a := range w.agents { + agentName := strings.ToLower(a.Name) names := make(map[dnsname.FQDN][]netip.Addr) // TODO: technically, DNS labels cannot start with numbers, but the rules are often not // strictly enforced. - fqdn, err := dnsname.ToFQDN(fmt.Sprintf("%s.%s.me.coder.", a.Name, w.Name)) + fqdn, err := dnsname.ToFQDN(fmt.Sprintf("%s.%s.me.coder.", agentName, wsName)) if err != nil { return err } names[fqdn] = []netip.Addr{CoderServicePrefix.AddrFromUUID(a.ID)} - fqdn, err = dnsname.ToFQDN(fmt.Sprintf("%s.%s.%s.coder.", a.Name, w.Name, w.ownerUsername)) + fqdn, err = dnsname.ToFQDN(fmt.Sprintf("%s.%s.%s.coder.", agentName, wsName, username)) if err != nil { return err } names[fqdn] = []netip.Addr{CoderServicePrefix.AddrFromUUID(a.ID)} if len(w.agents) == 1 { - fqdn, err := dnsname.ToFQDN(fmt.Sprintf("%s.coder.", w.Name)) + fqdn, err := dnsname.ToFQDN(fmt.Sprintf("%s.coder.", wsName)) if err != nil { return err } diff --git a/tailnet/proto/tailnet_drpc_old.go b/tailnet/proto/tailnet_drpc_old.go index dfe902bdd5..c98932c9f4 100644 --- a/tailnet/proto/tailnet_drpc_old.go +++ b/tailnet/proto/tailnet_drpc_old.go @@ -36,7 +36,7 @@ type DRPCTailnetClient23 interface { } // DRPCTailnetClient24 is the Tailnet API at v2.4. It is functionally identical to 2.3, because the -// change was to the Agent API (ResourcesMonitoring methods). +// change was to the Agent API (ResourcesMonitoring and ReportConnection methods). type DRPCTailnetClient24 interface { DRPCTailnetClient23 } diff --git a/tailnet/proto/version.go b/tailnet/proto/version.go index ea38518033..67ed79a040 100644 --- a/tailnet/proto/version.go +++ b/tailnet/proto/version.go @@ -43,6 +43,8 @@ import ( // - Shipped in Coder v2.{{placeholder}} // TODO Vincent: Replace with the correct version // - Added support for GetResourcesMonitoringConfiguration and // PushResourcesMonitoringUsage RPCs on the Agent API. +// - Added support for reporting connection events for auditing via the +// ReportConnection RPC on the Agent API. const ( CurrentMajor = 2 CurrentMinor = 4