diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 59496b017d886..bcb58924e7cba 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -354,8 +354,50 @@ jobs: uses: ./.github/actions/setup-tf - name: Test with PostgreSQL Database + env: + POSTGRES_VERSION: "13" + TS_DEBUG_DISCO: "true" + run: | + make test-postgres + + - name: Upload test stats to Datadog + timeout-minutes: 1 + continue-on-error: true + uses: ./.github/actions/upload-datadog + if: success() || failure() + with: + api-key: ${{ secrets.DATADOG_API_KEY }} + + # NOTE: this could instead be defined as a matrix strategy, but we want to + # only block merging if tests on postgres 13 fail. Using a matrix strategy + # here makes the check in the above `required` job rather complicated. + test-go-pg-16: + runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }} + needs: + - changes + if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main' + # This timeout must be greater than the timeout set by `go test` in + # `make test-postgres` to ensure we receive a trace of running + # goroutines. Setting this to the timeout +5m should work quite well + # even if some of the preceding steps are slow. + timeout-minutes: 25 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup Go + uses: ./.github/actions/setup-go + + - name: Setup Terraform + uses: ./.github/actions/setup-tf + + - name: Test with PostgreSQL Database + env: + POSTGRES_VERSION: "16" + TS_DEBUG_DISCO: "true" run: | - export TS_DEBUG_DISCO=true make test-postgres - name: Upload test stats to Datadog diff --git a/Makefile b/Makefile index 75c5a945e113b..c3059800c7515 100644 --- a/Makefile +++ b/Makefile @@ -36,6 +36,7 @@ GOOS := $(shell go env GOOS) GOARCH := $(shell go env GOARCH) GOOS_BIN_EXT := $(if $(filter windows, $(GOOS)),.exe,) VERSION := $(shell ./scripts/version.sh) +POSTGRES_VERSION ?= 16 # Use the highest ZSTD compression level in CI. ifdef CI @@ -814,7 +815,7 @@ test-migrations: test-postgres-docker # NOTE: we set --memory to the same size as a GitHub runner. test-postgres-docker: - docker rm -f test-postgres-docker || true + docker rm -f test-postgres-docker-${POSTGRES_VERSION} || true docker run \ --env POSTGRES_PASSWORD=postgres \ --env POSTGRES_USER=postgres \ @@ -822,11 +823,11 @@ test-postgres-docker: --env PGDATA=/tmp \ --tmpfs /tmp \ --publish 5432:5432 \ - --name test-postgres-docker \ + --name test-postgres-docker-${POSTGRES_VERSION} \ --restart no \ --detach \ --memory 16GB \ - gcr.io/coder-dev-1/postgres:13 \ + gcr.io/coder-dev-1/postgres:${POSTGRES_VERSION} \ -c shared_buffers=1GB \ -c work_mem=1GB \ -c effective_cache_size=1GB \ diff --git a/cli/ping.go b/cli/ping.go index 82becb016bde7..4d4e9308909dc 100644 --- a/cli/ping.go +++ b/cli/ping.go @@ -58,6 +58,9 @@ func (r *RootCmd) ping() *serpent.Command { _, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.") opts.BlockEndpoints = true } + if !r.noNetworkTelemetry { + opts.EnableTelemetry = true + } conn, err := workspacesdk.New(client).DialAgent(ctx, workspaceAgent.ID, opts) if err != nil { return err diff --git a/cli/portforward.go b/cli/portforward.go index 4c0b1d772eecc..2a0126032e3bb 100644 --- a/cli/portforward.go +++ b/cli/portforward.go @@ -106,6 +106,9 @@ func (r *RootCmd) portForward() *serpent.Command { _, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.") opts.BlockEndpoints = true } + if !r.noNetworkTelemetry { + opts.EnableTelemetry = true + } conn, err := workspacesdk.New(client).DialAgent(ctx, workspaceAgent.ID, opts) if err != nil { return err diff --git a/cli/root.go b/cli/root.go index 073486c640744..ac4bd19d74568 100644 --- a/cli/root.go +++ b/cli/root.go @@ -52,20 +52,21 @@ var ( ) const ( - varURL = "url" - varToken = "token" - varAgentToken = "agent-token" - varAgentTokenFile = "agent-token-file" - varAgentURL = "agent-url" - varHeader = "header" - varHeaderCommand = "header-command" - varNoOpen = "no-open" - varNoVersionCheck = "no-version-warning" - varNoFeatureWarning = "no-feature-warning" - varForceTty = "force-tty" - varVerbose = "verbose" - varOrganizationSelect = "organization" - varDisableDirect = "disable-direct-connections" + varURL = "url" + varToken = "token" + varAgentToken = "agent-token" + varAgentTokenFile = "agent-token-file" + varAgentURL = "agent-url" + varHeader = "header" + varHeaderCommand = "header-command" + varNoOpen = "no-open" + varNoVersionCheck = "no-version-warning" + varNoFeatureWarning = "no-feature-warning" + varForceTty = "force-tty" + varVerbose = "verbose" + varOrganizationSelect = "organization" + varDisableDirect = "disable-direct-connections" + varDisableNetworkTelemetry = "disable-network-telemetry" notLoggedInMessage = "You are not logged in. Try logging in using 'coder login '." @@ -436,6 +437,13 @@ func (r *RootCmd) Command(subcommands []*serpent.Command) (*serpent.Command, err Value: serpent.BoolOf(&r.disableDirect), Group: globalGroup, }, + { + Flag: varDisableNetworkTelemetry, + Env: "CODER_DISABLE_NETWORK_TELEMETRY", + Description: "Disable network telemetry.", + Value: serpent.BoolOf(&r.noNetworkTelemetry), + Group: globalGroup, + }, { Flag: "debug-http", Description: "Debug codersdk HTTP requests.", @@ -491,6 +499,7 @@ type RootCmd struct { versionFlag bool disableDirect bool debugHTTP bool + noNetworkTelemetry bool noVersionCheck bool noFeatureWarning bool diff --git a/cli/speedtest.go b/cli/speedtest.go index 42fe7604c6dc4..9f23d0c58f780 100644 --- a/cli/speedtest.go +++ b/cli/speedtest.go @@ -102,6 +102,9 @@ func (r *RootCmd) speedtest() *serpent.Command { _, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.") opts.BlockEndpoints = true } + if !r.noNetworkTelemetry { + opts.EnableTelemetry = true + } if pcapFile != "" { s := capture.New() opts.CaptureHook = s.LogPacket diff --git a/cli/ssh.go b/cli/ssh.go index e4e9fadf5e8e8..050c7d7fe48c3 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -243,8 +243,9 @@ func (r *RootCmd) ssh() *serpent.Command { } conn, err := workspacesdk.New(client). DialAgent(ctx, workspaceAgent.ID, &workspacesdk.DialAgentOptions{ - Logger: logger, - BlockEndpoints: r.disableDirect, + Logger: logger, + BlockEndpoints: r.disableDirect, + EnableTelemetry: !r.noNetworkTelemetry, }) if err != nil { return xerrors.Errorf("dial agent: %w", err) diff --git a/cli/testdata/coder_--help.golden b/cli/testdata/coder_--help.golden index e970347890eb2..294010e7678e6 100644 --- a/cli/testdata/coder_--help.golden +++ b/cli/testdata/coder_--help.golden @@ -66,6 +66,9 @@ variables or flags. --disable-direct-connections bool, $CODER_DISABLE_DIRECT_CONNECTIONS Disable direct (P2P) connections to workspaces. + --disable-network-telemetry bool, $CODER_DISABLE_NETWORK_TELEMETRY + Disable network telemetry. + --global-config string, $CODER_CONFIG_DIR (default: ~/.config/coderv2) Path to the global `coder` config directory. diff --git a/coderd/agentapi/api.go b/coderd/agentapi/api.go index 37c84a966f4cb..f99731cb78ddf 100644 --- a/coderd/agentapi/api.go +++ b/coderd/agentapi/api.go @@ -22,7 +22,6 @@ import ( "github.com/coder/coder/v2/coderd/database/pubsub" "github.com/coder/coder/v2/coderd/externalauth" "github.com/coder/coder/v2/coderd/prometheusmetrics" - "github.com/coder/coder/v2/coderd/schedule" "github.com/coder/coder/v2/coderd/telemetry" "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/coderd/workspacestats" @@ -62,7 +61,6 @@ type Options struct { Pubsub pubsub.Pubsub DerpMapFn func() *tailcfg.DERPMap TailnetCoordinator *atomic.Pointer[tailnet.Coordinator] - TemplateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore] StatsReporter *workspacestats.Reporter AppearanceFetcher *atomic.Pointer[appearance.Fetcher] PublishWorkspaceUpdateFn func(ctx context.Context, workspaceID uuid.UUID) diff --git a/coderd/agentapi/api_test.go b/coderd/agentapi/api_test.go new file mode 100644 index 0000000000000..98b1e072d65bf --- /dev/null +++ b/coderd/agentapi/api_test.go @@ -0,0 +1,106 @@ +package agentapi_test + +import ( + "context" + "net/url" + "sync/atomic" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "go.uber.org/goleak" + "tailscale.com/tailcfg" + + "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/coderd/agentapi" + "github.com/coder/coder/v2/coderd/appearance" + "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/coderd/externalauth" + "github.com/coder/coder/v2/coderd/prometheusmetrics" + "github.com/coder/coder/v2/coderd/schedule" + "github.com/coder/coder/v2/coderd/telemetry" + "github.com/coder/coder/v2/coderd/workspacestats" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/tailnet" + "github.com/coder/coder/v2/tailnet/tailnettest" + "github.com/coder/coder/v2/testutil" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} + +func Test_APIClose(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitMedium) + log := slogtest.Make(t, nil) + + db, pubsub := dbtestutil.NewDB(t) + fCoord := tailnettest.NewFakeCoordinator() + var coord tailnet.Coordinator = fCoord + coordPtr := atomic.Pointer[tailnet.Coordinator]{} + coordPtr.Store(&coord) + + mockTemplateScheduleStore := schedule.MockTemplateScheduleStore{} + var templateScheduleStore schedule.TemplateScheduleStore = mockTemplateScheduleStore + templateScheduleStorePtr := atomic.Pointer[schedule.TemplateScheduleStore]{} + templateScheduleStorePtr.Store(&templateScheduleStore) + + statsBatcher, closeBatcher, err := workspacestats.NewBatcher(ctx, workspacestats.BatcherWithStore(db)) + require.NoError(t, err) + t.Cleanup(closeBatcher) + statsTracker := workspacestats.NewTracker(db) + t.Cleanup(func() { + _ = statsTracker.Close() + }) + statsReporter := workspacestats.NewReporter(workspacestats.ReporterOptions{ + Database: db, + Logger: log, + Pubsub: pubsub, + TemplateScheduleStore: &templateScheduleStorePtr, + StatsBatcher: statsBatcher, + UsageTracker: statsTracker, + UpdateAgentMetricsFn: func(_ context.Context, _ prometheusmetrics.AgentMetricLabels, _ []*proto.Stats_Metric) {}, + AppStatBatchSize: 0, + }) + + appearanceFetcherPtr := atomic.Pointer[appearance.Fetcher]{} + appearanceFetcherPtr.Store(&appearance.DefaultFetcher) + + api := agentapi.New(agentapi.Options{ + AgentID: uuid.New(), + Ctx: ctx, + Log: log, + Database: db, + Pubsub: pubsub, + DerpMapFn: func() *tailcfg.DERPMap { + return &tailcfg.DERPMap{Regions: map[int]*tailcfg.DERPRegion{999: {RegionCode: "test"}}} + }, + TailnetCoordinator: &coordPtr, + StatsReporter: statsReporter, + AppearanceFetcher: &appearanceFetcherPtr, + PublishWorkspaceUpdateFn: func(_ context.Context, _ uuid.UUID) {}, + NetworkTelemetryBatchFn: func(_ []telemetry.NetworkEvent) {}, + AccessURL: &url.URL{ + Scheme: "http", + Host: "localhost", + }, + AppHostname: "", + AgentStatsRefreshInterval: time.Second, + DisableDirectConnections: false, + DerpForceWebSockets: false, + DerpMapUpdateFrequency: time.Second, + NetworkTelemetryBatchFrequency: time.Second, + NetworkTelemetryBatchMaxSize: 1, + ExternalAuthConfigs: []*externalauth.Config{}, + Experiments: codersdk.Experiments{}, + WorkspaceID: uuid.New(), + UpdateAgentMetricsFn: func(_ context.Context, _ prometheusmetrics.AgentMetricLabels, _ []*proto.Stats_Metric) {}, + }) + + err = api.Close() + require.NoError(t, err) +} diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 7ff97bba2968d..0d923db69d8fc 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -3101,6 +3101,34 @@ const docTemplate = `{ } } }, + "/templates": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Templates" + ], + "summary": "Get all templates", + "operationId": "get-all-templates", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.Template" + } + } + } + } + } + }, "/templates/{template}": { "get": { "security": [ @@ -8719,6 +8747,10 @@ const docTemplate = `{ } ] }, + "organization_id": { + "type": "string", + "format": "uuid" + }, "resource_id": { "type": "string", "format": "uuid" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index bc6fcc19142a9..46caa7d6146da 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -2725,6 +2725,30 @@ } } }, + "/templates": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Templates"], + "summary": "Get all templates", + "operationId": "get-all-templates", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.Template" + } + } + } + } + } + }, "/templates/{template}": { "get": { "security": [ @@ -7762,6 +7786,10 @@ } ] }, + "organization_id": { + "type": "string", + "format": "uuid" + }, "resource_id": { "type": "string", "format": "uuid" diff --git a/coderd/audit.go b/coderd/audit.go index a818f681038ed..37966ead7e916 100644 --- a/coderd/audit.go +++ b/coderd/audit.go @@ -18,6 +18,7 @@ import ( "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/dbauthz" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/searchquery" @@ -61,6 +62,10 @@ func (api *API) auditLogs(rw http.ResponseWriter, r *http.Request) { } dblogs, err := api.Database.GetAuditLogsOffset(ctx, filter) + if dbauthz.IsNotAuthorizedError(err) { + httpapi.Forbidden(rw) + return + } if err != nil { httpapi.InternalServerError(rw, err) return @@ -139,6 +144,9 @@ func (api *API) generateFakeAuditLog(rw http.ResponseWriter, r *http.Request) { if len(params.AdditionalFields) == 0 { params.AdditionalFields = json.RawMessage("{}") } + if params.OrganizationID == uuid.Nil { + params.OrganizationID = uuid.New() + } _, err = api.Database.InsertAuditLog(ctx, database.InsertAuditLogParams{ ID: uuid.New(), @@ -155,7 +163,7 @@ func (api *API) generateFakeAuditLog(rw http.ResponseWriter, r *http.Request) { AdditionalFields: params.AdditionalFields, RequestID: uuid.Nil, // no request ID to attach this to ResourceIcon: "", - OrganizationID: uuid.New(), + OrganizationID: params.OrganizationID, }) if err != nil { httpapi.InternalServerError(rw, err) diff --git a/coderd/audit_test.go b/coderd/audit_test.go index 9de6f65071a02..198b792c44ba8 100644 --- a/coderd/audit_test.go +++ b/coderd/audit_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "net/http" "strconv" "testing" "time" @@ -11,6 +12,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/require" + "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" @@ -135,6 +137,54 @@ func TestAuditLogs(t *testing.T) { require.Equal(t, auditLogs.AuditLogs[0].ResourceLink, fmt.Sprintf("/@%s/%s/builds/%s", workspace.OwnerName, workspace.Name, buildNumberString)) }) + + t.Run("Organization", func(t *testing.T) { + t.Parallel() + + logger := slogtest.Make(t, &slogtest.Options{ + IgnoreErrors: true, + }) + ctx := context.Background() + client := coderdtest.New(t, &coderdtest.Options{ + Logger: &logger, + }) + owner := coderdtest.CreateFirstUser(t, client) + orgAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgAdmin(owner.OrganizationID)) + + err := client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ + ResourceID: owner.UserID, + OrganizationID: owner.OrganizationID, + }) + require.NoError(t, err) + + // Add an extra audit log in another organization + err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ + ResourceID: owner.UserID, + OrganizationID: uuid.New(), + }) + require.NoError(t, err) + + // Fetching audit logs without an organization selector should fail + _, err = orgAdmin.AuditLogs(ctx, codersdk.AuditLogsRequest{ + Pagination: codersdk.Pagination{ + Limit: 5, + }, + }) + var sdkError *codersdk.Error + require.Error(t, err) + require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error") + require.Equal(t, http.StatusForbidden, sdkError.StatusCode()) + + // Using the organization selector allows the org admin to fetch audit logs + alogs, err := orgAdmin.AuditLogs(ctx, codersdk.AuditLogsRequest{ + SearchQuery: fmt.Sprintf("organization_id:%s", owner.OrganizationID.String()), + Pagination: codersdk.Pagination{ + Limit: 5, + }, + }) + require.NoError(t, err) + require.Len(t, alogs.AuditLogs, 1) + }) } func TestAuditLogsFilter(t *testing.T) { diff --git a/coderd/coderd.go b/coderd/coderd.go index 0f573c1921d97..292b0cb171574 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -835,7 +835,7 @@ func New(options *Options) *API { r.Post("/templateversions", api.postTemplateVersionsByOrganization) r.Route("/templates", func(r chi.Router) { r.Post("/", api.postTemplateByOrganization) - r.Get("/", api.templatesByOrganization) + r.Get("/", api.templatesByOrganization()) r.Get("/examples", api.templateExamples) r.Route("/{templatename}", func(r chi.Router) { r.Get("/", api.templateByOrganizationAndName) @@ -877,20 +877,25 @@ func New(options *Options) *API { }) }) }) - r.Route("/templates/{template}", func(r chi.Router) { + r.Route("/templates", func(r chi.Router) { r.Use( apiKeyMiddleware, - httpmw.ExtractTemplateParam(options.Database), ) - r.Get("/daus", api.templateDAUs) - r.Get("/", api.template) - r.Delete("/", api.deleteTemplate) - r.Patch("/", api.patchTemplateMeta) - r.Route("/versions", func(r chi.Router) { - r.Post("/archive", api.postArchiveTemplateVersions) - r.Get("/", api.templateVersionsByTemplate) - r.Patch("/", api.patchActiveTemplateVersion) - r.Get("/{templateversionname}", api.templateVersionByName) + r.Get("/", api.fetchTemplates(nil)) + r.Route("/{template}", func(r chi.Router) { + r.Use( + httpmw.ExtractTemplateParam(options.Database), + ) + r.Get("/daus", api.templateDAUs) + r.Get("/", api.template) + r.Delete("/", api.deleteTemplate) + r.Patch("/", api.patchTemplateMeta) + r.Route("/versions", func(r chi.Router) { + r.Post("/archive", api.postArchiveTemplateVersions) + r.Get("/", api.templateVersionsByTemplate) + r.Patch("/", api.patchActiveTemplateVersion) + r.Get("/{templateversionname}", api.templateVersionByName) + }) }) }) r.Route("/templateversions/{templateversion}", func(r chi.Router) { diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index b0b4adcf844c9..cc638cceb9dab 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1200,12 +1200,21 @@ func (q *querier) GetApplicationName(ctx context.Context) (string, error) { } func (q *querier) GetAuditLogsOffset(ctx context.Context, arg database.GetAuditLogsOffsetParams) ([]database.GetAuditLogsOffsetRow, error) { - // To optimize audit logs, we only check the global audit log permission once. - // This is because we expect a large unbounded set of audit logs, and applying a SQL - // filter would slow down the query for no benefit. - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAuditLog); err != nil { + // To optimize the authz checks for audit logs, do not run an authorize + // check on each individual audit log row. In practice, audit logs are either + // fetched from a global or an organization scope. + // Applying a SQL filter would slow down the query for no benefit on how this query is + // actually used. + + object := rbac.ResourceAuditLog + if arg.OrganizationID != uuid.Nil { + object = object.InOrg(arg.OrganizationID) + } + + if err := q.authorizeContext(ctx, policy.ActionRead, object); err != nil { return nil, err } + return q.db.GetAuditLogsOffset(ctx, arg) } diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index d2d60c3bb6541..830ce9e7241f4 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1928,6 +1928,9 @@ func (q *FakeQuerier) GetAuditLogsOffset(_ context.Context, arg database.GetAudi arg.Offset-- continue } + if arg.OrganizationID != uuid.Nil && arg.OrganizationID != alog.OrganizationID { + continue + } if arg.Action != "" && !strings.Contains(string(alog.Action), arg.Action) { continue } diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 2e1c1ad744978..3902b2e5f8461 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -500,52 +500,58 @@ WHERE resource_id = $4 ELSE true END + -- Filter organization_id + AND CASE + WHEN $5 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + audit_logs.organization_id = $5 + ELSE true + END -- Filter by resource_target AND CASE - WHEN $5 :: text != '' THEN - resource_target = $5 + WHEN $6 :: text != '' THEN + resource_target = $6 ELSE true END -- Filter action AND CASE - WHEN $6 :: text != '' THEN - action = $6 :: audit_action + WHEN $7 :: text != '' THEN + action = $7 :: audit_action ELSE true END -- Filter by user_id AND CASE - WHEN $7 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN - user_id = $7 + WHEN $8 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + user_id = $8 ELSE true END -- Filter by username AND CASE - WHEN $8 :: text != '' THEN - user_id = (SELECT id FROM users WHERE lower(username) = lower($8) AND deleted = false) + WHEN $9 :: text != '' THEN + user_id = (SELECT id FROM users WHERE lower(username) = lower($9) AND deleted = false) ELSE true END -- Filter by user_email AND CASE - WHEN $9 :: text != '' THEN - users.email = $9 + WHEN $10 :: text != '' THEN + users.email = $10 ELSE true END -- Filter by date_from AND CASE - WHEN $10 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN - "time" >= $10 + WHEN $11 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN + "time" >= $11 ELSE true END -- Filter by date_to AND CASE - WHEN $11 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN - "time" <= $11 + WHEN $12 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN + "time" <= $12 ELSE true END -- Filter by build_reason AND CASE - WHEN $12::text != '' THEN - workspace_builds.reason::text = $12 + WHEN $13::text != '' THEN + workspace_builds.reason::text = $13 ELSE true END ORDER BY @@ -561,6 +567,7 @@ type GetAuditLogsOffsetParams struct { Offset int32 `db:"offset" json:"offset"` ResourceType string `db:"resource_type" json:"resource_type"` ResourceID uuid.UUID `db:"resource_id" json:"resource_id"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` ResourceTarget string `db:"resource_target" json:"resource_target"` Action string `db:"action" json:"action"` UserID uuid.UUID `db:"user_id" json:"user_id"` @@ -611,6 +618,7 @@ func (q *sqlQuerier) GetAuditLogsOffset(ctx context.Context, arg GetAuditLogsOff arg.Offset, arg.ResourceType, arg.ResourceID, + arg.OrganizationID, arg.ResourceTarget, arg.Action, arg.UserID, diff --git a/coderd/database/queries/auditlogs.sql b/coderd/database/queries/auditlogs.sql index d05b5bbe371e0..653082bd69fee 100644 --- a/coderd/database/queries/auditlogs.sql +++ b/coderd/database/queries/auditlogs.sql @@ -59,6 +59,12 @@ WHERE resource_id = @resource_id ELSE true END + -- Filter organization_id + AND CASE + WHEN @organization_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + audit_logs.organization_id = @organization_id + ELSE true + END -- Filter by resource_target AND CASE WHEN @resource_target :: text != '' THEN diff --git a/coderd/members.go b/coderd/members.go index e15aa9d4821f9..24f712b8154c7 100644 --- a/coderd/members.go +++ b/coderd/members.go @@ -176,10 +176,11 @@ func (api *API) putMemberRoles(rw http.ResponseWriter, r *http.Request) { apiKey = httpmw.APIKey(r) auditor = api.Auditor.Load() aReq, commitAudit = audit.InitRequest[database.AuditableOrganizationMember](rw, &audit.RequestParams{ - Audit: *auditor, - Log: api.Logger, - Request: r, - Action: database.AuditActionWrite, + OrganizationID: organization.ID, + Audit: *auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, }) ) aReq.Old = member.OrganizationMember.Auditable(member.Username) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 8c8a3f64beed1..413ed999aa6a6 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -598,6 +598,7 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo WorkspaceOwnerSessionToken: sessionToken, WorkspaceOwnerSshPublicKey: ownerSSHPublicKey, WorkspaceOwnerSshPrivateKey: ownerSSHPrivateKey, + WorkspaceBuildId: workspaceBuild.ID.String(), }, LogLevel: input.LogLevel, }, diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index e0403b7c7db2d..36f2ac5f601ce 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -365,6 +365,7 @@ func TestAcquireJob(t *testing.T) { WorkspaceOwnerSessionToken: sessionToken, WorkspaceOwnerSshPublicKey: sshKey.PublicKey, WorkspaceOwnerSshPrivateKey: sshKey.PrivateKey, + WorkspaceBuildId: build.ID.String(), }, }, }) diff --git a/coderd/searchquery/search.go b/coderd/searchquery/search.go index cef971a731cbd..ab44530cc7c4c 100644 --- a/coderd/searchquery/search.go +++ b/coderd/searchquery/search.go @@ -30,6 +30,7 @@ func AuditLogs(query string) (database.GetAuditLogsOffsetParams, []codersdk.Vali const dateLayout = "2006-01-02" parser := httpapi.NewQueryParamParser() filter := database.GetAuditLogsOffsetParams{ + OrganizationID: parser.UUID(values, uuid.Nil, "organization_id"), ResourceID: parser.UUID(values, uuid.Nil, "resource_id"), ResourceTarget: parser.String(values, "", "resource_target"), Username: parser.String(values, "", "username"), diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index d2dfbaba44204..e35475c4add71 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -1240,8 +1240,8 @@ type NetworkEvent struct { Status string `json:"status"` // connected, disconnected DisconnectionReason string `json:"disconnection_reason"` ClientType string `json:"client_type"` // cli, agent, coderd, wsproxy - NodeIDSelf uuid.UUID `json:"node_id_self"` - NodeIDRemote uuid.UUID `json:"node_id_remote"` + NodeIDSelf uint64 `json:"node_id_self"` + NodeIDRemote uint64 `json:"node_id_remote"` P2PEndpoint NetworkEventP2PEndpoint `json:"p2p_endpoint"` LogIPHashes map[string]NetworkEventIPFields `json:"log_ip_hashes"` HomeDERP string `json:"home_derp"` @@ -1272,14 +1272,6 @@ func NetworkEventFromProto(proto *tailnetproto.TelemetryEvent) (NetworkEvent, er if err != nil { return NetworkEvent{}, xerrors.Errorf("parse id %q: %w", proto.Id, err) } - nodeIDSelf, err := uuid.ParseBytes(proto.NodeIdSelf) - if err != nil { - return NetworkEvent{}, xerrors.Errorf("parse node_id_self %q: %w", proto.NodeIdSelf, err) - } - nodeIDRemote, err := uuid.ParseBytes(proto.NodeIdRemote) - if err != nil { - return NetworkEvent{}, xerrors.Errorf("parse node_id_remote %q: %w", proto.NodeIdRemote, err) - } logIPHashes := make(map[string]NetworkEventIPFields, len(proto.LogIpHashes)) for k, v := range proto.LogIpHashes { @@ -1293,8 +1285,8 @@ func NetworkEventFromProto(proto *tailnetproto.TelemetryEvent) (NetworkEvent, er Status: strings.ToLower(proto.Status.String()), DisconnectionReason: proto.DisconnectionReason, ClientType: strings.ToLower(proto.ClientType.String()), - NodeIDSelf: nodeIDSelf, - NodeIDRemote: nodeIDRemote, + NodeIDSelf: proto.NodeIdSelf, + NodeIDRemote: proto.NodeIdRemote, P2PEndpoint: p2pEndpointFromProto(proto.P2PEndpoint), LogIPHashes: logIPHashes, HomeDERP: proto.HomeDerp, diff --git a/coderd/templates.go b/coderd/templates.go index b4c546814737e..3027321fdbba2 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -435,55 +435,78 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque // @Param organization path string true "Organization ID" format(uuid) // @Success 200 {array} codersdk.Template // @Router /organizations/{organization}/templates [get] -func (api *API) templatesByOrganization(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - organization := httpmw.OrganizationParam(r) +func (api *API) templatesByOrganization() http.HandlerFunc { + // TODO: Should deprecate this endpoint and make it akin to /workspaces with + // a filter. There isn't a need to make the organization filter argument + // part of the query url. + // mutate the filter to only include templates from the given organization. + return api.fetchTemplates(func(r *http.Request, arg *database.GetTemplatesWithFilterParams) { + organization := httpmw.OrganizationParam(r) + arg.OrganizationID = organization.ID + }) +} - p := httpapi.NewQueryParamParser() - values := r.URL.Query() +// @Summary Get all templates +// @ID get-all-templates +// @Security CoderSessionToken +// @Produce json +// @Tags Templates +// @Success 200 {array} codersdk.Template +// @Router /templates [get] +func (api *API) fetchTemplates(mutate func(r *http.Request, arg *database.GetTemplatesWithFilterParams)) http.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + p := httpapi.NewQueryParamParser() + values := r.URL.Query() + + deprecated := sql.NullBool{} + if values.Has("deprecated") { + deprecated = sql.NullBool{ + Bool: p.Boolean(values, false, "deprecated"), + Valid: true, + } + } + if len(p.Errors) > 0 { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Invalid query params.", + Validations: p.Errors, + }) + return + } - deprecated := sql.NullBool{} - if values.Has("deprecated") { - deprecated = sql.NullBool{ - Bool: p.Boolean(values, false, "deprecated"), - Valid: true, + prepared, err := api.HTTPAuth.AuthorizeSQLFilter(r, policy.ActionRead, rbac.ResourceTemplate.Type) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error preparing sql filter.", + Detail: err.Error(), + }) + return } - } - if len(p.Errors) > 0 { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Invalid query params.", - Validations: p.Errors, - }) - return - } - prepared, err := api.HTTPAuth.AuthorizeSQLFilter(r, policy.ActionRead, rbac.ResourceTemplate.Type) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error preparing sql filter.", - Detail: err.Error(), - }) - return - } + args := database.GetTemplatesWithFilterParams{ + Deprecated: deprecated, + } + if mutate != nil { + mutate(r, &args) + } - // Filter templates based on rbac permissions - templates, err := api.Database.GetAuthorizedTemplates(ctx, database.GetTemplatesWithFilterParams{ - OrganizationID: organization.ID, - Deprecated: deprecated, - }, prepared) - if errors.Is(err, sql.ErrNoRows) { - err = nil - } + // Filter templates based on rbac permissions + templates, err := api.Database.GetAuthorizedTemplates(ctx, args, prepared) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching templates in organization.", - Detail: err.Error(), - }) - return - } + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching templates in organization.", + Detail: err.Error(), + }) + return + } - httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplates(templates)) + httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplates(templates)) + } } // @Summary Get templates by organization and template name diff --git a/coderd/templates_test.go b/coderd/templates_test.go index 7aebaf41b1e1b..2813f713f5ea2 100644 --- a/coderd/templates_test.go +++ b/coderd/templates_test.go @@ -438,6 +438,42 @@ func TestTemplatesByOrganization(t *testing.T) { templates, err := client.TemplatesByOrganization(ctx, user.OrganizationID) require.NoError(t, err) require.Len(t, templates, 2) + + // Listing all should match + templates, err = client.Templates(ctx) + require.NoError(t, err) + require.Len(t, templates, 2) + }) + t.Run("MultipleOrganizations", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + owner := coderdtest.CreateFirstUser(t, client) + org2 := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{}) + user, _ := coderdtest.CreateAnotherUser(t, client, org2.ID) + + // 2 templates in first organization + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) + version2 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) + coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) + coderdtest.CreateTemplate(t, client, owner.OrganizationID, version2.ID) + + // 2 in the second organization + version3 := coderdtest.CreateTemplateVersion(t, client, org2.ID, nil) + version4 := coderdtest.CreateTemplateVersion(t, client, org2.ID, nil) + coderdtest.CreateTemplate(t, client, org2.ID, version3.ID) + coderdtest.CreateTemplate(t, client, org2.ID, version4.ID) + + ctx := testutil.Context(t, testutil.WaitLong) + + // All 4 are viewable by the owner + templates, err := client.Templates(ctx) + require.NoError(t, err) + require.Len(t, templates, 4) + + // Only 2 are viewable by the org user + templates, err = user.Templates(ctx) + require.NoError(t, err) + require.Len(t, templates, 2) }) } diff --git a/coderd/workspaceagentsrpc.go b/coderd/workspaceagentsrpc.go index 9853d170eff7c..efa187bc14e98 100644 --- a/coderd/workspaceagentsrpc.go +++ b/coderd/workspaceagentsrpc.go @@ -131,7 +131,6 @@ func (api *API) workspaceAgentRPC(rw http.ResponseWriter, r *http.Request) { Pubsub: api.Pubsub, DerpMapFn: api.DERPMap, TailnetCoordinator: &api.TailnetCoordinator, - TemplateScheduleStore: api.TemplateScheduleStore, AppearanceFetcher: &api.AppearanceFetcher, StatsReporter: api.statsReporter, PublishWorkspaceUpdateFn: api.publishWorkspaceUpdate, @@ -157,6 +156,7 @@ func (api *API) workspaceAgentRPC(rw http.ResponseWriter, r *http.Request) { WorkspaceID: build.WorkspaceID, // saves the extra lookup later UpdateAgentMetricsFn: api.UpdateAgentMetrics, }) + defer agentAPI.Close() streamID := tailnet.StreamID{ Name: fmt.Sprintf("%s-%s-%s", owner.Username, workspace.Name, workspaceAgent.Name), diff --git a/codersdk/audit.go b/codersdk/audit.go index b1d525d69179f..683db5406c13f 100644 --- a/codersdk/audit.go +++ b/codersdk/audit.go @@ -161,6 +161,7 @@ type CreateTestAuditLogRequest struct { AdditionalFields json.RawMessage `json:"additional_fields,omitempty"` Time time.Time `json:"time,omitempty" format:"date-time"` BuildReason BuildReason `json:"build_reason,omitempty" enums:"autostart,autostop,initiator"` + OrganizationID uuid.UUID `json:"organization_id,omitempty" format:"uuid"` } // AuditLogs retrieves audit logs from the given page. diff --git a/codersdk/organizations.go b/codersdk/organizations.go index bc9e2514b2c15..e494018258e48 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -362,6 +362,25 @@ func (c *Client) TemplatesByOrganization(ctx context.Context, organizationID uui return templates, json.NewDecoder(res.Body).Decode(&templates) } +// Templates lists all viewable templates +func (c *Client) Templates(ctx context.Context) ([]Template, error) { + res, err := c.Request(ctx, http.MethodGet, + "/api/v2/templates", + nil, + ) + if err != nil { + return nil, xerrors.Errorf("execute request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, ReadBodyAsError(res) + } + + var templates []Template + return templates, json.NewDecoder(res.Body).Decode(&templates) +} + // TemplateByName finds a template inside the organization provided with a case-insensitive name. func (c *Client) TemplateByName(ctx context.Context, organizationID uuid.UUID, name string) (Template, error) { if name == "" { diff --git a/codersdk/workspacesdk/connector.go b/codersdk/workspacesdk/connector.go index d6349adaf6b40..dc4bebd147e09 100644 --- a/codersdk/workspacesdk/connector.go +++ b/codersdk/workspacesdk/connector.go @@ -3,8 +3,10 @@ package workspacesdk import ( "context" "errors" + "fmt" "io" "net/http" + "slices" "sync" "time" @@ -14,6 +16,7 @@ import ( "tailscale.com/tailcfg" "cdr.dev/slog" + "github.com/coder/coder/v2/buildinfo" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/tailnet" "github.com/coder/coder/v2/tailnet/proto" @@ -35,6 +38,7 @@ type tailnetConn interface { // // 1) run the Coordinate API and pass node information back and forth // 2) stream DERPMap updates and program the Conn +// 3) Send network telemetry events // // These functions share the same websocket, and so are combined here so that if we hit a problem // we tear the whole thing down and start over with a new websocket. @@ -56,31 +60,26 @@ type tailnetAPIConnector struct { dialOptions *websocket.DialOptions conn tailnetConn + clientMu sync.RWMutex + client proto.DRPCTailnetClient + connected chan error isFirst bool closed chan struct{} } -// runTailnetAPIConnector creates and runs a tailnetAPIConnector -func runTailnetAPIConnector( - ctx context.Context, logger slog.Logger, - agentID uuid.UUID, coordinateURL string, dialOptions *websocket.DialOptions, - conn tailnetConn, -) *tailnetAPIConnector { - tac := &tailnetAPIConnector{ +// Create a new tailnetAPIConnector without running it +func newTailnetAPIConnector(ctx context.Context, logger slog.Logger, agentID uuid.UUID, coordinateURL string, dialOptions *websocket.DialOptions) *tailnetAPIConnector { + return &tailnetAPIConnector{ ctx: ctx, logger: logger, agentID: agentID, coordinateURL: coordinateURL, dialOptions: dialOptions, - conn: conn, + conn: nil, connected: make(chan error, 1), closed: make(chan struct{}), } - tac.gracefulCtx, tac.cancelGracefulCtx = context.WithCancel(context.Background()) - go tac.manageGracefulTimeout() - go tac.run() - return tac } // manageGracefulTimeout allows the gracefulContext to last 1 second longer than the main context @@ -96,18 +95,33 @@ func (tac *tailnetAPIConnector) manageGracefulTimeout() { } } -func (tac *tailnetAPIConnector) run() { - tac.isFirst = true - defer close(tac.closed) - for retrier := retry.New(50*time.Millisecond, 10*time.Second); retrier.Wait(tac.ctx); { - tailnetClient, err := tac.dial() - if err != nil { - continue +// Runs a tailnetAPIConnector using the provided connection +func (tac *tailnetAPIConnector) runConnector(conn tailnetConn) { + tac.conn = conn + tac.gracefulCtx, tac.cancelGracefulCtx = context.WithCancel(context.Background()) + go tac.manageGracefulTimeout() + go func() { + tac.isFirst = true + defer close(tac.closed) + for retrier := retry.New(50*time.Millisecond, 10*time.Second); retrier.Wait(tac.ctx); { + tailnetClient, err := tac.dial() + if err != nil { + continue + } + tac.clientMu.Lock() + tac.client = tailnetClient + tac.clientMu.Unlock() + tac.logger.Debug(tac.ctx, "obtained tailnet API v2+ client") + tac.coordinateAndDERPMap(tailnetClient) + tac.logger.Debug(tac.ctx, "tailnet API v2+ connection lost") } - tac.logger.Debug(tac.ctx, "obtained tailnet API v2+ client") - tac.coordinateAndDERPMap(tailnetClient) - tac.logger.Debug(tac.ctx, "tailnet API v2+ connection lost") - } + }() +} + +var permanentErrorStatuses = []int{ + http.StatusConflict, // returned if client/agent connections disabled (browser only) + http.StatusBadRequest, // returned if API mismatch + http.StatusNotFound, // returned if user doesn't have permission or agent doesn't exist } func (tac *tailnetAPIConnector) dial() (proto.DRPCTailnetClient, error) { @@ -115,8 +129,18 @@ func (tac *tailnetAPIConnector) dial() (proto.DRPCTailnetClient, error) { // nolint:bodyclose ws, res, err := websocket.Dial(tac.ctx, tac.coordinateURL, tac.dialOptions) if tac.isFirst { - if res != nil && res.StatusCode == http.StatusConflict { + if res != nil && slices.Contains(permanentErrorStatuses, res.StatusCode) { err = codersdk.ReadBodyAsError(res) + // A bit more human-readable help in the case the API version was rejected + var sdkErr *codersdk.Error + if xerrors.As(err, &sdkErr) { + if sdkErr.Message == AgentAPIMismatchMessage && + sdkErr.StatusCode() == http.StatusBadRequest { + sdkErr.Helper = fmt.Sprintf( + "Ensure your client release version (%s, different than the API version) matches the server release version", + buildinfo.Version()) + } + } tac.connected <- err return nil, err } @@ -172,7 +196,10 @@ func (tac *tailnetAPIConnector) coordinateAndDERPMap(client proto.DRPCTailnetCli // we do NOT want to gracefully disconnect on the coordinate() routine. So, we'll just // close the underlying connection. This will trigger a retry of the control plane in // run(). + tac.clientMu.Lock() client.DRPCConn().Close() + tac.client = nil + tac.clientMu.Unlock() // Note that derpMap() logs it own errors, we don't bother here. } }() @@ -236,3 +263,18 @@ func (tac *tailnetAPIConnector) derpMap(client proto.DRPCTailnetClient) error { tac.conn.SetDERPMap(dm) } } + +func (tac *tailnetAPIConnector) SendTelemetryEvent(event *proto.TelemetryEvent) { + tac.clientMu.RLock() + // We hold the lock for the entire telemetry request, but this would only block + // a coordinate retry, and closing the connection. + defer tac.clientMu.RUnlock() + if tac.client == nil { + return + } + ctx, cancel := context.WithTimeout(tac.ctx, 5*time.Second) + defer cancel() + _, _ = tac.client.PostTelemetry(ctx, &proto.TelemetryRequest{ + Events: []*proto.TelemetryEvent{event}, + }) +} diff --git a/codersdk/workspacesdk/connector_internal_test.go b/codersdk/workspacesdk/connector_internal_test.go index 0b75e460ad669..04fb3bc127fdd 100644 --- a/codersdk/workspacesdk/connector_internal_test.go +++ b/codersdk/workspacesdk/connector_internal_test.go @@ -18,6 +18,8 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/apiversion" + "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/tailnet" "github.com/coder/coder/v2/tailnet/proto" @@ -70,7 +72,8 @@ func TestTailnetAPIConnector_Disconnects(t *testing.T) { fConn := newFakeTailnetConn() - uut := runTailnetAPIConnector(ctx, logger, agentID, svr.URL, &websocket.DialOptions{}, fConn) + uut := newTailnetAPIConnector(ctx, logger, agentID, svr.URL, &websocket.DialOptions{}) + uut.runConnector(fConn) call := testutil.RequireRecvCtx(ctx, t, fCoord.CoordinateCalls) reqTun := testutil.RequireRecvCtx(ctx, t, call.Reqs) @@ -97,6 +100,41 @@ func TestTailnetAPIConnector_Disconnects(t *testing.T) { require.NotNil(t, reqDisc.Disconnect) } +func TestTailnetAPIConnector_UplevelVersion(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) + agentID := uuid.UUID{0x55} + + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + sVer := apiversion.New(proto.CurrentMajor, proto.CurrentMinor-1) + + // the following matches what Coderd does; + // c.f. coderd/workspaceagents.go: workspaceAgentClientCoordinate + cVer := r.URL.Query().Get("version") + if err := sVer.Validate(cVer); err != nil { + httpapi.Write(ctx, w, http.StatusBadRequest, codersdk.Response{ + Message: AgentAPIMismatchMessage, + Validations: []codersdk.ValidationError{ + {Field: "version", Detail: err.Error()}, + }, + }) + return + } + })) + + fConn := newFakeTailnetConn() + + uut := runTailnetAPIConnector(ctx, logger, agentID, svr.URL, &websocket.DialOptions{}, fConn) + + err := testutil.RequireRecvCtx(ctx, t, uut.connected) + var sdkErr *codersdk.Error + require.ErrorAs(t, err, &sdkErr) + require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode()) + require.Equal(t, AgentAPIMismatchMessage, sdkErr.Message) + require.NotEmpty(t, sdkErr.Helper) +} + type fakeTailnetConn struct{} func (*fakeTailnetConn) UpdatePeers([]*proto.CoordinateResponse_PeerUpdate) error { diff --git a/codersdk/workspacesdk/workspacesdk.go b/codersdk/workspacesdk/workspacesdk.go index f1e3bd67ea3dc..ca4d8add730cc 100644 --- a/codersdk/workspacesdk/workspacesdk.go +++ b/codersdk/workspacesdk/workspacesdk.go @@ -21,7 +21,6 @@ import ( "cdr.dev/slog" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/tailnet" - "github.com/coder/coder/v2/tailnet/proto" ) // AgentIP is a static IPv6 address with the Tailscale prefix that is used to route @@ -55,6 +54,8 @@ const ( AgentMinimumListeningPort = 9 ) +const AgentAPIMismatchMessage = "Unknown or unsupported API version" + // AgentIgnoredListeningPorts contains a list of ports to ignore when looking for // running applications inside a workspace. We want to ignore non-HTTP servers, // so we pre-populate this list with common ports that are not HTTP servers. @@ -180,6 +181,9 @@ type DialAgentOptions struct { // CaptureHook is a callback that captures Disco packets and packets sent // into the tailnet tunnel. CaptureHook capture.Callback + // Whether the client will send network telemetry events + // Enable instead of Disable so it's initialized to false + EnableTelemetry bool } func (c *Client) DialAgent(dialCtx context.Context, agentID uuid.UUID, options *DialAgentOptions) (agentConn *AgentConn, err error) { @@ -195,29 +199,6 @@ func (c *Client) DialAgent(dialCtx context.Context, agentID uuid.UUID, options * options.BlockEndpoints = true } - ip := tailnet.IP() - var header http.Header - if headerTransport, ok := c.client.HTTPClient.Transport.(*codersdk.HeaderTransport); ok { - header = headerTransport.Header - } - conn, err := tailnet.NewConn(&tailnet.Options{ - Addresses: []netip.Prefix{netip.PrefixFrom(ip, 128)}, - DERPMap: connInfo.DERPMap, - DERPHeader: &header, - DERPForceWebSockets: connInfo.DERPForceWebSockets, - Logger: options.Logger, - BlockEndpoints: c.client.DisableDirectConnections || options.BlockEndpoints, - CaptureHook: options.CaptureHook, - }) - if err != nil { - return nil, xerrors.Errorf("create tailnet: %w", err) - } - defer func() { - if err != nil { - _ = conn.Close() - } - }() - headers := make(http.Header) tokenHeader := codersdk.SessionTokenHeader if c.client.SessionTokenHeader != "" { @@ -239,19 +220,54 @@ func (c *Client) DialAgent(dialCtx context.Context, agentID uuid.UUID, options * return nil, xerrors.Errorf("parse url: %w", err) } q := coordinateURL.Query() - q.Add("version", proto.CurrentVersion.String()) + // TODO (ethanndickson) - the current version includes 2 additions we don't currently use: + // + // 2.1 GetAnnouncementBanners on the Agent API (version locked to Tailnet API) + // 2.2 PostTelemetry on the Tailnet API + // + // So, asking for API 2.2 just makes us incompatible back level servers, for no real benefit. + // As a temporary measure, we'll specifically ask for API version 2.0 until we implement sending + // telemetry. + q.Add("version", "2.0") coordinateURL.RawQuery = q.Encode() - connector := runTailnetAPIConnector(ctx, options.Logger, - agentID, coordinateURL.String(), + connector := newTailnetAPIConnector(ctx, options.Logger, agentID, coordinateURL.String(), &websocket.DialOptions{ HTTPClient: c.client.HTTPClient, HTTPHeader: headers, // Need to disable compression to avoid a data-race. CompressionMode: websocket.CompressionDisabled, - }, - conn, - ) + }) + + ip := tailnet.IP() + var header http.Header + if headerTransport, ok := c.client.HTTPClient.Transport.(*codersdk.HeaderTransport); ok { + header = headerTransport.Header + } + var telemetrySink tailnet.TelemetrySink + if options.EnableTelemetry { + telemetrySink = connector + } + conn, err := tailnet.NewConn(&tailnet.Options{ + Addresses: []netip.Prefix{netip.PrefixFrom(ip, 128)}, + DERPMap: connInfo.DERPMap, + DERPHeader: &header, + DERPForceWebSockets: connInfo.DERPForceWebSockets, + Logger: options.Logger, + BlockEndpoints: c.client.DisableDirectConnections || options.BlockEndpoints, + CaptureHook: options.CaptureHook, + TelemetrySink: telemetrySink, + }) + if err != nil { + return nil, xerrors.Errorf("create tailnet: %w", err) + } + defer func() { + if err != nil { + _ = conn.Close() + } + }() + connector.runConnector(conn) + options.Logger.Debug(ctx, "running tailnet API v2+ connector") select { diff --git a/docker-compose.yaml b/docker-compose.yaml index 9b41c5f47ae61..58692aa73e1f1 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -25,7 +25,9 @@ services: database: condition: service_healthy database: - image: "postgres:14.2" + # Minimum supported version is 13. + # More versions here: https://hub.docker.com/_/postgres + image: "postgres:16" ports: - "5432:5432" environment: diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 161c5bc41213a..305b3c0e733f6 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1181,6 +1181,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "action": "create", "additional_fields": [0], "build_reason": "autostart", + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "resource_type": "template", "time": "2019-08-24T14:15:22Z" @@ -1194,6 +1195,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `action` | [codersdk.AuditAction](#codersdkauditaction) | false | | | | `additional_fields` | array of integer | false | | | | `build_reason` | [codersdk.BuildReason](#codersdkbuildreason) | false | | | +| `organization_id` | string | false | | | | `resource_id` | string | false | | | | `resource_type` | [codersdk.ResourceType](#codersdkresourcetype) | false | | | | `time` | string | false | | | diff --git a/docs/api/templates.md b/docs/api/templates.md index de0498c3de87b..b85811f41d0b8 100644 --- a/docs/api/templates.md +++ b/docs/api/templates.md @@ -617,6 +617,132 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Get all templates + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/templates \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /templates` + +### Example responses + +> 200 Response + +```json +[ + { + "active_user_count": 0, + "active_version_id": "eae64611-bd53-4a80-bb77-df1e432c0fbc", + "activity_bump_ms": 0, + "allow_user_autostart": true, + "allow_user_autostop": true, + "allow_user_cancel_workspace_jobs": true, + "autostart_requirement": { + "days_of_week": ["monday"] + }, + "autostop_requirement": { + "days_of_week": ["monday"], + "weeks": 0 + }, + "build_time_stats": { + "property1": { + "p50": 123, + "p95": 146 + }, + "property2": { + "p50": 123, + "p95": 146 + } + }, + "created_at": "2019-08-24T14:15:22Z", + "created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f", + "created_by_name": "string", + "default_ttl_ms": 0, + "deprecated": true, + "deprecation_message": "string", + "description": "string", + "display_name": "string", + "failure_ttl_ms": 0, + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "max_port_share_level": "owner", + "name": "string", + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "provisioner": "terraform", + "require_active_version": true, + "time_til_dormant_autodelete_ms": 0, + "time_til_dormant_ms": 0, + "updated_at": "2019-08-24T14:15:22Z" + } +] +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | --------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.Template](schemas.md#codersdktemplate) | + +

Response Schema

+ +Status Code **200** + +| Name | Type | Required | Restrictions | Description | +| ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | -------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `[array item]` | array | false | | | +| `» active_user_count` | integer | false | | Active user count is set to -1 when loading. | +| `» active_version_id` | string(uuid) | false | | | +| `» activity_bump_ms` | integer | false | | | +| `» allow_user_autostart` | boolean | false | | Allow user autostart and AllowUserAutostop are enterprise-only. Their values are only used if your license is entitled to use the advanced template scheduling feature. | +| `» allow_user_autostop` | boolean | false | | | +| `» allow_user_cancel_workspace_jobs` | boolean | false | | | +| `» autostart_requirement` | [codersdk.TemplateAutostartRequirement](schemas.md#codersdktemplateautostartrequirement) | false | | | +| `»» days_of_week` | array | false | | Days of week is a list of days of the week in which autostart is allowed to happen. If no days are specified, autostart is not allowed. | +| `» autostop_requirement` | [codersdk.TemplateAutostopRequirement](schemas.md#codersdktemplateautostoprequirement) | false | | Autostop requirement and AutostartRequirement are enterprise features. Its value is only used if your license is entitled to use the advanced template scheduling feature. | +| `»» days_of_week` | array | false | | Days of week is a list of days of the week on which restarts are required. Restarts happen within the user's quiet hours (in their configured timezone). If no days are specified, restarts are not required. Weekdays cannot be specified twice. | +| Restarts will only happen on weekdays in this list on weeks which line up with Weeks. | +| `»» weeks` | integer | false | | Weeks is the number of weeks between required restarts. Weeks are synced across all workspaces (and Coder deployments) using modulo math on a hardcoded epoch week of January 2nd, 2023 (the first Monday of 2023). Values of 0 or 1 indicate weekly restarts. Values of 2 indicate fortnightly restarts, etc. | +| `» build_time_stats` | [codersdk.TemplateBuildTimeStats](schemas.md#codersdktemplatebuildtimestats) | false | | | +| `»» [any property]` | [codersdk.TransitionStats](schemas.md#codersdktransitionstats) | false | | | +| `»»» p50` | integer | false | | | +| `»»» p95` | integer | false | | | +| `» created_at` | string(date-time) | false | | | +| `» created_by_id` | string(uuid) | false | | | +| `» created_by_name` | string | false | | | +| `» default_ttl_ms` | integer | false | | | +| `» deprecated` | boolean | false | | | +| `» deprecation_message` | string | false | | | +| `» description` | string | false | | | +| `» display_name` | string | false | | | +| `» failure_ttl_ms` | integer | false | | Failure ttl ms TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis are enterprise-only. Their values are used if your license is entitled to use the advanced template scheduling feature. | +| `» icon` | string | false | | | +| `» id` | string(uuid) | false | | | +| `» max_port_share_level` | [codersdk.WorkspaceAgentPortShareLevel](schemas.md#codersdkworkspaceagentportsharelevel) | false | | | +| `» name` | string | false | | | +| `» organization_id` | string(uuid) | false | | | +| `» provisioner` | string | false | | | +| `» require_active_version` | boolean | false | | Require active version mandates that workspaces are built with the active template version. | +| `» time_til_dormant_autodelete_ms` | integer | false | | | +| `» time_til_dormant_ms` | integer | false | | | +| `» updated_at` | string(date-time) | false | | | + +#### Enumerated Values + +| Property | Value | +| ---------------------- | --------------- | +| `max_port_share_level` | `owner` | +| `max_port_share_level` | `authenticated` | +| `max_port_share_level` | `public` | +| `provisioner` | `terraform` | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get template metadata by ID ### Code samples diff --git a/docs/cli.md b/docs/cli.md index 70dd29e28b9da..2962425499040 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -149,6 +149,15 @@ Enable verbose output. Disable direct (P2P) connections to workspaces. +### --disable-network-telemetry + +| | | +| ----------- | --------------------------------------------- | +| Type | bool | +| Environment | $CODER_DISABLE_NETWORK_TELEMETRY | + +Disable network telemetry. + ### --global-config | | | diff --git a/dogfood/Dockerfile b/dogfood/Dockerfile index 8bfdb4d2bdb0b..57520ddef67a7 100644 --- a/dogfood/Dockerfile +++ b/dogfood/Dockerfile @@ -146,7 +146,7 @@ RUN apt-get update --quiet && apt-get install --yes \ openssl \ packer \ pkg-config \ - postgresql-13 \ + postgresql-16 \ python3 \ python3-pip \ rsync \ @@ -209,8 +209,8 @@ RUN apt-get update && \ npm cache clean --force # Ensure PostgreSQL binaries are in the users $PATH. -RUN update-alternatives --install /usr/local/bin/initdb initdb /usr/lib/postgresql/13/bin/initdb 100 && \ - update-alternatives --install /usr/local/bin/postgres postgres /usr/lib/postgresql/13/bin/postgres 100 +RUN update-alternatives --install /usr/local/bin/initdb initdb /usr/lib/postgresql/16/bin/initdb 100 && \ + update-alternatives --install /usr/local/bin/postgres postgres /usr/lib/postgresql/16/bin/postgres 100 # Create links for injected dependencies RUN ln --symbolic /var/tmp/coder/coder-cli/coder /usr/local/bin/coder && \ diff --git a/enterprise/cli/testdata/coder_--help.golden b/enterprise/cli/testdata/coder_--help.golden index 7c2ff5c835dff..6895cdde3c9f4 100644 --- a/enterprise/cli/testdata/coder_--help.golden +++ b/enterprise/cli/testdata/coder_--help.golden @@ -30,6 +30,9 @@ variables or flags. --disable-direct-connections bool, $CODER_DISABLE_DIRECT_CONNECTIONS Disable direct (P2P) connections to workspaces. + --disable-network-telemetry bool, $CODER_DISABLE_NETWORK_TELEMETRY + Disable network telemetry. + --global-config string, $CODER_CONFIG_DIR (default: ~/.config/coderv2) Path to the global `coder` config directory. diff --git a/enterprise/coderd/workspaceagents_test.go b/enterprise/coderd/workspaceagents_test.go index 17b38a0332570..12c308987fd14 100644 --- a/enterprise/coderd/workspaceagents_test.go +++ b/enterprise/coderd/workspaceagents_test.go @@ -46,8 +46,9 @@ func TestBlockNonBrowser(t *testing.T) { }, }) r := setupWorkspaceAgent(t, client, user, 0) + ctx := testutil.Context(t, testutil.WaitShort) //nolint:gocritic // Testing that even the owner gets blocked. - _, err := workspacesdk.New(client).DialAgent(context.Background(), r.sdkAgent.ID, nil) + _, err := workspacesdk.New(client).DialAgent(ctx, r.sdkAgent.ID, nil) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusConflict, apiErr.StatusCode()) @@ -65,8 +66,9 @@ func TestBlockNonBrowser(t *testing.T) { }, }) r := setupWorkspaceAgent(t, client, user, 0) + ctx := testutil.Context(t, testutil.WaitShort) //nolint:gocritic // Testing RBAC is not the point of this test. - conn, err := workspacesdk.New(client).DialAgent(context.Background(), r.sdkAgent.ID, nil) + conn, err := workspacesdk.New(client).DialAgent(ctx, r.sdkAgent.ID, nil) require.NoError(t, err) _ = conn.Close() }) diff --git a/flake.nix b/flake.nix index ca31783e36045..ee6fbca7bd923 100644 --- a/flake.nix +++ b/flake.nix @@ -59,7 +59,7 @@ pixman pkg-config playwright-driver.browsers - postgresql_13 + postgresql_16 protobuf protoc-gen-go ripgrep @@ -97,7 +97,7 @@ name = "coder-${osArch}"; # Updated with ./scripts/update-flake.sh`. # This should be updated whenever go.mod changes! - vendorHash = "sha256-+K95kbYNMKiYk7obN2gjNCtBvaXBWPLKm12N6cF9ImQ="; + vendorHash = "sha256-e0L6osJwG0EF0M3TefxaAjDvN4jvQHxTGEUEECNO1Vw="; proxyVendor = true; src = ./.; nativeBuildInputs = with pkgs; [ getopt openssl zstd ]; diff --git a/provisioner/terraform/provision.go b/provisioner/terraform/provision.go index 20135c6034818..cd28f93854c0f 100644 --- a/provisioner/terraform/provision.go +++ b/provisioner/terraform/provision.go @@ -210,6 +210,7 @@ func provisionEnv( "CODER_WORKSPACE_TEMPLATE_ID="+metadata.GetTemplateId(), "CODER_WORKSPACE_TEMPLATE_NAME="+metadata.GetTemplateName(), "CODER_WORKSPACE_TEMPLATE_VERSION="+metadata.GetTemplateVersion(), + "CODER_WORKSPACE_BUILD_ID="+metadata.GetWorkspaceBuildId(), ) for key, value := range provisionersdk.AgentScriptEnv() { env = append(env, key+"="+value) diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index 8e8b2ca085eb3..39fe557ae43e1 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -1640,6 +1640,7 @@ type Metadata struct { WorkspaceOwnerGroups []string `protobuf:"bytes,14,rep,name=workspace_owner_groups,json=workspaceOwnerGroups,proto3" json:"workspace_owner_groups,omitempty"` WorkspaceOwnerSshPublicKey string `protobuf:"bytes,15,opt,name=workspace_owner_ssh_public_key,json=workspaceOwnerSshPublicKey,proto3" json:"workspace_owner_ssh_public_key,omitempty"` WorkspaceOwnerSshPrivateKey string `protobuf:"bytes,16,opt,name=workspace_owner_ssh_private_key,json=workspaceOwnerSshPrivateKey,proto3" json:"workspace_owner_ssh_private_key,omitempty"` + WorkspaceBuildId string `protobuf:"bytes,17,opt,name=workspace_build_id,json=workspaceBuildId,proto3" json:"workspace_build_id,omitempty"` } func (x *Metadata) Reset() { @@ -1786,6 +1787,13 @@ func (x *Metadata) GetWorkspaceOwnerSshPrivateKey() string { return "" } +func (x *Metadata) GetWorkspaceBuildId() string { + if x != nil { + return x.WorkspaceBuildId + } + return "" +} + // Config represents execution configuration shared by all subsequent requests in the Session type Config struct { state protoimpl.MessageState @@ -2900,7 +2908,7 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x73, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, - 0x73, 0x4e, 0x75, 0x6c, 0x6c, 0x22, 0xc1, 0x06, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x73, 0x4e, 0x75, 0x6c, 0x6c, 0x22, 0xef, 0x06, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x53, 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x72, 0x61, @@ -2952,143 +2960,146 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x73, 0x73, 0x68, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x53, 0x73, 0x68, 0x50, - 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x22, 0x8a, 0x01, 0x0a, 0x06, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x17, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, - 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x15, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x53, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x12, 0x14, 0x0a, 0x05, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, - 0x74, 0x65, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x5f, 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x13, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x4c, 0x6f, - 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x0e, 0x0a, 0x0c, 0x50, 0x61, 0x72, 0x73, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xa3, 0x02, 0x0a, 0x0d, 0x50, 0x61, 0x72, 0x73, 0x65, - 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x4c, - 0x0a, 0x12, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, - 0x62, 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, - 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x11, 0x74, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, - 0x72, 0x65, 0x61, 0x64, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x72, 0x65, - 0x61, 0x64, 0x6d, 0x65, 0x12, 0x54, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x70, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, - 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0d, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, 0x1a, 0x40, 0x0a, 0x12, 0x57, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, 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, 0xb5, 0x02, 0x0a, - 0x0b, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, - 0x53, 0x0a, 0x15, 0x72, 0x69, 0x63, 0x68, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, - 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, - 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, - 0x13, 0x72, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, - 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, - 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x76, 0x61, 0x72, 0x69, 0x61, - 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x59, 0x0a, 0x17, 0x65, 0x78, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x70, 0x72, 0x6f, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x15, 0x65, - 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x73, 0x22, 0xf8, 0x01, 0x0a, 0x0c, 0x50, 0x6c, 0x61, 0x6e, 0x43, 0x6f, 0x6d, - 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x33, 0x0a, 0x09, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, - 0x12, 0x3a, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, - 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x61, 0x0a, 0x17, - 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x78, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x15, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x22, - 0x41, 0x0a, 0x0c, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x12, 0x77, 0x6f, 0x72, + 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, + 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x42, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, 0x22, 0x8a, 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x12, 0x36, 0x0a, 0x17, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x15, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x53, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, + 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x5f, + 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x13, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x4c, + 0x65, 0x76, 0x65, 0x6c, 0x22, 0x0e, 0x0a, 0x0c, 0x50, 0x61, 0x72, 0x73, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x22, 0xa3, 0x02, 0x0a, 0x0d, 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, + 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x4c, 0x0a, 0x12, + 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, + 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, + 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x11, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, + 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, + 0x61, 0x64, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x72, 0x65, 0x61, 0x64, + 0x6d, 0x65, 0x12, 0x54, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, + 0x74, 0x61, 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, + 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, 0x1a, 0x40, 0x0a, 0x12, 0x57, 0x6f, 0x72, 0x6b, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, 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, 0xb5, 0x02, 0x0a, 0x0b, 0x50, + 0x6c, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x53, 0x0a, + 0x15, 0x72, 0x69, 0x63, 0x68, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, + 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x13, 0x72, + 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, + 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, + 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x59, 0x0a, 0x17, 0x65, 0x78, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, + 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x15, 0x65, 0x78, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x73, 0x22, 0xf8, 0x01, 0x0a, 0x0c, 0x50, 0x6c, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, + 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x3a, + 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0a, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x61, 0x0a, 0x17, 0x65, 0x78, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x15, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x22, 0x41, 0x0a, + 0x0c, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, + 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x22, 0x8f, 0x02, 0x0a, 0x0d, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x33, + 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x22, 0x8f, 0x02, 0x0a, 0x0d, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x43, 0x6f, 0x6d, 0x70, - 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, - 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, - 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, - 0x73, 0x12, 0x61, 0x0a, 0x17, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, - 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, - 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x15, 0x65, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, + 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, + 0x74, 0x65, 0x72, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, + 0x61, 0x0a, 0x17, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, + 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x29, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x73, 0x22, 0x0f, 0x0a, 0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x8c, 0x02, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x2d, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x12, 0x31, 0x0a, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, - 0x72, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x05, 0x70, 0x61, - 0x72, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x04, 0x70, - 0x6c, 0x61, 0x6e, 0x12, 0x31, 0x0a, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, - 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x12, 0x34, 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x48, 0x00, 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x06, 0x0a, 0x04, - 0x74, 0x79, 0x70, 0x65, 0x22, 0xd1, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, - 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x32, 0x0a, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, - 0x74, 0x65, 0x48, 0x00, 0x52, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x04, 0x70, - 0x6c, 0x61, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, - 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x32, 0x0a, 0x05, - 0x61, 0x70, 0x70, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x43, - 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, - 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x3f, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, - 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x00, 0x12, - 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, - 0x46, 0x4f, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x03, 0x12, 0x09, - 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, 0x3b, 0x0a, 0x0f, 0x41, 0x70, 0x70, - 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, - 0x4f, 0x57, 0x4e, 0x45, 0x52, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x55, 0x54, 0x48, 0x45, - 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, - 0x42, 0x4c, 0x49, 0x43, 0x10, 0x02, 0x2a, 0x37, 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, 0x0a, - 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f, 0x50, - 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x02, 0x32, - 0x49, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x12, 0x3a, - 0x0a, 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x30, 0x5a, 0x2e, 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, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x65, 0x72, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x15, 0x65, 0x78, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x73, 0x22, 0x0f, 0x0a, 0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x22, 0x8c, 0x02, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x2d, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x31, + 0x0a, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x05, 0x70, 0x61, 0x72, 0x73, + 0x65, 0x12, 0x2e, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x6c, + 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, + 0x6e, 0x12, 0x31, 0x0a, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, + 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x05, 0x61, + 0x70, 0x70, 0x6c, 0x79, 0x12, 0x34, 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x48, 0x00, 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x22, 0xd1, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, + 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x32, 0x0a, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, + 0x48, 0x00, 0x52, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x04, 0x70, 0x6c, 0x61, + 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x32, 0x0a, 0x05, 0x61, 0x70, + 0x70, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x43, 0x6f, 0x6d, + 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x42, 0x06, + 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x3f, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, + 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, + 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, + 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, + 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, 0x3b, 0x0a, 0x0f, 0x41, 0x70, 0x70, 0x53, 0x68, + 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x57, + 0x4e, 0x45, 0x52, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, + 0x49, 0x43, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, + 0x49, 0x43, 0x10, 0x02, 0x2a, 0x37, 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, 0x0a, 0x05, 0x53, + 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f, 0x50, 0x10, 0x01, + 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x02, 0x32, 0x49, 0x0a, + 0x0b, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x12, 0x3a, 0x0a, 0x07, + 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x30, 0x5a, 0x2e, 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, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index a17b78bcc3ba4..71d8bd12e8d1e 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -228,6 +228,7 @@ message Metadata { repeated string workspace_owner_groups = 14; string workspace_owner_ssh_public_key = 15; string workspace_owner_ssh_private_key = 16; + string workspace_build_id = 17; } // Config represents execution configuration shared by all subsequent requests in the Session diff --git a/site/e2e/provisionerGenerated.ts b/site/e2e/provisionerGenerated.ts index 5c3d7ebc53512..fa98b21b7fd37 100644 --- a/site/e2e/provisionerGenerated.ts +++ b/site/e2e/provisionerGenerated.ts @@ -233,6 +233,7 @@ export interface Metadata { workspaceOwnerGroups: string[]; workspaceOwnerSshPublicKey: string; workspaceOwnerSshPrivateKey: string; + workspaceBuildId: string; } /** Config represents execution configuration shared by all subsequent requests in the Session */ @@ -850,6 +851,9 @@ export const Metadata = { if (message.workspaceOwnerSshPrivateKey !== "") { writer.uint32(130).string(message.workspaceOwnerSshPrivateKey); } + if (message.workspaceBuildId !== "") { + writer.uint32(138).string(message.workspaceBuildId); + } return writer; }, }; diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index cdcacf1823edb..4f4b4c8333304 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -282,6 +282,7 @@ export interface CreateTestAuditLogRequest { readonly additional_fields?: Record; readonly time?: string; readonly build_reason?: BuildReason; + readonly organization_id?: string; } // From codersdk/apikey.go diff --git a/tailnet/conn.go b/tailnet/conn.go index 8b82c455e4788..3a2843539ed04 100644 --- a/tailnet/conn.go +++ b/tailnet/conn.go @@ -99,6 +99,15 @@ type Options struct { // ForceNetworkUp forces the network to be considered up. magicsock will not // do anything if it thinks it can't reach the internet. ForceNetworkUp bool + // TelemetrySink is optional. + TelemetrySink TelemetrySink +} + +// TelemetrySink allows tailnet.Conn to send network telemetry to the Coder +// server. +type TelemetrySink interface { + // SendTelemetryEvent sends a telemetry event to some external sink. + SendTelemetryEvent(event *proto.TelemetryEvent) } // NodeID creates a Tailscale NodeID from the last 8 bytes of a UUID. It ensures @@ -113,6 +122,13 @@ func NodeID(uid uuid.UUID) tailcfg.NodeID { return tailcfg.NodeID(id) } +func (c *Conn) sendTelemetryEvent(e *proto.TelemetryEvent) { + if c.telemetrySink == nil { + return + } + c.telemetrySink.SendTelemetryEvent(e) +} + // NewConn constructs a new Wireguard server that will accept connections from the addresses provided. func NewConn(options *Options) (conn *Conn, err error) { if options == nil { @@ -259,6 +275,7 @@ func NewConn(options *Options) (conn *Conn, err error) { wireguardEngine: wireguardEngine, configMaps: cfgMaps, nodeUpdater: nodeUp, + telemetrySink: options.TelemetrySink, } defer func() { if err != nil { @@ -316,6 +333,7 @@ type Conn struct { wireguardRouter *router.Config wireguardEngine wgengine.Engine listeners map[listenKey]*listener + telemetrySink TelemetrySink trafficStats *connstats.Statistics } diff --git a/tailnet/logger.go b/tailnet/logger.go new file mode 100644 index 0000000000000..044fcde0a9ca3 --- /dev/null +++ b/tailnet/logger.go @@ -0,0 +1,200 @@ +package tailnet + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "io" + "net/netip" + "regexp" + "strings" + "sync" + + "golang.org/x/xerrors" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/sloghuman" + "github.com/coder/coder/v2/cryptorand" + "github.com/coder/coder/v2/tailnet/proto" +) + +var ipv4And6Regex = regexp.MustCompile(`(((::ffff:)?(25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}|([a-f0-9:]+:+)+[a-f0-9]+)`) + +// Used to store a number of slog logger, and a logger sink for creating network telemetry events +type multiLogger struct { + loggers []slog.Logger +} + +func newMultiLogger(loggers ...slog.Logger) multiLogger { + return multiLogger{loggers: loggers} +} + +func (m multiLogger) appendLogger(logger slog.Logger) multiLogger { + return multiLogger{loggers: append(m.loggers, logger)} +} + +func (m multiLogger) Critical(ctx context.Context, msg string, fields ...any) { + for _, i := range m.loggers { + i.Critical(ctx, msg, fields...) + } +} + +func (m multiLogger) Debug(ctx context.Context, msg string, fields ...any) { + for _, i := range m.loggers { + i.Debug(ctx, msg, fields...) + } +} + +func (m multiLogger) Error(ctx context.Context, msg string, fields ...any) { + for _, i := range m.loggers { + i.Error(ctx, msg, fields...) + } +} + +func (m multiLogger) Fatal(ctx context.Context, msg string, fields ...any) { + for _, i := range m.loggers { + i.Fatal(ctx, msg, fields...) + } +} + +func (m multiLogger) Info(ctx context.Context, msg string, fields ...any) { + for _, i := range m.loggers { + i.Info(ctx, msg, fields...) + } +} + +func (m multiLogger) Warn(ctx context.Context, msg string, fields ...any) { + for _, i := range m.loggers { + i.Warn(ctx, msg, fields...) + } +} + +func (m multiLogger) Named(name string) multiLogger { + var loggers []slog.Logger + for _, i := range m.loggers { + loggers = append(loggers, i.Named(name)) + } + return multiLogger{loggers: loggers} +} + +func (m multiLogger) With(fields ...slog.Field) multiLogger { + var loggers []slog.Logger + for _, i := range m.loggers { + loggers = append(loggers, i.With(fields...)) + } + return multiLogger{loggers: loggers} +} + +// A logger sink that extracts (anonymized) IP addresses from logs for building +// network telemetry events +type bufferLogSink struct { + sink slog.Sink + mu sync.Mutex + logs []string + // We use the same salt so the same IP hashes to the same value. + hashSalt string + // A cache to avoid hashing the same IP multiple times. + ipToHash map[string]string + hashedIPs map[string]*proto.IPFields +} + +var _ slog.Sink = &bufferLogSink{} + +var _ io.Writer = &bufferLogSink{} + +func newBufferLogSink() (*bufferLogSink, error) { + hashSalt, err := cryptorand.String(16) + if err != nil { + return nil, err + } + out := &bufferLogSink{ + logs: []string{}, + hashSalt: hashSalt, + ipToHash: make(map[string]string), + hashedIPs: make(map[string]*proto.IPFields), + } + out.sink = sloghuman.Sink(out) + return out, nil +} + +func (b *bufferLogSink) getLogs() ([]string, map[string]*proto.IPFields) { + b.mu.Lock() + defer b.mu.Unlock() + return append([]string{}, b.logs...), b.hashedIPs +} + +// Write implements io.Writer. +func (b *bufferLogSink) Write(p []byte) (n int, err error) { + b.mu.Lock() + defer b.mu.Unlock() + + // sloghuman writes a full log line in a single Write call with a trailing + // newline. + logLine := strings.TrimSuffix(string(p), "\n") + + logLineSplit := strings.SplitN(logLine, "]", 2) + logLineAfterLevel := logLine + if len(logLineAfterLevel) == 2 { + logLineAfterLevel = logLineSplit[1] + } + for _, match := range ipv4And6Regex.FindAllString(logLineAfterLevel, -1) { + hash, err := b.processIPLocked(match) + if err == nil { + logLine = strings.ReplaceAll(logLine, match, hash) + } + } + + b.logs = append(b.logs, logLine) + return len(p), nil +} + +// LogEntry implements slog.Sink. +func (b *bufferLogSink) LogEntry(ctx context.Context, e slog.SinkEntry) { + // This will call (*bufferLogSink).Write + b.sink.LogEntry(ctx, e) +} + +// Sync implements slog.Sink. +func (b *bufferLogSink) Sync() { + b.sink.Sync() +} + +// processIPLocked will look up the IP in the cache, or hash and salt it and add +// to the cache. It will also add it to hashedIPs. +// +// b.mu must be held. +func (b *bufferLogSink) processIPLocked(ip string) (string, error) { + if hashStr, ok := b.ipToHash[ip]; ok { + return hashStr, nil + } + + addr, err := netip.ParseAddr(ip) + if err != nil { + return "", xerrors.Errorf("failed to parse IP %q: %w", ip, err) + } + version := int32(4) + if addr.Is6() { + version = 6 + } + + class := proto.IPFields_PUBLIC + switch { + case addr.IsLoopback(): + class = proto.IPFields_LOOPBACK + case addr.IsLinkLocalUnicast(): + class = proto.IPFields_LINK_LOCAL + case addr.IsLinkLocalMulticast(): + class = proto.IPFields_LINK_LOCAL + case addr.IsPrivate(): + class = proto.IPFields_PRIVATE + } + + hash := sha256.Sum256([]byte(b.hashSalt + ip)) + hashStr := hex.EncodeToString(hash[:]) + b.ipToHash[ip] = hashStr + b.hashedIPs[hashStr] = &proto.IPFields{ + Version: version, + Class: class, + } + return hashStr, nil +} diff --git a/tailnet/logger_internal_test.go b/tailnet/logger_internal_test.go new file mode 100644 index 0000000000000..7b0b253c39672 --- /dev/null +++ b/tailnet/logger_internal_test.go @@ -0,0 +1,145 @@ +package tailnet + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/tailnet/proto" + + "cdr.dev/slog" +) + +func TestBufferLogSink(t *testing.T) { + t.Parallel() + + t.Run("NoIP", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + sink, err := newBufferLogSink() + require.NoError(t, err) + logger := slog.Make(sink).Leveled(slog.LevelDebug) + + logger.Debug(ctx, "line1") + logger.Debug(ctx, "line2 fe80") + logger.Debug(ctx, "line3 xxxx::x") + + logs, hashes := sink.getLogs() + require.Len(t, logs, 3) + require.Len(t, hashes, 0) + require.Contains(t, logs[0], "line1") + require.Contains(t, logs[1], "line2 fe80") + require.Contains(t, logs[2], "line3 xxxx::x") + }) + + t.Run("OneOrMoreIPs", func(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + ip string + expectedVersion int32 + expectedClass proto.IPFields_IPClass + }{ + { + name: "IPv4/Public", + ip: "142.250.71.78", + expectedVersion: 4, + expectedClass: proto.IPFields_PUBLIC, + }, + { + name: "IPv4/Private", + ip: "192.168.0.1", + expectedVersion: 4, + expectedClass: proto.IPFields_PRIVATE, + }, + { + name: "IPv4/LinkLocal", + ip: "169.254.1.1", + expectedVersion: 4, + expectedClass: proto.IPFields_LINK_LOCAL, + }, + { + name: "IPv4/Loopback", + ip: "127.0.0.1", + expectedVersion: 4, + expectedClass: proto.IPFields_LOOPBACK, + }, + { + name: "IPv6/Public", + ip: "2404:6800:4006:812::200e", + expectedVersion: 6, + expectedClass: proto.IPFields_PUBLIC, + }, + { + name: "IPv6/Private", + ip: "fd12:3456:789a:1::1", + expectedVersion: 6, + expectedClass: proto.IPFields_PRIVATE, + }, + { + name: "IPv6/LinkLocal", + ip: "fe80::1", + expectedVersion: 6, + expectedClass: proto.IPFields_LINK_LOCAL, + }, + { + name: "IPv6/Loopback", + ip: "::1", + expectedVersion: 6, + expectedClass: proto.IPFields_LOOPBACK, + }, + { + name: "IPv6/IPv4Mapped", + ip: "::ffff:1.2.3.4", + expectedVersion: 6, + expectedClass: proto.IPFields_PUBLIC, + }, + } + + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { + t.Parallel() + ctx := context.Background() + sink, err := newBufferLogSink() + require.NoError(t, err) + logger := slog.Make(sink).Leveled(slog.LevelDebug) + + ipWithPort := c.ip + ":8080" + if c.expectedVersion == 6 { + ipWithPort = fmt.Sprintf("[%s]:8080", c.ip) + } + + logger.Debug(ctx, "line1", slog.F("ip", c.ip)) + logger.Debug(ctx, fmt.Sprintf("line2: %s/24", c.ip)) + logger.Debug(ctx, fmt.Sprintf("line3: %s foo (%s)", ipWithPort, c.ip)) + + logs, hashes := sink.getLogs() + require.Len(t, logs, 3) + require.Len(t, hashes, 1) + for _, log := range logs { + t.Log(log) + } + + // This only runs once since we only processed a single IP. + for expectedHash, ipFields := range hashes { + hashedIPWithPort := expectedHash + ":8080" + if c.expectedVersion == 6 { + hashedIPWithPort = fmt.Sprintf("[%s]:8080", expectedHash) + } + + require.Contains(t, logs[0], "line1") + require.Contains(t, logs[0], "ip="+expectedHash) + require.Contains(t, logs[1], fmt.Sprintf("line2: %s/24", expectedHash)) + require.Contains(t, logs[2], fmt.Sprintf("line3: %s foo (%s)", hashedIPWithPort, expectedHash)) + + require.Equal(t, c.expectedVersion, ipFields.Version) + require.Equal(t, c.expectedClass, ipFields.Class) + } + }) + } + }) +} diff --git a/tailnet/proto/tailnet.pb.go b/tailnet/proto/tailnet.pb.go index f293b7ba69f55..a9268f04d992c 100644 --- a/tailnet/proto/tailnet.pb.go +++ b/tailnet/proto/tailnet.pb.go @@ -835,8 +835,8 @@ type TelemetryEvent struct { Status TelemetryEvent_Status `protobuf:"varint,4,opt,name=status,proto3,enum=coder.tailnet.v2.TelemetryEvent_Status" json:"status,omitempty"` DisconnectionReason string `protobuf:"bytes,5,opt,name=disconnection_reason,json=disconnectionReason,proto3" json:"disconnection_reason,omitempty"` ClientType TelemetryEvent_ClientType `protobuf:"varint,6,opt,name=client_type,json=clientType,proto3,enum=coder.tailnet.v2.TelemetryEvent_ClientType" json:"client_type,omitempty"` - NodeIdSelf []byte `protobuf:"bytes,7,opt,name=node_id_self,json=nodeIdSelf,proto3" json:"node_id_self,omitempty"` - NodeIdRemote []byte `protobuf:"bytes,8,opt,name=node_id_remote,json=nodeIdRemote,proto3" json:"node_id_remote,omitempty"` + NodeIdSelf uint64 `protobuf:"varint,7,opt,name=node_id_self,json=nodeIdSelf,proto3" json:"node_id_self,omitempty"` + NodeIdRemote uint64 `protobuf:"varint,8,opt,name=node_id_remote,json=nodeIdRemote,proto3" json:"node_id_remote,omitempty"` P2PEndpoint *TelemetryEvent_P2PEndpoint `protobuf:"bytes,9,opt,name=p2p_endpoint,json=p2pEndpoint,proto3" json:"p2p_endpoint,omitempty"` LogIpHashes map[string]*IPFields `protobuf:"bytes,10,rep,name=log_ip_hashes,json=logIpHashes,proto3" json:"log_ip_hashes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` HomeDerp string `protobuf:"bytes,11,opt,name=home_derp,json=homeDerp,proto3" json:"home_derp,omitempty"` @@ -925,18 +925,18 @@ func (x *TelemetryEvent) GetClientType() TelemetryEvent_ClientType { return TelemetryEvent_CLI } -func (x *TelemetryEvent) GetNodeIdSelf() []byte { +func (x *TelemetryEvent) GetNodeIdSelf() uint64 { if x != nil { return x.NodeIdSelf } - return nil + return 0 } -func (x *TelemetryEvent) GetNodeIdRemote() []byte { +func (x *TelemetryEvent) GetNodeIdRemote() uint64 { if x != nil { return x.NodeIdRemote } - return nil + return 0 } func (x *TelemetryEvent) GetP2PEndpoint() *TelemetryEvent_P2PEndpoint { @@ -2046,10 +2046,10 @@ var file_tailnet_proto_tailnet_proto_rawDesc = []byte{ 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0a, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, 0x0c, 0x6e, 0x6f, 0x64, - 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x73, 0x65, 0x6c, 0x66, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x73, 0x65, 0x6c, 0x66, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x53, 0x65, 0x6c, 0x66, 0x12, 0x24, 0x0a, 0x0e, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x52, 0x65, 0x6d, 0x6f, 0x74, + 0x01, 0x28, 0x04, 0x52, 0x0c, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x12, 0x4f, 0x0a, 0x0c, 0x70, 0x32, 0x70, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, diff --git a/tailnet/proto/tailnet.proto b/tailnet/proto/tailnet.proto index b4e4b50bbe992..b8e97d8a7a493 100644 --- a/tailnet/proto/tailnet.proto +++ b/tailnet/proto/tailnet.proto @@ -170,8 +170,8 @@ message TelemetryEvent { Status status = 4; string disconnection_reason = 5; ClientType client_type = 6; - bytes node_id_self = 7; - bytes node_id_remote = 8; + uint64 node_id_self = 7; + uint64 node_id_remote = 8; P2PEndpoint p2p_endpoint = 9; map log_ip_hashes = 10; string home_derp = 11;